diff --git a/Tests/kstars_ui/test_ekos.cpp b/Tests/kstars_ui/test_ekos.cpp index e2557bc14..508248aaf 100644 --- a/Tests/kstars_ui/test_ekos.cpp +++ b/Tests/kstars_ui/test_ekos.cpp @@ -1,339 +1,337 @@ /* KStars UI tests Copyright (C) 2018, 2020 Csaba Kertesz Jasem Mutlaq Eric Dejouhanet This application 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. */ #include "test_ekos.h" #if defined(HAVE_INDI) #include "kstars_ui_tests.h" #include "test_kstars_startup.h" #include "ekos/manager.h" #include "ekos/profileeditor.h" #include "kstars.h" #include "auxiliary/ksmessagebox.h" #include #include #include #include #include #include #include #include #include #include TestEkos::TestEkos(QObject *parent): QObject(parent) { } void TestEkos::initTestCase() { } void TestEkos::cleanupTestCase() { } void TestEkos::init() { KTRY_OPEN_EKOS(); KVERIFY_EKOS_IS_OPENED(); } void TestEkos::cleanup() { foreach (QDialog * d, KStars::Instance()->findChildren()) if (d->isVisible()) d->hide(); KTRY_CLOSE_EKOS(); KVERIFY_EKOS_IS_HIDDEN(); } void TestEkos::testOpenClose() { /* No-op, we just use init+cleanup */ } void TestEkos::testSimulatorProfile() { Ekos::Manager * const ekos = Ekos::Manager::Instance(); // --------- First step: selecting the Simulators profile // Verify that the test profile exists, and select it QString const p("Simulators"); QComboBox* profileCBox = Ekos::Manager::Instance()->findChild("profileCombo"); QVERIFY(profileCBox != nullptr); profileCBox->setCurrentText(p); QTRY_COMPARE(profileCBox->currentText(), p); // --------- Second step: starting Ekos with the Simulators profile QString const buttonReadyToStart("media-playback-start"); QString const buttonReadyToStop("media-playback-stop"); // Check the start button icon as visual feedback about Ekos state, and click to start Ekos QPushButton* startEkos = ekos->findChild("processINDIB"); QVERIFY(startEkos != nullptr); QVERIFY(!buttonReadyToStart.compare(startEkos->icon().name())); QTest::mouseClick(startEkos, Qt::LeftButton); // --------- Third step: waiting for Ekos to finish startup // The INDI property pages automatically raised on top, but as we got a handle to the button, we continue to test as is // Wait until Ekos gives feedback on the INDI client startup - button changes to symbol "stop" QTRY_VERIFY_WITH_TIMEOUT(!buttonReadyToStop.compare(startEkos->icon().name()), 7000); // It might be that our Simulators profile is not auto-connecting and we should do that manually // We assume that by default it's not the case // We have no feedback on whether all our devices are connected, so we need to hack a delay in... QWARN("HACK HACK HACK adding delay here for devices to connect"); QTest::qWait(5000); // Verify the device connection button is unavailable QPushButton * connectDevices = ekos->findChild("connectB"); QVERIFY(connectDevices != nullptr); QVERIFY(!connectDevices->isEnabled()); #if QT_VERSION >= 0x050800 QEXPECT_FAIL("", "Ekos resets the simulation clock when starting a profile.", Continue); QCOMPARE(llround(KStars::Instance()->data()->clock()->utc().toLocalTime().toMSecsSinceEpoch()/1000.0), TestKStarsStartup::m_InitialConditions.dateTime.toSecsSinceEpoch()); #endif QEXPECT_FAIL("", "Ekos resumes the simulation clock when starting a profile.", Continue); QVERIFY(!KStars::Instance()->data()->clock()->isActive()); // --------- Fourth step: waiting for Ekos to finish stopping // Start button that became a stop button is now disabled - we need to disconnect devices first QVERIFY(!startEkos->isEnabled()); // Verify the device disconnection button is available, disconnect devices QPushButton * const b = ekos->findChild("disconnectB"); QVERIFY(b != nullptr); QVERIFY(b->isEnabled()); QTimer::singleShot(200, Ekos::Manager::Instance(), [&] { QTest::mouseClick(b, Qt::LeftButton); }); - QWARN("Intentionally leaving a delay here for BZ398192"); - QTest::qWait(5000); // --------- Fifth step: waiting for Ekos to finish stopping // Start button that became a stop button has to be available - QTRY_VERIFY_WITH_TIMEOUT(startEkos->isEnabled(), 7000); + QTRY_VERIFY_WITH_TIMEOUT(startEkos->isEnabled(), 10000); // Hang INDI client up QTimer::singleShot(200, ekos, [&] { QTest::mouseClick(startEkos, Qt::LeftButton); }); - QTRY_VERIFY_WITH_TIMEOUT(!buttonReadyToStart.compare(startEkos->icon().name()), 7000); + QTRY_VERIFY_WITH_TIMEOUT(!buttonReadyToStart.compare(startEkos->icon().name()), 10000); } void TestEkos::testManipulateProfiles() { // Because we don't want to manage the order of tests, we do the profile manipulation in three steps of the same test // We use that poor man's shared variable to hold the result of the first (creation) and second (edition) test step. // The ProfileEditor is exec()'d, so test code must be made asynchronous, and QTimer::singleShot is an easy way to do that. // We use two timers, one to run the end-user test, which eventually will close the dialog, and a second one to really close if the test step fails. bool testIsSuccessful = false; Ekos::Manager * const ekos = Ekos::Manager::Instance(); QString testProfileName = QString("testUI%1").arg(rand() % 100000); // FIXME: Move this to fixtures // --------- First step: creating the profile // Because the dialog is modal, the remainder of the test is made asynchronous QTimer::singleShot(1000, ekos, [&] { // Find the Profile Editor dialog ProfileEditor* profileEditor = ekos->findChild("profileEditorDialog"); /* // If the name of the widget is unknown, use this snippet to look it up from its class if (profileEditor == nullptr) foreach (QWidget *w, QApplication::topLevelWidgets()) if (w->inherits("ProfileEditor")) profileEditor = qobject_cast (w); */ QVERIFY(profileEditor != nullptr); // Create a test profile QLineEdit * const profileNameLE = profileEditor->findChild("profileIN"); QVERIFY(nullptr != profileNameLE); profileNameLE->setText(testProfileName); QCOMPARE(profileNameLE->text(), testProfileName); // Setting an item programmatically in a treeview combobox... QComboBox * const mountCBox = profileEditor->findChild("mountCombo"); QVERIFY(nullptr != mountCBox); QString lookup("Telescope Simulator"); // FIXME: Move this to fixtures // Match the text recursively in the model, this results in a model index with a parent QModelIndexList const list = mountCBox->model()->match(mountCBox->model()->index(0, 0), Qt::DisplayRole, QVariant::fromValue(lookup), 1, Qt::MatchRecursive); QVERIFY(0 < list.count()); QModelIndex const &item = list.first(); //QWARN(QString("Found text '%1' at #%2, parent at #%3").arg(item.data().toString()).arg(item.row()).arg(item.parent().row()).toStdString().data()); QCOMPARE(list.value(0).data().toString(), lookup); QVERIFY(!item.parent().parent().isValid()); // Now set the combobox model root to the match's parent mountCBox->setRootModelIndex(item.parent()); // And set the text as if the end-user had selected it mountCBox->setCurrentText(lookup); QCOMPARE(mountCBox->currentText(), lookup); QComboBox * const ccdCBox = profileEditor->findChild("ccdCombo"); QVERIFY(nullptr != ccdCBox); lookup = "CCD Simulator"; ccdCBox->setCurrentText(lookup); // FIXME: Move this to fixtures QCOMPARE(ccdCBox->currentText(), lookup); // Save the profile using the "Save" button QDialogButtonBox* buttons = profileEditor->findChild("dialogButtons"); QVERIFY(nullptr != buttons); QTest::mouseClick(buttons->button(QDialogButtonBox::Save), Qt::LeftButton); testIsSuccessful = true; }); // Cancel the Profile Editor dialog if the test failed - this will happen after pushing the add button below QTimer * closeDialog = new QTimer(this); closeDialog->setSingleShot(true); closeDialog->setInterval(1000); ekos->connect(closeDialog, &QTimer::timeout, [&] { ProfileEditor* profileEditor = ekos->findChild("profileEditorDialog"); if (profileEditor != nullptr) profileEditor->reject(); }); // Click on "Add profile" button, and let the async tests run on the modal dialog QPushButton* addButton = ekos->findChild("addProfileB"); QVERIFY(addButton != nullptr); QTest::mouseClick(addButton, Qt::LeftButton); // Click handler returned, stop the timer closing the dialog on failure closeDialog->stop(); delete closeDialog; // Verification of the first test step QVERIFY(testIsSuccessful); testIsSuccessful = false; // --------- Second step: editing and verifying the profile // Verify that the test profile exists, and select it QComboBox* profileCBox = ekos->findChild("profileCombo"); QVERIFY(profileCBox != nullptr); profileCBox->setCurrentText(testProfileName); QCOMPARE(profileCBox->currentText(), testProfileName); // Because the dialog is modal, the remainder of the test is made asynchronous QTimer::singleShot(200, ekos, [&] { // Find the Profile Editor dialog ProfileEditor* profileEditor = ekos->findChild("profileEditorDialog"); QVERIFY(profileEditor != nullptr); // Verify the values set by addEkosProfileTest QLineEdit* profileNameLE = ekos->findChild("profileIN"); QVERIFY(profileNameLE != nullptr); QCOMPARE(profileNameLE->text(), profileCBox->currentText()); QComboBox* mountCBox = ekos->findChild("mountCombo"); QVERIFY(mountCBox != nullptr); QCOMPARE(mountCBox->currentText(), QString("Telescope Simulator")); QComboBox* ccdCBox = ekos->findChild("ccdCombo"); QVERIFY(ccdCBox != nullptr); QCOMPARE(ccdCBox->currentText(), QString("CCD Simulator")); // Cancel the dialog using the "Close" button QDialogButtonBox* buttons = profileEditor->findChild("dialogButtons"); QVERIFY(nullptr != buttons); QTest::mouseClick(buttons->button(QDialogButtonBox::Close), Qt::LeftButton); testIsSuccessful = true; }); // Cancel the Profile Editor dialog if the test failed - this will happen after pushing the edit button below closeDialog = new QTimer(this); closeDialog->setSingleShot(true); closeDialog->setInterval(1000); ekos->connect(closeDialog, &QTimer::timeout, [&] { ProfileEditor* profileEditor = ekos->findChild("profileEditorDialog"); if (profileEditor != nullptr) profileEditor->reject(); }); // Click on "Edit profile" button, and let the async tests run on the modal dialog QPushButton* editButton = ekos->findChild("editProfileB"); QVERIFY(editButton != nullptr); QTest::mouseClick(editButton, Qt::LeftButton); // Click handler returned, stop the timer closing the dialog on failure closeDialog->stop(); delete closeDialog; // Verification of the second test step QVERIFY(testIsSuccessful); testIsSuccessful = false; // Verify that the test profile still exists, and select it profileCBox = ekos->findChild("profileCombo"); QVERIFY(profileCBox != nullptr); profileCBox->setCurrentText(testProfileName); QCOMPARE(profileCBox->currentText(), testProfileName); // --------- Third step: deleting the profile // The yes/no modal dialog is not really used as a blocking question, but let's keep the remainder of the test is made asynchronous QTimer::singleShot(200, ekos, [&] { // This trick is from https://stackoverflow.com/questions/38596785 QTRY_VERIFY_WITH_TIMEOUT(QApplication::activeModalWidget() != nullptr, 1000); // This is not a regular dialog but a KStars-customized dialog, so we can't search for a yes button from QDialogButtonBox KSMessageBox * const dialog = qobject_cast (QApplication::activeModalWidget()); QVERIFY(dialog != nullptr); emit dialog->accept(); testIsSuccessful = true; }); // Click on "Remove profile" button - this will display a modal yes/no dialog QPushButton* removeButton = ekos->findChild("deleteProfileB"); QVERIFY(removeButton != nullptr); QTest::mouseClick(removeButton, Qt::LeftButton); // Pressing delete-profile triggers the open() function of the yes/no modal dialog, which returns immediately, so wait for it to disappear QTRY_VERIFY_WITH_TIMEOUT(QApplication::activeModalWidget() == nullptr, 1000); // Verification of the third test step QVERIFY(testIsSuccessful); testIsSuccessful = false; // Verify that the test profile doesn't exist anymore profileCBox = ekos->findChild("profileCombo"); QVERIFY(profileCBox != nullptr); profileCBox->setCurrentText(testProfileName); QVERIFY(profileCBox->currentText() != testProfileName); } #endif // HAVE_INDI diff --git a/Tests/kstars_ui/test_ekos_focus.cpp b/Tests/kstars_ui/test_ekos_focus.cpp index f523f71b8..749106525 100644 --- a/Tests/kstars_ui/test_ekos_focus.cpp +++ b/Tests/kstars_ui/test_ekos_focus.cpp @@ -1,179 +1,174 @@ /* KStars UI tests Copyright (C) 2020 Eric Dejouhanet This application 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. */ #include "test_ekos_focus.h" #if defined(HAVE_INDI) #include "kstars_ui_tests.h" #include "test_ekos.h" #include "test_ekos_simulator.h" TestEkosFocus::TestEkosFocus(QObject *parent) : QObject(parent) { } void TestEkosFocus::initTestCase() { KVERIFY_EKOS_IS_HIDDEN(); KTRY_OPEN_EKOS(); KVERIFY_EKOS_IS_OPENED(); KTRY_EKOS_START_SIMULATORS(); // HACK: Reset clock to initial conditions KHACK_RESET_EKOS_TIME(); } void TestEkosFocus::cleanupTestCase() { KTRY_EKOS_STOP_SIMULATORS(); KTRY_CLOSE_EKOS(); KVERIFY_EKOS_IS_HIDDEN(); } void TestEkosFocus::init() { } void TestEkosFocus::cleanup() { } void TestEkosFocus::testStarDetection_data() { #if QT_VERSION < 0x050900 QSKIP("Skipping fixture-based test on old QT version."); #else QTest::addColumn("NAME"); QTest::addColumn("RA"); QTest::addColumn("DEC"); // Altitude computation taken from SchedulerJob::findAltitude GeoLocation * const geo = KStarsData::Instance()->geo(); KStarsDateTime const now(KStarsData::Instance()->lt()); KSNumbers const numbers(now.djd()); CachingDms const LST = geo->GSTtoLST(geo->LTtoUT(now).gst()); std::list Objects = { "Polaris", "Mizar", "M 51", "M 13", "M 47", "Pherkab", "Dubhe", "Vega", "NGC 2238", "M 81" }; size_t count = 0; foreach (char const *name, Objects) { SkyObject const * const so = KStars::Instance()->data()->objectNamed(name); if (so != nullptr) { SkyObject o(*so); o.updateCoordsNow(&numbers); o.EquatorialToHorizontal(&LST, geo->lat()); if (10.0 < o.alt().Degrees()) { QTest::addRow("%s", name) << name << o.ra().toHMSString() << o.dec().toDMSString(); count++; } else QWARN(QString("Fixture '%1' altitude is '%2' degrees, discarding.").arg(name).arg(so->alt().Degrees()).toStdString().c_str()); } } if (!count) QSKIP("No usable fixture objects, bypassing test."); #endif } void TestEkosFocus::testStarDetection() { #if QT_VERSION < 0x050900 QSKIP("Skipping fixture-based test on old QT version."); #else Ekos::Manager * const ekos = Ekos::Manager::Instance(); QFETCH(QString, NAME); QFETCH(QString, RA); QFETCH(QString, DEC); qDebug("Test focusing on '%s' RA '%s' DEC '%s'", NAME.toStdString().c_str(), RA.toStdString().c_str(), DEC.toStdString().c_str()); // Just sync to RA/DEC to make the mount teleport to the object QWARN("During this test, the mount is not tracking - we leave it as is for the feature in the CCD simulator to trigger a failure."); QTRY_VERIFY_WITH_TIMEOUT(ekos->mountModule() != nullptr, 5000); QVERIFY(ekos->mountModule()->sync(RA, DEC)); // Wait for Focus to come up, switch to Focus tab QTRY_VERIFY_WITH_TIMEOUT(ekos->focusModule() != nullptr, 5000); KTRY_EKOS_GADGET(QTabWidget, toolsWidget); toolsWidget->setCurrentWidget(ekos->focusModule()); QTRY_COMPARE_WITH_TIMEOUT(toolsWidget->currentWidget(), ekos->focusModule(), 1000); QWARN("The Focus capture button toggles after Ekos is started, leave a bit of time for it to settle."); QTest::qWait(500); KTRY_FOCUS_GADGET(QLineEdit, starsOut); // Run the focus procedure for SEP KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 100.0); - KTRY_FOCUS_CAPTURE(1, 1); - QWARN("No way to know when star detection procedure is fininshed."); - QTest::qWait(1000); + KTRY_FOCUS_DETECT(1, 3); QTRY_VERIFY_WITH_TIMEOUT(starsOut->text().toInt() >= 1, 5000); // Run the focus procedure for Centroid KTRY_FOCUS_CONFIGURE("Centroid", "Iterative", 0.0, 100.0); - KTRY_FOCUS_CAPTURE(1, 1); - QWARN("No way to know when star detection procedure is fininshed."); - QTest::qWait(1000); + KTRY_FOCUS_DETECT(1, 3); QTRY_VERIFY_WITH_TIMEOUT(starsOut->text().toInt() >= 1, 5000); - // Run the focus procedure for Threshold - KTRY_FOCUS_CONFIGURE("Threshold", "Iterative", 0.0, 100.0); - KTRY_FOCUS_CAPTURE(1, 1); - QWARN("No way to know when procedure is fininshed."); - QTest::qWait(1000); + // Run the focus procedure for Threshold - disable full-field + KTRY_FOCUS_CONFIGURE("Threshold", "Iterative", 0.0, 0.0); + KTRY_FOCUS_DETECT(1, 3); QTRY_VERIFY_WITH_TIMEOUT(starsOut->text().toInt() >= 1, 5000); - // Run the focus procedure for Gradient - KTRY_FOCUS_CONFIGURE("Gradient", "Iterative", 0.0, 100.0); - KTRY_FOCUS_CAPTURE(1, 1); - QWARN("No way to know when procedure is fininshed."); - QTest::qWait(1000); + // Run the focus procedure for Gradient - disable full-field + KTRY_FOCUS_CONFIGURE("Gradient", "Iterative", 0.0, 0.0); + KTRY_FOCUS_DETECT(1, 3); + QTRY_VERIFY_WITH_TIMEOUT(starsOut->text().toInt() >= 1, 5000); + + // Longer exposure + KTRY_FOCUS_CONFIGURE("SEP", "Iterative", 0.0, 100.0); + KTRY_FOCUS_DETECT(8, 1); QTRY_VERIFY_WITH_TIMEOUT(starsOut->text().toInt() >= 1, 5000); // Run the focus procedure again to cover more code // Filtering annulus is independent of the detection method // Run the HFR average over three frames with SEP to avoid - for (double inner = 0.0; inner < 100.0; inner += 23.0) + for (double inner = 0.0; inner < 100.0; inner += 43.0) { - for (double outer = 100.0; inner < outer; outer -= 22.0) + for (double outer = 100.0; inner < outer; outer -= 42.0) { KTRY_FOCUS_CONFIGURE("SEP", "Iterative", inner, outer); - KTRY_FOCUS_CAPTURE(0.5, 5); - QTest::qWait(1000); + KTRY_FOCUS_DETECT(0.1, 2); } } - // Test threshold + // Test threshold - disable full-field for (double threshold = 80.0; threshold < 200.0; threshold += 13.3) { KTRY_FOCUS_GADGET(QDoubleSpinBox, thresholdSpin); thresholdSpin->setValue(threshold); - KTRY_FOCUS_CONFIGURE("Threshold", "Iterative", 0, 100.0); - KTRY_FOCUS_CAPTURE(0.5, 1); - QTest::qWait(1000); + KTRY_FOCUS_CONFIGURE("Threshold", "Iterative", 0, 0.0); + KTRY_FOCUS_DETECT(0.1, 1); } #endif } #endif // HAVE_INDI diff --git a/Tests/kstars_ui/test_ekos_focus.h b/Tests/kstars_ui/test_ekos_focus.h index 4d289dfef..84d87ea72 100644 --- a/Tests/kstars_ui/test_ekos_focus.h +++ b/Tests/kstars_ui/test_ekos_focus.h @@ -1,117 +1,119 @@ /* KStars UI tests Copyright (C) 2020 Eric Dejouhanet This application 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. */ #ifndef TESTEKOSFOCUS_H #define TESTEKOSFOCUS_H #include "config-kstars.h" #if defined(HAVE_INDI) #include #include #include #include #include #include #include /** @brief Helper to retrieve a gadget in the Focus tab specifically. * @param klass is the class of the gadget to look for. * @param name is the gadget name to look for in the UI configuration. * @warning Fails the test if the gadget "name" of class "klass" does not exist in the Focus module */ #define KTRY_FOCUS_GADGET(klass, name) klass * const name = Ekos::Manager::Instance()->focusModule()->findChild(#name); \ QVERIFY2(name != nullptr, QString(#klass " '%1' does not exist and cannot be used").arg(#name).toStdString().c_str()) /** @brief Helper to click a button in the Focus tab specifically. * @param button is the gadget name of the button to look for in the UI configuration. * @warning Fails the test if the button is not currently enabled. */ #define KTRY_FOCUS_CLICK(button) do { \ QTimer::singleShot(200, Ekos::Manager::Instance(), []() { \ KTRY_FOCUS_GADGET(QPushButton, button); \ QVERIFY2(button->isEnabled(), QString("QPushButton '%1' is disabled and cannot be clicked").arg(#button).toStdString().c_str()); \ QTest::mouseClick(button, Qt::LeftButton); }); } while(false) /** @brief Helper to set a string text into a QComboBox in the Focus module. * @param combobox is the gadget name of the QComboBox to look for in the UI configuration. * @param text is the string text to set in the gadget. * @note This is a contrived method to set a text into a QComboBox programmatically *and* emit the "activated" message. * @warning Fails the test if the name does not exist in the Focus UI or if the text cannot be set in the gadget. */ #define KTRY_FOCUS_COMBO_SET(combobox, text) do { \ KTRY_FOCUS_GADGET(QComboBox, combobox); \ int const cbIndex = combobox->findText(text); \ QVERIFY(0 <= cbIndex); \ combobox->setCurrentIndex(cbIndex); \ combobox->activated(cbIndex); \ QCOMPARE(combobox->currentText(), QString(text)); } while(false); /** @brief Helper for exposure. * @param exposure is the amount of seconds to expose for. * @param averaged is the number of frames the procedure should average before computation. * @note The Focus capture button is disabled during exposure. * @warning Fails the test if the exposure cannot be entered or if the capture button is * disabled or does not toggle during exposure or if the stop button is not the opposite of the capture button. */ -#define KTRY_FOCUS_CAPTURE(exposure, averaged) do { \ +/** @{ */ +#define KTRY_FOCUS_DETECT(exposure, averaged) do { \ KTRY_FOCUS_GADGET(QDoubleSpinBox, exposureIN); \ exposureIN->setValue(exposure); \ KTRY_FOCUS_GADGET(QSpinBox, focusFramesSpin); \ focusFramesSpin->setValue(averaged); \ KTRY_FOCUS_GADGET(QPushButton, captureB); \ KTRY_FOCUS_GADGET(QPushButton, stopFocusB); \ QTRY_VERIFY_WITH_TIMEOUT(captureB->isEnabled(), 1000); \ QTRY_VERIFY_WITH_TIMEOUT(!stopFocusB->isEnabled(), 1000); \ KTRY_FOCUS_CLICK(captureB); \ QTRY_VERIFY_WITH_TIMEOUT(!captureB->isEnabled(), 1000); \ - QTRY_VERIFY_WITH_TIMEOUT(stopFocusB->isEnabled(), 1000); \ - QTest::qWait(1.2*exposure*averaged); \ + QVERIFY(stopFocusB->isEnabled()); \ + QTest::qWait(exposure*averaged*1000); \ QTRY_VERIFY_WITH_TIMEOUT(captureB->isEnabled(), 5000); \ - QTRY_VERIFY_WITH_TIMEOUT(!stopFocusB->isEnabled(), 5000); } while (false) + QVERIFY(!stopFocusB->isEnabled()); } while (false) +/** @} */ /** brief Helper to configure main star detection parameters. * @param detection is the name of the star detection method to use. * @param algorithm is the name of the autofocus algorithm to use. * @param fieldin is the lower radius of the annulus filtering stars. * @param fieldout is the upper radius of the annulus filtering stars. * @warning Fails the test if detection, algorithm, full-field checkbox or annulus fields cannot be used. */ #define KTRY_FOCUS_CONFIGURE(detection, algorithm, fieldin, fieldout) do { \ KTRY_FOCUS_GADGET(QCheckBox, useFullField); \ - useFullField->setCheckState(Qt::CheckState::Checked); \ + useFullField->setCheckState(fieldin < fieldout ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); \ KTRY_FOCUS_GADGET(QDoubleSpinBox, fullFieldInnerRing); \ fullFieldInnerRing->setValue(fieldin); \ KTRY_FOCUS_GADGET(QDoubleSpinBox, fullFieldOuterRing); \ fullFieldOuterRing->setValue(fieldout); \ KTRY_FOCUS_COMBO_SET(focusDetectionCombo, detection); \ KTRY_FOCUS_COMBO_SET(focusAlgorithmCombo, algorithm); } while (false) class TestEkosFocus : public QObject { Q_OBJECT public: explicit TestEkosFocus(QObject *parent = nullptr); private slots: void initTestCase(); void cleanupTestCase(); void init(); void cleanup(); void testStarDetection_data(); void testStarDetection(); }; #endif // HAVE_INDI #endif // TESTEKOSFOCUS_H diff --git a/kstars/ekos/focus/focus.cpp b/kstars/ekos/focus/focus.cpp index 843ac527f..da859d1e3 100644 --- a/kstars/ekos/focus/focus.cpp +++ b/kstars/ekos/focus/focus.cpp @@ -1,3715 +1,3712 @@ /* Ekos Copyright (C) 2012 Jasem Mutlaq This application 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. */ #include "focus.h" #include "focusadaptor.h" #include "focusalgorithms.h" #include "polynomialfit.h" #include "kstars.h" #include "kstarsdata.h" #include "Options.h" #include "auxiliary/kspaths.h" #include "auxiliary/ksmessagebox.h" #include "ekos/manager.h" #include "ekos/auxiliary/darklibrary.h" #include "fitsviewer/fitsdata.h" #include "fitsviewer/fitstab.h" #include "fitsviewer/fitsview.h" #include "indi/indifilter.h" #include "ksnotification.h" #include #include #include #include #include #define FOCUS_TIMEOUT_THRESHOLD 120000 #define MAXIMUM_ABS_ITERATIONS 30 #define MAXIMUM_RESET_ITERATIONS 2 #define AUTO_STAR_TIMEOUT 45000 #define MINIMUM_PULSE_TIMER 32 #define MAX_RECAPTURE_RETRIES 3 #define MINIMUM_POLY_SOLUTIONS 2 namespace Ekos { Focus::Focus() { // #1 Set the UI setupUi(this); // #2 Register DBus qRegisterMetaType("Ekos::FocusState"); qDBusRegisterMetaType(); new FocusAdaptor(this); QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Focus", this); // #3 Init connections initConnections(); // #4 Init Plots initPlots(); // #5 Init View initView(); // #6 Reset all buttons to default states resetButtons(); // #7 Image Effects for (auto &filter : FITSViewer::filterTypes) filterCombo->addItem(filter); filterCombo->setCurrentIndex(Options::focusEffect()); defaultScale = static_cast(Options::focusEffect()); connect(filterCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::filterChangeWarning); // #8 Load All settings loadSettings(); // #9 Init Setting Connection now initSettingsConnections(); //Note: This is to prevent a button from being called the default button //and then executing when the user hits the enter key such as when on a Text Box QList qButtons = findChildren(); for (auto &button : qButtons) button->setAutoDefault(false); appendLogText(i18n("Idle.")); } Focus::~Focus() { if (focusingWidget->parent() == nullptr) toggleFocusingWidgetFullScreen(); } void Focus::resetFrame() { if (currentCCD) { ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); if (targetChip) { //fx=fy=fw=fh=0; targetChip->resetFrame(); int x, y, w, h; targetChip->getFrame(&x, &y, &w, &h); qCDebug(KSTARS_EKOS_FOCUS) << "Frame is reset. X:" << x << "Y:" << y << "W:" << w << "H:" << h << "binX:" << 1 << "binY:" << 1; QVariantMap settings; settings["x"] = x; settings["y"] = y; settings["w"] = w; settings["h"] = h; settings["binx"] = 1; settings["biny"] = 1; frameSettings[targetChip] = settings; starSelected = false; starCenter = QVector3D(); subFramed = false; focusView->setTrackingBox(QRect()); } } } bool Focus::setCamera(const QString &device) { for (int i = 0; i < CCDCaptureCombo->count(); i++) if (device == CCDCaptureCombo->itemText(i)) { CCDCaptureCombo->setCurrentIndex(i); checkCCD(i); return true; } return false; } QString Focus::camera() { if (currentCCD) return currentCCD->getDeviceName(); return QString(); } void Focus::checkCCD(int ccdNum) { if (ccdNum == -1) { ccdNum = CCDCaptureCombo->currentIndex(); if (ccdNum == -1) return; } if (ccdNum >= 0 && ccdNum <= CCDs.count()) { currentCCD = CCDs.at(ccdNum); ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); if (targetChip && targetChip->isCapturing()) return; for (ISD::CCD *oneCCD : CCDs) { if (oneCCD == currentCCD) continue; if (captureInProgress == false) oneCCD->disconnect(this); } if (targetChip) { targetChip->setImageView(focusView, FITS_FOCUS); binningCombo->setEnabled(targetChip->canBin()); useSubFrame->setEnabled(targetChip->canSubframe()); if (targetChip->canBin()) { int subBinX = 1, subBinY = 1; binningCombo->clear(); targetChip->getMaxBin(&subBinX, &subBinY); for (int i = 1; i <= subBinX; i++) binningCombo->addItem(QString("%1x%2").arg(i).arg(i)); activeBin = Options::focusXBin(); binningCombo->setCurrentIndex(activeBin - 1); } else activeBin = 1; QStringList isoList = targetChip->getISOList(); ISOCombo->clear(); if (isoList.isEmpty()) { ISOCombo->setEnabled(false); ISOLabel->setEnabled(false); } else { ISOCombo->setEnabled(true); ISOLabel->setEnabled(true); ISOCombo->addItems(isoList); ISOCombo->setCurrentIndex(targetChip->getISOIndex()); } connect(currentCCD, &ISD::CCD::videoStreamToggled, this, &Ekos::Focus::setVideoStreamEnabled, Qt::UniqueConnection); liveVideoB->setEnabled(currentCCD->hasVideoStream()); if (currentCCD->hasVideoStream()) setVideoStreamEnabled(currentCCD->isStreamingEnabled()); else liveVideoB->setIcon(QIcon::fromTheme("camera-off")); bool hasGain = currentCCD->hasGain(); gainLabel->setEnabled(hasGain); gainIN->setEnabled(hasGain && currentCCD->getGainPermission() != IP_RO); if (hasGain) { double gain = 0, min = 0, max = 0, step = 1; currentCCD->getGainMinMaxStep(&min, &max, &step); if (currentCCD->getGain(&gain)) { gainIN->setMinimum(min); gainIN->setMaximum(max); if (step > 0) gainIN->setSingleStep(step); double defaultGain = Options::focusGain(); if (defaultGain > 0) gainIN->setValue(defaultGain); else gainIN->setValue(gain); } } else gainIN->clear(); } } syncCCDInfo(); } void Focus::syncCCDInfo() { if (currentCCD == nullptr) return; ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); useSubFrame->setEnabled(targetChip->canSubframe()); if (frameSettings.contains(targetChip) == false) { int x, y, w, h; if (targetChip->getFrame(&x, &y, &w, &h)) { int binx = 1, biny = 1; targetChip->getBinning(&binx, &biny); if (w > 0 && h > 0) { int minX, maxX, minY, maxY, minW, maxW, minH, maxH; targetChip->getFrameMinMax(&minX, &maxX, &minY, &maxY, &minW, &maxW, &minH, &maxH); QVariantMap settings; settings["x"] = useSubFrame->isChecked() ? x : minX; settings["y"] = useSubFrame->isChecked() ? y : minY; settings["w"] = useSubFrame->isChecked() ? w : maxW; settings["h"] = useSubFrame->isChecked() ? h : maxH; settings["binx"] = binx; settings["biny"] = biny; frameSettings[targetChip] = settings; } } } } void Focus::addFilter(ISD::GDInterface *newFilter) { for (auto &oneFilter : Filters) { if (oneFilter->getDeviceName() == newFilter->getDeviceName()) return; } FilterCaptureLabel->setEnabled(true); FilterDevicesCombo->setEnabled(true); FilterPosLabel->setEnabled(true); FilterPosCombo->setEnabled(true); filterManagerB->setEnabled(true); FilterDevicesCombo->addItem(newFilter->getDeviceName()); Filters.append(static_cast(newFilter)); int filterWheelIndex = 1; if (Options::defaultFocusFilterWheel().isEmpty() == false) filterWheelIndex = FilterDevicesCombo->findText(Options::defaultFocusFilterWheel()); if (filterWheelIndex < 1) filterWheelIndex = 1; checkFilter(filterWheelIndex); FilterDevicesCombo->setCurrentIndex(filterWheelIndex); } bool Focus::setFilterWheel(const QString &device) { bool deviceFound = false; for (int i = 1; i < FilterDevicesCombo->count(); i++) if (device == FilterDevicesCombo->itemText(i)) { checkFilter(i); deviceFound = true; break; } if (deviceFound == false) return false; return true; } QString Focus::filterWheel() { if (FilterDevicesCombo->currentIndex() >= 1) return FilterDevicesCombo->currentText(); return QString(); } bool Focus::setFilter(const QString &filter) { if (FilterDevicesCombo->currentIndex() >= 1) { FilterPosCombo->setCurrentText(filter); return true; } return false; } QString Focus::filter() { return FilterPosCombo->currentText(); } void Focus::checkFilter(int filterNum) { if (filterNum == -1) { filterNum = FilterDevicesCombo->currentIndex(); if (filterNum == -1) return; } // "--" is no filter if (filterNum == 0) { currentFilter = nullptr; currentFilterPosition = -1; FilterPosCombo->clear(); return; } if (filterNum <= Filters.count()) currentFilter = Filters.at(filterNum - 1); //Options::setDefaultFocusFilterWheel(currentFilter->getDeviceName()); filterManager->setCurrentFilterWheel(currentFilter); FilterPosCombo->clear(); FilterPosCombo->addItems(filterManager->getFilterLabels()); currentFilterPosition = filterManager->getFilterPosition(); FilterPosCombo->setCurrentIndex(currentFilterPosition - 1); //Options::setDefaultFocusFilterWheelFilter(FilterPosCombo->currentText()); exposureIN->setValue(filterManager->getFilterExposure()); } void Focus::addFocuser(ISD::GDInterface *newFocuser) { ISD::Focuser *oneFocuser = static_cast(newFocuser); if (Focusers.contains(oneFocuser)) return; focuserCombo->addItem(oneFocuser->getDeviceName()); Focusers.append(oneFocuser); currentFocuser = oneFocuser; checkFocuser(); } bool Focus::setFocuser(const QString &device) { for (int i = 0; i < focuserCombo->count(); i++) if (device == focuserCombo->itemText(i)) { focuserCombo->setCurrentIndex(i); checkFocuser(i); return true; } return false; } QString Focus::focuser() { if (currentFocuser) return currentFocuser->getDeviceName(); return QString(); } void Focus::checkFocuser(int FocuserNum) { if (FocuserNum == -1) FocuserNum = focuserCombo->currentIndex(); if (FocuserNum == -1) { currentFocuser = nullptr; return; } if (FocuserNum < Focusers.count()) currentFocuser = Focusers.at(FocuserNum); filterManager->setFocusReady(currentFocuser->isConnected()); // Disconnect all focusers for (auto &oneFocuser : Focusers) { disconnect(oneFocuser, &ISD::GDInterface::numberUpdated, this, &Ekos::Focus::processFocusNumber); } hasDeviation = currentFocuser->hasDeviation(); canAbsMove = currentFocuser->canAbsMove(); if (canAbsMove) { getAbsFocusPosition(); absTicksSpin->setEnabled(true); absTicksLabel->setEnabled(true); startGotoB->setEnabled(true); absTicksSpin->setValue(currentPosition); } else { absTicksSpin->setEnabled(false); absTicksLabel->setEnabled(false); startGotoB->setEnabled(false); } canRelMove = currentFocuser->canRelMove(); // In case we have a purely relative focuser, we pretend // it is an absolute focuser with initial point set at 50,000. // This is done we can use the same algorithm used for absolute focuser. if (canAbsMove == false && canRelMove == true) { currentPosition = 50000; absMotionMax = 100000; absMotionMin = 0; } canTimerMove = currentFocuser->canTimerMove(); // In case we have a timer-based focuser and using the linear focus algorithm, // we pretend it is an absolute focuser with initial point set at 50,000. // These variables don't have in impact on timer-based focusers if the algorithm // is not the linear focus algorithm. if (!canAbsMove && !canRelMove && canTimerMove) { currentPosition = 50000; absMotionMax = 100000; absMotionMin = 0; } focusType = (canRelMove || canAbsMove || canTimerMove) ? FOCUS_AUTO : FOCUS_MANUAL; bool hasBacklash = currentFocuser->hasBacklash(); focusBacklashSpin->setEnabled(hasBacklash); focusBacklashSpin->disconnect(this); if (hasBacklash) { double min = 0, max = 0, step = 0; currentFocuser->getMinMaxStep("FOCUS_BACKLASH_STEPS", "FOCUS_BACKLASH_VALUE", &min, &max, &step); focusBacklashSpin->setMinimum(min); focusBacklashSpin->setMaximum(max); focusBacklashSpin->setSingleStep(step); focusBacklashSpin->setValue(currentFocuser->getBacklash()); connect(focusBacklashSpin, static_cast(&QSpinBox::valueChanged), this, [this](int value) { if (currentFocuser) currentFocuser->setBacklash(value); }); } else { focusBacklashSpin->setValue(0); } connect(currentFocuser, &ISD::GDInterface::numberUpdated, this, &Ekos::Focus::processFocusNumber, Qt::UniqueConnection); //connect(currentFocuser, SIGNAL(propertyDefined(INDI::Property*)), this, &Ekos::Focus::(registerFocusProperty(INDI::Property*)), Qt::UniqueConnection); resetButtons(); //if (!inAutoFocus && !inFocusLoop && !captureInProgress && !inSequenceFocus) // emit autoFocusFinished(true, -1); } void Focus::addCCD(ISD::GDInterface *newCCD) { if (CCDs.contains(static_cast(newCCD))) return; CCDs.append(static_cast(newCCD)); CCDCaptureCombo->addItem(newCCD->getDeviceName()); checkCCD(); } void Focus::getAbsFocusPosition() { if (!canAbsMove) return; INumberVectorProperty *absMove = currentFocuser->getBaseDevice()->getNumber("ABS_FOCUS_POSITION"); if (absMove) { currentPosition = absMove->np[0].value; absMotionMax = absMove->np[0].max; absMotionMin = absMove->np[0].min; absTicksSpin->setMinimum(absMove->np[0].min); absTicksSpin->setMaximum(absMove->np[0].max); absTicksSpin->setSingleStep(absMove->np[0].step); maxTravelIN->setMinimum(absMove->np[0].min); maxTravelIN->setMaximum(absMove->np[0].max); absTicksLabel->setText(QString::number(static_cast(currentPosition))); stepIN->setMaximum(absMove->np[0].max / 2); //absTicksSpin->setValue(currentPosition); } } void Focus::start() { if (currentCCD == nullptr) { appendLogText(i18n("No CCD connected.")); return; } lastFocusDirection = FOCUS_NONE; polySolutionFound = 0; waitStarSelectTimer.stop(); starsHFR.clear(); lastHFR = 0; if (canAbsMove) { absIterations = 0; getAbsFocusPosition(); pulseDuration = stepIN->value(); } else if (canRelMove) { //appendLogText(i18n("Setting dummy central position to 50000")); absIterations = 0; pulseDuration = stepIN->value(); //currentPosition = 50000; absMotionMax = 100000; absMotionMin = 0; } else { pulseDuration = stepIN->value(); absIterations = 0; absMotionMax = 100000; absMotionMin = 0; if (pulseDuration <= MINIMUM_PULSE_TIMER) { appendLogText(i18n("Starting pulse step is too low. Increase the step size to %1 or higher...", MINIMUM_PULSE_TIMER * 5)); return; } } inAutoFocus = true; focuserAdditionalMovement = 0; HFRFrames.clear(); resetButtons(); reverseDir = false; /*if (fw > 0 && fh > 0) starSelected= true; else starSelected= false;*/ clearDataPoints(); if (firstGaus) { profilePlot->removeGraph(firstGaus); firstGaus = nullptr; } // Options::setFocusTicks(stepIN->value()); // Options::setFocusTolerance(toleranceIN->value()); // //Options::setFocusExposure(exposureIN->value()); // Options::setFocusMaxTravel(maxTravelIN->value()); // Options::setFocusBoxSize(focusBoxSize->value()); // Options::setFocusSubFrame(useSubFrame->isChecked()); // Options::setFocusAutoStarEnabled(useAutoStar->isChecked()); // Options::setSuspendGuiding(suspendGuideCheck->isChecked()); // Options::setUseFocusDarkFrame(darkFrameCheck->isChecked()); // Options::setFocusFramesCount(focusFramesSpin->value()); // Options::setFocusUseFullField(useFullField->isChecked()); qCDebug(KSTARS_EKOS_FOCUS) << "Starting focus with box size: " << focusBoxSize->value() << " Subframe: " << ( useSubFrame->isChecked() ? "yes" : "no" ) << " Autostar: " << ( useAutoStar->isChecked() ? "yes" : "no" ) << " Full frame: " << ( useFullField->isChecked() ? "yes" : "no " ) << " [" << fullFieldInnerRing->value() << "%," << fullFieldOuterRing->value() << "%]" << " Step Size: " << stepIN->value() << " Threshold: " << thresholdSpin->value() << " Tolerance: " << toleranceIN->value() << " Frames: " << 1 /*focusFramesSpin->value()*/ << " Maximum Travel: " << maxTravelIN->value(); if (useAutoStar->isChecked()) appendLogText(i18n("Autofocus in progress...")); else appendLogText(i18n("Please wait until image capture is complete...")); if (suspendGuideCheck->isChecked()) { m_GuidingSuspended = true; emit suspendGuiding(); } //emit statusUpdated(true); state = Ekos::FOCUS_PROGRESS; qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state); emit newStatus(state); // Denoise with median filter //defaultScale = FITS_MEDIAN; KSNotification::event(QLatin1String("FocusStarted"), i18n("Autofocus operation started")); // Used for all the focuser types. if (focusAlgorithm == FOCUS_LINEAR) { const int position = static_cast(currentPosition); FocusAlgorithmInterface::FocusParams params( maxTravelIN->value(), stepIN->value(), position, absMotionMin, absMotionMax, MAXIMUM_ABS_ITERATIONS, toleranceIN->value() / 100.0, filter()); linearFocuser.reset(MakeLinearFocuser(params)); linearRequestedPosition = linearFocuser->initialPosition(); const int newPosition = adjustLinearPosition(position, linearRequestedPosition); if (newPosition != position) { if (!changeFocus(newPosition - position)) { abort(); setAutoFocusResult(false); } // Avoid the capture below. return; } } capture(); } int Focus::adjustLinearPosition(int position, int newPosition) { if (newPosition > position) { constexpr int extraMotionSteps = 5; int adjustment = extraMotionSteps * stepIN->value(); if (newPosition + adjustment > absMotionMax) adjustment = static_cast(absMotionMax) - newPosition; focuserAdditionalMovement = adjustment; qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: extending outward movement by %1").arg(adjustment); return newPosition + adjustment; } return newPosition; } void Focus::checkStopFocus() { if (inSequenceFocus == true) { inSequenceFocus = false; setAutoFocusResult(false); } if (captureInProgress && inAutoFocus == false && inFocusLoop == false) { captureB->setEnabled(true); stopFocusB->setEnabled(false); appendLogText(i18n("Capture aborted.")); } abort(); } void Focus::abort() { stop(true); } void Focus::stop(bool aborted) { qCDebug(KSTARS_EKOS_FOCUS) << "Stopping Focus"; captureTimeout.stop(); ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); inAutoFocus = false; focuserAdditionalMovement = 0; inFocusLoop = false; // Why starSelected is set to false below? We should retain star selection status under: // 1. Autostar is off, or // 2. Toggle subframe, or // 3. Reset frame // 4. Manual motion? //starSelected = false; polySolutionFound = 0; captureInProgress = false; captureFailureCounter = 0; minimumRequiredHFR = -1; noStarCount = 0; HFRFrames.clear(); //maxHFR=1; disconnect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Focus::newFITS); disconnect(currentCCD, &ISD::CCD::captureFailed, this, &Ekos::Focus::processCaptureFailure); if (rememberUploadMode != currentCCD->getUploadMode()) currentCCD->setUploadMode(rememberUploadMode); if (rememberCCDExposureLooping) currentCCD->setExposureLoopingEnabled(true); targetChip->abortExposure(); resetButtons(); absIterations = 0; HFRInc = 0; reverseDir = false; //emit statusUpdated(false); if (aborted) { state = Ekos::FOCUS_ABORTED; qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state); emit newStatus(state); } } void Focus::capture() { captureTimeout.stop(); if (captureInProgress) { qCWarning(KSTARS_EKOS_FOCUS) << "Capture called while already in progress. Capture is ignored."; return; } if (currentCCD == nullptr) { appendLogText(i18n("No CCD connected.")); return; } waitStarSelectTimer.stop(); ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); double seqExpose = exposureIN->value(); if (currentCCD->isConnected() == false) { appendLogText(i18n("Error: Lost connection to CCD.")); return; } if (currentCCD->isBLOBEnabled() == false) { currentCCD->setBLOBEnabled(true); } if (currentFilter != nullptr && FilterPosCombo->currentIndex() != -1) { if (currentFilter->isConnected() == false) { appendLogText(i18n("Error: Lost connection to filter wheel.")); return; } int targetPosition = FilterPosCombo->currentIndex() + 1; QString lockedFilter = filterManager->getFilterLock(FilterPosCombo->currentText()); // We change filter if: // 1. Target position is not equal to current position. // 2. Locked filter of CURRENT filter is a different filter. if (lockedFilter != "--" && lockedFilter != FilterPosCombo->currentText()) { int lockedFilterIndex = FilterPosCombo->findText(lockedFilter); if (lockedFilterIndex >= 0) { // Go back to this filter one we are done fallbackFilterPending = true; fallbackFilterPosition = targetPosition; targetPosition = lockedFilterIndex + 1; } } filterPositionPending = (targetPosition != currentFilterPosition); // If either the target position is not equal to the current position, OR if (filterPositionPending) { // Apply all policies except autofocus since we are already in autofocus module doh. filterManager->setFilterPosition(targetPosition, static_cast(FilterManager::CHANGE_POLICY | FilterManager::OFFSET_POLICY)); return; } } if (currentCCD->getUploadMode() == ISD::CCD::UPLOAD_LOCAL) { rememberUploadMode = ISD::CCD::UPLOAD_LOCAL; currentCCD->setUploadMode(ISD::CCD::UPLOAD_CLIENT); } rememberCCDExposureLooping = currentCCD->isLooping(); if (rememberCCDExposureLooping) currentCCD->setExposureLoopingEnabled(false); currentCCD->setTransformFormat(ISD::CCD::FORMAT_FITS); targetChip->setBinning(activeBin, activeBin); targetChip->setCaptureMode(FITS_FOCUS); // Always disable filtering if using a dark frame and then re-apply after subtraction. TODO: Implement this in capture and guide and align if (darkFrameCheck->isChecked()) targetChip->setCaptureFilter(FITS_NONE); else targetChip->setCaptureFilter(defaultScale); if (ISOCombo->isEnabled() && ISOCombo->currentIndex() != -1 && targetChip->getISOIndex() != ISOCombo->currentIndex()) targetChip->setISOIndex(ISOCombo->currentIndex()); if (gainIN->isEnabled()) currentCCD->setGain(gainIN->value()); connect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Focus::newFITS); connect(currentCCD, &ISD::CCD::captureFailed, this, &Ekos::Focus::processCaptureFailure); targetChip->setFrameType(FRAME_LIGHT); if (frameSettings.contains(targetChip)) { QVariantMap settings = frameSettings[targetChip]; targetChip->setFrame(settings["x"].toInt(), settings["y"].toInt(), settings["w"].toInt(), settings["h"].toInt()); settings["binx"] = activeBin; settings["biny"] = activeBin; frameSettings[targetChip] = settings; } captureInProgress = true; focusView->setBaseSize(focusingWidget->size()); // Timeout is exposure duration + timeout threshold in seconds captureTimeout.start(seqExpose * 1000 + FOCUS_TIMEOUT_THRESHOLD); targetChip->capture(seqExpose); if (inFocusLoop == false) { appendLogText(i18n("Capturing image...")); if (inAutoFocus == false) { captureB->setEnabled(false); stopFocusB->setEnabled(true); } } } bool Focus::focusIn(int ms) { if (ms == -1) ms = stepIN->value(); return changeFocus(-ms); } bool Focus::focusOut(int ms) { if (ms == -1) ms = stepIN->value(); return changeFocus(ms); } // If amount > 0 we focus out, otherwise in. bool Focus::changeFocus(int amount) { if (currentFocuser == nullptr) return false; // This needs to be re-thought. Just returning does not set the timer // and the algorithm ends in limbo. // Ignore zero // if (amount == 0) // return true; if (currentFocuser->isConnected() == false) { appendLogText(i18n("Error: Lost connection to Focuser.")); return false; } const int absAmount = abs(amount); const bool focusingOut = amount > 0; const QString dirStr = focusingOut ? i18n("outward") : i18n("inward"); lastFocusDirection = focusingOut ? FOCUS_OUT : FOCUS_IN; qCDebug(KSTARS_EKOS_FOCUS) << "Focus " << dirStr << " (" << absAmount << ")"; if (focusingOut) currentFocuser->focusOut(); else currentFocuser->focusIn(); if (canAbsMove) { currentFocuser->moveAbs(currentPosition + amount); appendLogText(i18n("Focusing %2 by %1 steps...", absAmount, dirStr)); } else if (canRelMove) { currentFocuser->moveRel(absAmount); appendLogText(i18np("Focusing %2 by %1 step...", "Focusing %2 by %1 steps...", absAmount, dirStr)); } else { currentFocuser->moveByTimer(absAmount); appendLogText(i18n("Focusing %2 by %1 ms...", absAmount, dirStr)); } return true; } void Focus::newFITS(IBLOB *bp) { if (bp == nullptr) { capture(); return; } // Ignore guide head if there is any. if (!strcmp(bp->name, "CCD2")) return; captureTimeout.stop(); captureTimeoutCounter = 0; ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); disconnect(currentCCD, &ISD::CCD::BLOBUpdated, this, &Ekos::Focus::newFITS); disconnect(currentCCD, &ISD::CCD::captureFailed, this, &Ekos::Focus::processCaptureFailure); if (darkFrameCheck->isChecked()) { FITSData *darkData = DarkLibrary::Instance()->getDarkFrame(targetChip, exposureIN->value()); QVariantMap settings = frameSettings[targetChip]; uint16_t offsetX = settings["x"].toInt() / settings["binx"].toInt(); uint16_t offsetY = settings["y"].toInt() / settings["biny"].toInt(); connect(DarkLibrary::Instance(), &DarkLibrary::darkFrameCompleted, this, [&](bool completed) { DarkLibrary::Instance()->disconnect(this); darkFrameCheck->setChecked(completed); if (completed) setCaptureComplete(); else abort(); + resetButtons(); }); connect(DarkLibrary::Instance(), &DarkLibrary::newLog, this, &Ekos::Focus::appendLogText); targetChip->setCaptureFilter(defaultScale); if (darkData) DarkLibrary::Instance()->subtract(darkData, focusView, defaultScale, offsetX, offsetY); else { DarkLibrary::Instance()->captureAndSubtract(targetChip, focusView, exposureIN->value(), offsetX, offsetY); } return; } setCaptureComplete(); + resetButtons(); } double Focus::analyzeSources(FITSData *image_data) { // When we're using FULL field view, we always use either CENTROID algorithm which is the default // standard algorithm in KStars, or SEP. The other algorithms are too inefficient to run on full frames and require // a bounding box for them to be effective in near real-time application. if (Options::focusUseFullField()) { - Q_ASSERT_X(focusView->getTrackingBox().isNull(), __FUNCTION__, "Tracking box is disabled when detecting in full-field"); + focusView->setTrackingBoxEnabled(false); if (focusDetection != ALGORITHM_CENTROID && focusDetection != ALGORITHM_SEP) focusView->findStars(ALGORITHM_CENTROID); else focusView->findStars(focusDetection); focusView->setStarFilterRange(static_cast (fullFieldInnerRing->value() / 100.0), static_cast (fullFieldOuterRing->value() / 100.0)); focusView->filterStars(); // Get the average HFR of the whole frame return image_data->getHFR(HFR_AVERAGE); } else { // If star is already selected then use whatever algorithm currently selected. if (starSelected) { focusView->findStars(focusDetection); return image_data->getHFR(HFR_MAX); } else { // Disable tracking box focusView->setTrackingBoxEnabled(false); // If algorithm is set something other than Centeroid or SEP, then force Centroid // Since it is the most reliable detector when nothing was selected before. if (focusDetection != ALGORITHM_CENTROID && focusDetection != ALGORITHM_SEP) focusView->findStars(ALGORITHM_CENTROID); else // Otherwise, continue to find use using the selected algorithm focusView->findStars(focusDetection); // Reenable tracking box focusView->setTrackingBoxEnabled(true); // Get maximum HFR in the frame return image_data->getHFR(HFR_MAX); } } } bool Focus::appendHFR(double newHFR) { // Add new HFR to existing values, even if invalid HFRFrames.append(newHFR); // Prepare a work vector with valid HFR values QVector samples(HFRFrames); samples.erase(std::remove_if(samples.begin(), samples.end(), [](const double HFR) { return HFR == -1; }), samples.end()); // Perform simple sigma clipping if more than a few samples if (samples.count() > 3) { // Sort all HFRs and extract the median std::sort(samples.begin(), samples.end()); const auto median = ((samples.size() % 2) ? samples[samples.size() / 2] : (static_cast(samples[samples.size() / 2 - 1]) + samples[samples.size() / 2]) * .5); // Extract the mean const auto mean = std::accumulate(samples.begin(), samples.end(), .0) / samples.size(); // Extract the variance double variance = 0; foreach (auto val, samples) variance += (val - mean) * (val - mean); // Deduce the standard deviation const double stddev = sqrt(variance / samples.size()); // Reject those 2 sigma away from median const double sigmaHigh = median + stddev * 2; const double sigmaLow = median - stddev * 2; // FIXME: why is the first value not considered? // FIXME: what if there are less than 3 samples after clipping? QMutableVectorIterator i(samples); while (i.hasNext()) { auto val = i.next(); if (val > sigmaHigh || val < sigmaLow) i.remove(); } } // Consolidate the average HFR currentHFR = samples.isEmpty() ? -1 : std::accumulate(samples.begin(), samples.end(), .0) / samples.size(); // Return whether we need more frame based on user requirement return HFRFrames.count() < focusFramesSpin->value(); } void Focus::setCaptureComplete() { DarkLibrary::Instance()->disconnect(this); // If we have a box, sync the bounding box to its position. syncTrackingBoxPosition(); // Notify user if we're not looping if (inFocusLoop == false) appendLogText(i18n("Image received.")); - // If we're not looping and not in autofocus, enable user to capture again. if (captureInProgress && inFocusLoop == false && inAutoFocus == false) - { - captureB->setEnabled(true); - stopFocusB->setEnabled(false); currentCCD->setUploadMode(rememberUploadMode); - } if (rememberCCDExposureLooping) currentCCD->setExposureLoopingEnabled(true); captureInProgress = false; // Get handle to the image data FITSData *image_data = focusView->getImageData(); // Emit the tracking (bounding) box view emit newStarPixmap(focusView->getTrackingBoxPixmap(10)); // If we are not looping; OR // If we are looping but we already have tracking box enabled; OR // If we are asked to analyze _all_ the stars within the field // THEN let's find stars in the image and get current HFR if (inFocusLoop == false || (inFocusLoop && (focusView->isTrackingBoxEnabled() || Options::focusUseFullField()))) { // First check that we haven't already search for stars // Since star-searching algorithm are time-consuming, we should only search when necessary if (image_data->areStarsSearched() == false) { currentHFR = analyzeSources(image_data); focusView->updateFrame(); } // Let's now report the current HFR qCDebug(KSTARS_EKOS_FOCUS) << "Focus newFITS #" << HFRFrames.count() + 1 << ": Current HFR " << currentHFR << " Num stars " << (starSelected ? 1 : image_data->getDetectedStars()); // Take the new HFR into account, eventually continue to stack samples if (appendHFR(currentHFR)) { capture(); return; } else HFRFrames.clear(); // Let signal the current HFR now depending on whether the focuser is absolute or relative if (canAbsMove) emit newHFR(currentHFR, static_cast(currentPosition)); else emit newHFR(currentHFR, -1); // Format the HFR value into a string QString HFRText = QString("%1").arg(currentHFR, 0, 'f', 2); HFROut->setText(HFRText); starsOut->setText(QString("%1").arg(image_data->getDetectedStars())); // Display message in case _last_ HFR was negative if (lastHFR == -1) appendLogText(i18n("FITS received. No stars detected.")); // If we have a valid HFR value if (currentHFR > 0) { // Check if we're done from polynomial fitting algorithm if (focusAlgorithm == FOCUS_POLYNOMIAL && polySolutionFound == MINIMUM_POLY_SOLUTIONS) { polySolutionFound = 0; appendLogText(i18n("Autofocus complete after %1 iterations.", hfr_position.count())); stop(); setAutoFocusResult(true); graphPolynomialFunction(); return; } Edge *maxStarHFR = nullptr; // Center tracking box around selected star (if it valid) either in: // 1. Autofocus // 2. CheckFocus (minimumHFRCheck) // The starCenter _must_ already be defined, otherwise, we proceed until // the latter half of the function searches for a star and define it. if (starCenter.isNull() == false && (inAutoFocus || minimumRequiredHFR >= 0) && (maxStarHFR = image_data->getMaxHFRStar()) != nullptr) { // Now we have star selected in the frame starSelected = true; starCenter.setX(qMax(0, static_cast(maxStarHFR->x))); starCenter.setY(qMax(0, static_cast(maxStarHFR->y))); syncTrackingBoxPosition(); // Record the star information (X, Y, currentHFR) QVector3D oneStar = starCenter; oneStar.setZ(currentHFR); starsHFR.append(oneStar); } else { // Record the star information (X, Y, currentHFR) QVector3D oneStar(starCenter.x(), starCenter.y(), currentHFR); starsHFR.append(oneStar); } if (currentHFR > maxHFR) maxHFR = currentHFR; // Append point to the #Iterations vs #HFR chart in case of looping or in case in autofocus with a focus // that does not support position feedback. // If inAutoFocus is true without canAbsMove and without canRelMove, canTimerMove must be true. // We'd only want to execute this if the focus linear algorithm is not being used, as that // algorithm simulates a position-based system even for timer-based focusers. if (inFocusLoop || (inAutoFocus && canAbsMove == false && canRelMove == false && focusAlgorithm != FOCUS_LINEAR)) { if (hfr_position.empty()) hfr_position.append(1); else hfr_position.append(hfr_position.last() + 1); hfr_value.append(currentHFR); drawHFRPlot(); } } else { // Let's record an invalid star result QVector3D oneStar(starCenter.x(), starCenter.y(), -1); starsHFR.append(oneStar); } // Try to average values and find if we have bogus results if (inAutoFocus && starsHFR.count() > 3) { float mean = 0, sum = 0, stddev = 0, noHFR = 0; for (int i = 0; i < starsHFR.count(); i++) { sum += starsHFR[i].x(); if (starsHFR[i].z() == -1) noHFR++; } mean = sum / starsHFR.count(); // Calculate standard deviation for (int i = 0; i < starsHFR.count(); i++) stddev += pow(starsHFR[i].x() - mean, 2); stddev = sqrt(stddev / starsHFR.count()); if (currentHFR == -1 && (stddev > focusBoxSize->value() / 10.0 || noHFR / starsHFR.count() > 0.75)) { appendLogText(i18n("No reliable star is detected. Aborting...")); abort(); setAutoFocusResult(false); return; } } } // If we are just framing, let's capture again if (inFocusLoop) { capture(); return; } // Get target chip ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); // Get target chip binning int subBinX = 1, subBinY = 1; if (!targetChip->getBinning(&subBinX, &subBinY)) qCDebug(KSTARS_EKOS_FOCUS) << "Warning: target chip is reporting no binning property, using 1x1."; // If star is NOT yet selected in a non-full-frame situation // then let's now try to find the star. This step is skipped for full frames // since there isn't a single star to select as we are only interested in the overall average HFR. // We need to check if we can find the star right away, or if we need to _subframe_ around the // selected star. if (Options::focusUseFullField() == false && starCenter.isNull()) { int x = 0, y = 0, w = 0, h = 0; // Let's get the stored frame settings for this particular chip if (frameSettings.contains(targetChip)) { QVariantMap settings = frameSettings[targetChip]; x = settings["x"].toInt(); y = settings["y"].toInt(); w = settings["w"].toInt(); h = settings["h"].toInt(); } else // Otherwise let's get the target chip frame coordinates. targetChip->getFrame(&x, &y, &w, &h); // In case auto star is selected. if (useAutoStar->isChecked()) { // Do we have a valid star detected? Edge *maxStar = image_data->getMaxHFRStar(); if (maxStar == nullptr) { appendLogText(i18n("Failed to automatically select a star. Please select a star manually.")); // Center the tracking box in the frame and display it focusView->setTrackingBox(QRect(w - focusBoxSize->value() / (subBinX * 2), h - focusBoxSize->value() / (subBinY * 2), focusBoxSize->value() / subBinX, focusBoxSize->value() / subBinY)); focusView->setTrackingBoxEnabled(true); // Use can now move it to select the desired star state = Ekos::FOCUS_WAITING; qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state); emit newStatus(state); // Start the wait timer so we abort after a timeout if the user does not make a choice waitStarSelectTimer.start(); return; } // set the tracking box on maxStar starCenter.setX(maxStar->x); starCenter.setY(maxStar->y); starCenter.setZ(subBinX); syncTrackingBoxPosition(); defaultScale = static_cast(filterCombo->currentIndex()); // Do we need to subframe? if (subFramed == false && useSubFrame->isEnabled() && useSubFrame->isChecked()) { int offset = (static_cast(focusBoxSize->value()) / subBinX) * 1.5; int subX = (maxStar->x - offset) * subBinX; int subY = (maxStar->y - offset) * subBinY; int subW = offset * 2 * subBinX; int subH = offset * 2 * subBinY; int minX, maxX, minY, maxY, minW, maxW, minH, maxH; targetChip->getFrameMinMax(&minX, &maxX, &minY, &maxY, &minW, &maxW, &minH, &maxH); // Try to limit the subframed selection if (subX < minX) subX = minX; if (subY < minY) subY = minY; if ((subW + subX) > maxW) subW = maxW - subX; if ((subH + subY) > maxH) subH = maxH - subY; // Now we store the subframe coordinates in the target chip frame settings so we // reuse it later when we capture again. QVariantMap settings = frameSettings[targetChip]; settings["x"] = subX; settings["y"] = subY; settings["w"] = subW; settings["h"] = subH; settings["binx"] = subBinX; settings["biny"] = subBinY; qCDebug(KSTARS_EKOS_FOCUS) << "Frame is subframed. X:" << subX << "Y:" << subY << "W:" << subW << "H:" << subH << "binX:" << subBinX << "binY:" << subBinY; starsHFR.clear(); frameSettings[targetChip] = settings; // Set the star center in the center of the subframed coordinates starCenter.setX(subW / (2 * subBinX)); starCenter.setY(subH / (2 * subBinY)); starCenter.setZ(subBinX); subFramed = true; focusView->setFirstLoad(true); // Now let's capture again for the actual requested subframed image. capture(); return; } // If we're subframed or don't need subframe, let's record the max star coordinates else { starCenter.setX(maxStar->x); starCenter.setY(maxStar->y); starCenter.setZ(subBinX); // Let's now capture again if we're autofocusing if (inAutoFocus) { capture(); return; } } } // If manual selection is enabled then let's ask the user to select the focus star else { appendLogText(i18n("Capture complete. Select a star to focus.")); starSelected = false; // Let's now display and set the tracking box in the center of the frame // so that the user moves it around to select the desired star. int subBinX = 1, subBinY = 1; targetChip->getBinning(&subBinX, &subBinY); focusView->setTrackingBox(QRect((w - focusBoxSize->value()) / (subBinX * 2), (h - focusBoxSize->value()) / (2 * subBinY), focusBoxSize->value() / subBinX, focusBoxSize->value() / subBinY)); focusView->setTrackingBoxEnabled(true); // Now we wait state = Ekos::FOCUS_WAITING; qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state); emit newStatus(state); // If the user does not select for a timeout period, we abort. waitStarSelectTimer.start(); return; } } // Check if the focus module is requested to verify if the minimum HFR value is met. if (minimumRequiredHFR >= 0) { // In case we failed to detected, we capture again. if (currentHFR == -1) { if (noStarCount++ < MAX_RECAPTURE_RETRIES) { appendLogText(i18n("No stars detected, capturing again...")); // On Last Attempt reset focus frame to capture full frame and recapture star if possible if (noStarCount == MAX_RECAPTURE_RETRIES) resetFrame(); capture(); return; } // If we exceeded maximum tries we abort else { noStarCount = 0; setAutoFocusResult(false); } } // If the detect current HFR is more than the minimum required HFR // then we should start the autofocus process now to bring it down. else if (currentHFR > minimumRequiredHFR) { qCDebug(KSTARS_EKOS_FOCUS) << "Current HFR:" << currentHFR << "is above required minimum HFR:" << minimumRequiredHFR << ". Starting AutoFocus..."; inSequenceFocus = true; start(); } // Otherwise, the current HFR is fine and lower than the required minimum HFR so we announce success. else { qCDebug(KSTARS_EKOS_FOCUS) << "Current HFR:" << currentHFR << "is below required minimum HFR:" << minimumRequiredHFR << ". Autofocus successful."; setAutoFocusResult(true); drawProfilePlot(); } // We reset minimum required HFR and call it a day. minimumRequiredHFR = -1; return; } // Let's draw the HFR Plot drawProfilePlot(); // If focus logging is enabled, let's save the frame. if (Options::focusLogging() && Options::saveFocusImages()) { QDir dir; QDateTime now = KStarsData::Instance()->lt(); QString path = KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + "autofocus/" + now.toString("yyyy-MM-dd"); dir.mkpath(path); // IS8601 contains colons but they are illegal under Windows OS, so replacing them with '-' // The timestamp is no longer ISO8601 but it should solve interoperality issues between different OS hosts QString name = "autofocus_frame_" + now.toString("HH-mm-ss") + ".fits"; QString filename = path + QStringLiteral("/") + name; focusView->getImageData()->saveFITS(filename); } // If we are not in autofocus process, we're done. if (inAutoFocus == false) return; // Set state to progress if (state != Ekos::FOCUS_PROGRESS) { state = Ekos::FOCUS_PROGRESS; qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state); emit newStatus(state); } // Now let's kick in the algorithms if (focusAlgorithm == FOCUS_LINEAR) autoFocusLinear(); else if (canAbsMove || canRelMove) // Position-based algorithms autoFocusAbs(); else // Time open-looped algorithms autoFocusRel(); } void Focus::clearDataPoints() { maxHFR = 1; hfr_position.clear(); hfr_value.clear(); polynomialGraph->data()->clear(); focusPoint->data()->clear(); polynomialGraphIsShown = false; HFRPlot->clearItems(); polynomialFit.reset(); drawHFRPlot(); } void Focus::drawHFRIndeces() { // Put the sample number inside the plot point's circle. for (int i = 0; i < hfr_position.size(); ++i) { QCPItemText *textLabel = new QCPItemText(HFRPlot); textLabel->setPositionAlignment(Qt::AlignCenter | Qt::AlignHCenter); textLabel->position->setType(QCPItemPosition::ptPlotCoords); textLabel->position->setCoords(hfr_position[i], hfr_value[i]); textLabel->setText(QString::number(i + 1)); textLabel->setFont(QFont(font().family(), 12)); textLabel->setPen(Qt::NoPen); textLabel->setColor(Qt::red); } } void Focus::drawHFRPlot() { // DrawHFRPlot is the base on which other things are built upon. // Clear any previous annotations. HFRPlot->clearItems(); v_graph->setData(hfr_position, hfr_value); drawHFRIndeces(); double minHFRVal = currentHFR / 2.5; if (hfr_value.size() > 0) minHFRVal = std::max(0, static_cast(0.9 * *std::min_element(hfr_value.begin(), hfr_value.end()))); // True for the position-based algorithms and those that simulate position. if (inFocusLoop == false && (canAbsMove || canRelMove || (focusAlgorithm == FOCUS_LINEAR))) { const double minPosition = hfr_position.empty() ? 0 : *std::min_element(hfr_position.constBegin(), hfr_position.constEnd()); const double maxPosition = hfr_position.empty() ? 1e6 : *std::max_element(hfr_position.constBegin(), hfr_position.constEnd()); HFRPlot->xAxis->setRange(minPosition - pulseDuration, maxPosition + pulseDuration); HFRPlot->yAxis->setRange(minHFRVal, maxHFR); } else { //HFRPlot->xAxis->setLabel(i18n("Iteration")); HFRPlot->xAxis->setRange(1, hfr_value.count() + 1); HFRPlot->yAxis->setRange(currentHFR / 2.5, maxHFR * 1.25); } HFRPlot->replot(); } void Focus::drawProfilePlot() { QVector currentIndexes; QVector currentFrequencies; // HFR = 50% * 1.36 = 68% aka one standard deviation double stdDev = currentHFR * 1.36; float start = -stdDev * 4; float end = stdDev * 4; float step = stdDev * 4 / 20.0; for (double x = start; x < end; x += step) { currentIndexes.append(x); currentFrequencies.append((1 / (stdDev * sqrt(2 * M_PI))) * exp(-1 * (x * x) / (2 * (stdDev * stdDev)))); } currentGaus->setData(currentIndexes, currentFrequencies); if (lastGausIndexes.count() > 0) lastGaus->setData(lastGausIndexes, lastGausFrequencies); if (focusType == FOCUS_AUTO && firstGaus == nullptr) { firstGaus = profilePlot->addGraph(); QPen pen; pen.setStyle(Qt::DashDotLine); pen.setWidth(2); pen.setColor(Qt::darkMagenta); firstGaus->setPen(pen); firstGaus->setData(currentIndexes, currentFrequencies); } else if (firstGaus) { profilePlot->removeGraph(firstGaus); firstGaus = nullptr; } profilePlot->rescaleAxes(); profilePlot->replot(); lastGausIndexes = currentIndexes; lastGausFrequencies = currentFrequencies; profilePixmap = profilePlot->grab(); //.scaled(200, 200, Qt::KeepAspectRatio, Qt::SmoothTransformation); emit newProfilePixmap(profilePixmap); } bool Focus::autoFocusChecks() { if (++absIterations > MAXIMUM_ABS_ITERATIONS) { appendLogText(i18n("Autofocus failed to reach proper focus. Try increasing tolerance value.")); abort(); setAutoFocusResult(false); return false; } // No stars detected, try to capture again if (currentHFR == -1) { if (noStarCount < MAX_RECAPTURE_RETRIES) { appendLogText(i18n("No stars detected, capturing again...")); capture(); noStarCount++; return false; } else if (noStarCount == MAX_RECAPTURE_RETRIES) { currentHFR = 20; noStarCount++; } else { appendLogText(i18n("Failed to detect any stars. Reset frame and try again.")); abort(); setAutoFocusResult(false); return false; } } else noStarCount = 0; return true; } void Focus::autoFocusLinear() { if (!autoFocusChecks()) return; if (!canAbsMove && !canRelMove && canTimerMove) { const bool kFixPosition = true; if (kFixPosition && (linearRequestedPosition != static_cast(currentPosition))) { qCDebug(KSTARS_EKOS_FOCUS) << "Linear: warning, changing position " << currentPosition << " to " << linearRequestedPosition; currentPosition = linearRequestedPosition; } } hfr_position.append(currentPosition); hfr_value.append(currentHFR); drawHFRPlot(); if (hfr_position.size() > 3) { polynomialFit.reset(new PolynomialFit(2, hfr_position, hfr_value)); double min_position, min_value; const FocusAlgorithmInterface::FocusParams ¶ms = linearFocuser->getParams(); double searchMin = std::max(params.minPositionAllowed, params.startPosition - params.maxTravel); double searchMax = std::min(params.maxPositionAllowed, params.startPosition + params.maxTravel); if (polynomialFit->findMinimum(linearFocuser->getParams().startPosition, searchMin, searchMax, &min_position, &min_value)) { QPen pen; pen.setWidth(1); pen.setColor(QColor(180, 180, 180)); polynomialGraph->setPen(pen); polynomialFit->drawPolynomial(HFRPlot, polynomialGraph); polynomialFit->drawMinimum(HFRPlot, focusPoint, min_position, min_value, font()); } else { // During development of this algorithm, we show the polynomial graph in red if // no minimum was found. That happens when the order-2 polynomial is an inverted U // instead of a U shape (i.e. it has a maximum, but no minimum). QPen pen; pen.setWidth(1); pen.setColor(QColor(254, 0, 0)); polynomialGraph->setPen(pen); polynomialFit->drawPolynomial(HFRPlot, polynomialGraph); polynomialGraph->data()->clear(); focusPoint->data()->clear(); } } linearRequestedPosition = linearFocuser->newMeasurement(currentPosition, currentHFR); const int nextPosition = adjustLinearPosition(static_cast(currentPosition), linearRequestedPosition); if (linearRequestedPosition == -1) { if (linearFocuser->isDone() && linearFocuser->solution() != -1) { appendLogText(i18np("Autofocus complete after %1 iteration.", "Autofocus complete after %1 iterations.", hfr_position.count())); stop(); setAutoFocusResult(true); } else { qCDebug(KSTARS_EKOS_FOCUS) << linearFocuser->doneReason(); appendLogText("Linear autofocus algorithm aborted."); abort(); setAutoFocusResult(false); } return; } else { const int delta = nextPosition - currentPosition; if (!changeFocus(delta)) { abort(); setAutoFocusResult(false); } return; } } void Focus::autoFocusAbs() { static int minHFRPos = 0, focusOutLimit = 0, focusInLimit = 0; static double minHFR = 0; double targetPosition = 0, delta = 0; QString deltaTxt = QString("%1").arg(fabs(currentHFR - minHFR) * 100.0, 0, 'g', 3); QString HFRText = QString("%1").arg(currentHFR, 0, 'g', 3); qCDebug(KSTARS_EKOS_FOCUS) << "========================================"; qCDebug(KSTARS_EKOS_FOCUS) << "Current HFR: " << currentHFR << " Current Position: " << currentPosition; qCDebug(KSTARS_EKOS_FOCUS) << "Last minHFR: " << minHFR << " Last MinHFR Pos: " << minHFRPos; qCDebug(KSTARS_EKOS_FOCUS) << "Delta: " << deltaTxt << "%"; qCDebug(KSTARS_EKOS_FOCUS) << "========================================"; if (minHFR) appendLogText(i18n("FITS received. HFR %1 @ %2. Delta (%3%)", HFRText, currentPosition, deltaTxt)); else appendLogText(i18n("FITS received. HFR %1 @ %2.", HFRText, currentPosition)); if (!autoFocusChecks()) return; hfr_position.append(currentPosition); hfr_value.append(currentHFR); drawHFRPlot(); switch (lastFocusDirection) { case FOCUS_NONE: lastHFR = currentHFR; initialFocuserAbsPosition = currentPosition; minHFR = currentHFR; minHFRPos = currentPosition; HFRDec = 0; HFRInc = 0; focusOutLimit = 0; focusInLimit = 0; if (!changeFocus(pulseDuration)) { abort(); setAutoFocusResult(false); } break; case FOCUS_IN: case FOCUS_OUT: static int lastHFRPos = 0, initSlopePos = 0; static double initSlopeHFR = 0; if (reverseDir && focusInLimit && focusOutLimit && fabs(currentHFR - minHFR) < (toleranceIN->value() / 100.0) && HFRInc == 0) { if (absIterations <= 2) { appendLogText( i18n("Change in HFR is too small. Try increasing the step size or decreasing the tolerance.")); abort(); setAutoFocusResult(false); } else if (noStarCount > 0) { appendLogText(i18n("Failed to detect focus star in frame. Capture and select a focus star.")); abort(); setAutoFocusResult(false); } else { appendLogText(i18n("Autofocus complete after %1 iterations.", hfr_position.count())); stop(); setAutoFocusResult(true); if (focusAlgorithm == FOCUS_POLYNOMIAL) graphPolynomialFunction(); } break; } else if (currentHFR < lastHFR) { double slope = 0; // Let's try to calculate slope of the V curve. if (initSlopeHFR == 0 && HFRInc == 0 && HFRDec >= 1) { initSlopeHFR = lastHFR; initSlopePos = lastHFRPos; qCDebug(KSTARS_EKOS_FOCUS) << "Setting initial slop to " << initSlopePos << " @ HFR " << initSlopeHFR; } // Let's now limit the travel distance of the focuser if (lastFocusDirection == FOCUS_OUT && lastHFRPos < focusInLimit && fabs(currentHFR - lastHFR) > 0.1) { focusInLimit = lastHFRPos; qCDebug(KSTARS_EKOS_FOCUS) << "New FocusInLimit " << focusInLimit; } else if (lastFocusDirection == FOCUS_IN && lastHFRPos > focusOutLimit && fabs(currentHFR - lastHFR) > 0.1) { focusOutLimit = lastHFRPos; qCDebug(KSTARS_EKOS_FOCUS) << "New FocusOutLimit " << focusOutLimit; } // If we have slope, get next target position if (initSlopeHFR && absMotionMax > 50) { double factor = 0.5; slope = (currentHFR - initSlopeHFR) / (currentPosition - initSlopePos); if (fabs(currentHFR - minHFR) * 100.0 < 0.5) factor = 1 - fabs(currentHFR - minHFR) * 10; targetPosition = currentPosition + (currentHFR * factor - currentHFR) / slope; if (targetPosition < 0) { factor = 1; while (targetPosition < 0 && factor > 0) { factor -= 0.005; targetPosition = currentPosition + (currentHFR * factor - currentHFR) / slope; } } qCDebug(KSTARS_EKOS_FOCUS) << "Using slope to calculate target pulse..."; } // Otherwise proceed iteratively else { if (lastFocusDirection == FOCUS_IN) targetPosition = currentPosition - pulseDuration; else targetPosition = currentPosition + pulseDuration; qCDebug(KSTARS_EKOS_FOCUS) << "Proceeding iteratively to next target pulse ..."; } qCDebug(KSTARS_EKOS_FOCUS) << "V-Curve Slope " << slope << " current Position " << currentPosition << " targetPosition " << targetPosition; lastHFR = currentHFR; // Let's keep track of the minimum HFR if (lastHFR < minHFR) { minHFR = lastHFR; minHFRPos = currentPosition; qCDebug(KSTARS_EKOS_FOCUS) << "new minHFR " << minHFR << " @ position " << minHFRPos; } lastHFRPos = currentPosition; // HFR is decreasing, we are on the right direction HFRDec++; HFRInc = 0; } else { // HFR increased, let's deal with it. HFRInc++; HFRDec = 0; // Reality Check: If it's first time, let's capture again and see if it changes. /*if (HFRInc <= 1 && reverseDir == false) { capture(); return; } // Looks like we're going away from optimal HFR else {*/ reverseDir = true; lastHFR = currentHFR; lastHFRPos = currentPosition; initSlopeHFR = 0; HFRInc = 0; qCDebug(KSTARS_EKOS_FOCUS) << "Focus is moving away from optimal HFR."; // Let's set new limits if (lastFocusDirection == FOCUS_IN) { focusInLimit = currentPosition; qCDebug(KSTARS_EKOS_FOCUS) << "Setting focus IN limit to " << focusInLimit; if (hfr_position.count() > 3) { focusOutLimit = hfr_position[hfr_position.count() - 3]; qCDebug(KSTARS_EKOS_FOCUS) << "Setting focus OUT limit to " << focusOutLimit; } } else { focusOutLimit = currentPosition; qCDebug(KSTARS_EKOS_FOCUS) << "Setting focus OUT limit to " << focusOutLimit; if (hfr_position.count() > 3) { focusInLimit = hfr_position[hfr_position.count() - 3]; qCDebug(KSTARS_EKOS_FOCUS) << "Setting focus IN limit to " << focusInLimit; } } bool polyMinimumFound = false; if (focusAlgorithm == FOCUS_POLYNOMIAL && hfr_position.count() > 5) { polynomialFit.reset(new PolynomialFit(3, hfr_position, hfr_value)); double a = *std::min_element(hfr_position.constBegin(), hfr_position.constEnd()); double b = *std::max_element(hfr_position.constBegin(), hfr_position.constEnd()); double min_position = 0, min_hfr = 0; polyMinimumFound = polynomialFit->findMinimum(minHFRPos, a, b, &min_position, &min_hfr); qCDebug(KSTARS_EKOS_FOCUS) << "Found Minimum?" << (polyMinimumFound ? "Yes" : "No"); if (polyMinimumFound) { qCDebug(KSTARS_EKOS_FOCUS) << "Minimum Solution:" << min_hfr << "@" << min_position; polySolutionFound++; targetPosition = floor(min_position); appendLogText(i18n("Found polynomial solution @ %1", QString::number(min_position, 'f', 0))); polynomialFit->drawPolynomial(HFRPlot, polynomialGraph); polynomialFit->drawMinimum(HFRPlot, focusPoint, min_position, min_hfr, font()); } } if (polyMinimumFound == false) { // Decrease pulse pulseDuration = pulseDuration * 0.75; // Let's get close to the minimum HFR position so far detected if (lastFocusDirection == FOCUS_OUT) targetPosition = minHFRPos - pulseDuration / 2; else targetPosition = minHFRPos + pulseDuration / 2; } qCDebug(KSTARS_EKOS_FOCUS) << "new targetPosition " << targetPosition; } // Limit target Pulse to algorithm limits if (focusInLimit != 0 && lastFocusDirection == FOCUS_IN && targetPosition < focusInLimit) { targetPosition = focusInLimit; qCDebug(KSTARS_EKOS_FOCUS) << "Limiting target pulse to focus in limit " << targetPosition; } else if (focusOutLimit != 0 && lastFocusDirection == FOCUS_OUT && targetPosition > focusOutLimit) { targetPosition = focusOutLimit; qCDebug(KSTARS_EKOS_FOCUS) << "Limiting target pulse to focus out limit " << targetPosition; } // Limit target pulse to focuser limits if (targetPosition < absMotionMin) targetPosition = absMotionMin; else if (targetPosition > absMotionMax) targetPosition = absMotionMax; // Ops, we can't go any further, we're done. if (targetPosition == currentPosition) { appendLogText(i18n("Autofocus complete after %1 iterations.", hfr_position.count())); stop(); setAutoFocusResult(true); if (focusAlgorithm == FOCUS_POLYNOMIAL) graphPolynomialFunction(); return; } // Ops, deadlock if (focusOutLimit && focusOutLimit == focusInLimit) { appendLogText(i18n("Deadlock reached. Please try again with different settings.")); abort(); setAutoFocusResult(false); return; } if (fabs(targetPosition - initialFocuserAbsPosition) > maxTravelIN->value()) { int minTravelLimit = qMax(0.0, initialFocuserAbsPosition - maxTravelIN->value()); int maxTravelLimit = qMin(absMotionMax, initialFocuserAbsPosition + maxTravelIN->value()); // In case we are asked to go below travel limit, but we are not there yet // let us go there and see the result before aborting if (fabs(currentPosition - minTravelLimit) > 10 && targetPosition < minTravelLimit) { targetPosition = minTravelLimit; } // Same for max travel else if (fabs(currentPosition - maxTravelLimit) > 10 && targetPosition > maxTravelLimit) { targetPosition = maxTravelLimit; } else { qCDebug(KSTARS_EKOS_FOCUS) << "targetPosition (" << targetPosition << ") - initHFRAbsPos (" << initialFocuserAbsPosition << ") exceeds maxTravel distance of " << maxTravelIN->value(); appendLogText("Maximum travel limit reached. Autofocus aborted."); abort(); setAutoFocusResult(false); break; } } // Get delta for next move delta = (targetPosition - currentPosition); qCDebug(KSTARS_EKOS_FOCUS) << "delta (targetPosition - currentPosition) " << delta; // Limit to Maximum permitted delta (Max Single Step Size) double limitedDelta = qMax(-1.0 * maxSingleStepIN->value(), qMin(1.0 * maxSingleStepIN->value(), delta)); if (std::fabs(limitedDelta - delta) > 0) { qCDebug(KSTARS_EKOS_FOCUS) << "Limited delta to maximum permitted single step " << maxSingleStepIN->value(); delta = limitedDelta; } // Now cross your fingers and wait if (!changeFocus(delta)) { abort(); setAutoFocusResult(false); } break; } } void Focus::graphPolynomialFunction() { if (polynomialGraph && polynomialFit) { polynomialGraphIsShown = true; polynomialFit->drawPolynomial(HFRPlot, polynomialGraph); } } void Focus::autoFocusRel() { static int noStarCount = 0; static double minHFR = 1e6; QString deltaTxt = QString("%1").arg(fabs(currentHFR - minHFR) * 100.0, 0, 'g', 2); QString minHFRText = QString("%1").arg(minHFR, 0, 'g', 3); QString HFRText = QString("%1").arg(currentHFR, 0, 'g', 3); appendLogText(i18n("FITS received. HFR %1. Delta (%2%) Min HFR (%3)", HFRText, deltaTxt, minHFRText)); if (pulseDuration <= MINIMUM_PULSE_TIMER) { appendLogText(i18n("Autofocus failed to reach proper focus. Try adjusting the tolerance value.")); abort(); setAutoFocusResult(false); return; } // No stars detected, try to capture again if (currentHFR == -1) { if (noStarCount++ < MAX_RECAPTURE_RETRIES) { appendLogText(i18n("No stars detected, capturing again...")); capture(); return; } else currentHFR = 20; } else noStarCount = 0; switch (lastFocusDirection) { case FOCUS_NONE: lastHFR = currentHFR; minHFR = 1e6; changeFocus(-pulseDuration); break; case FOCUS_IN: case FOCUS_OUT: if (fabs(currentHFR - minHFR) < (toleranceIN->value() / 100.0) && HFRInc == 0) { appendLogText(i18n("Autofocus complete after %1 iterations.", hfr_position.count())); stop(); setAutoFocusResult(true); if (focusAlgorithm == FOCUS_POLYNOMIAL) graphPolynomialFunction(); break; } else if (currentHFR < lastHFR) { if (currentHFR < minHFR) minHFR = currentHFR; lastHFR = currentHFR; changeFocus(lastFocusDirection == FOCUS_IN ? -pulseDuration : pulseDuration); HFRInc = 0; } else { HFRInc++; lastHFR = currentHFR; HFRInc = 0; pulseDuration *= 0.75; if (!changeFocus(lastFocusDirection == FOCUS_IN ? pulseDuration : -pulseDuration)) { abort(); setAutoFocusResult(false); } } break; } } /*void Focus::registerFocusProperty(INDI::Property *prop) { // Return if it is not our current focuser if (strcmp(prop->getDeviceName(), currentFocuser->getDeviceName())) return; // Do not make unnecessary function call // Check if current focuser supports absolute mode if (canAbsMove == false && currentFocuser->canAbsMove()) { canAbsMove = true; getAbsFocusPosition(); absTicksSpin->setEnabled(true); absTicksLabel->setEnabled(true); startGotoB->setEnabled(true); } // Do not make unnecessary function call // Check if current focuser supports relative mode if (canRelMove == false && currentFocuser->canRelMove()) canRelMove = true; if (canTimerMove == false && currentFocuser->canTimerMove()) { canTimerMove = true; resetButtons(); } }*/ void Focus::autoFocusProcessPositionChange(IPState state) { if (state == IPS_OK && captureInProgress == false) { // Normally, if we are auto-focusing, after we move the focuser we capture an image. // However, the Linear algorithm, at the start of its passes, requires two // consecutive focuser moves--the first out further than we want, and a second // move back in, so that we eliminate backlash and are always moving in before a capture. if (focuserAdditionalMovement > 0) { int temp = focuserAdditionalMovement; focuserAdditionalMovement = 0; qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: un-doing extension. Moving back in by %1").arg(temp); if (!focusIn(temp)) { appendLogText(i18n("Focuser error, check INDI panel.")); abort(); setAutoFocusResult(false); } } else { QTimer::singleShot(FocusSettleTime->value() * 1000, this, &Ekos::Focus::capture); } } else if (state == IPS_ALERT) { appendLogText(i18n("Focuser error, check INDI panel.")); abort(); setAutoFocusResult(false); } } void Focus::processFocusNumber(INumberVectorProperty *nvp) { qCDebug(KSTARS_EKOS_FOCUS) << QString("processFocusNumber %1 %2") .arg(nvp->name).arg(nvp->s == IPS_OK ? "OK" : "ERROR"); // Return if it is not our current focuser if (nvp->device != currentFocuser->getDeviceName()) return; if (!strcmp(nvp->name, "FOCUS_BACKLASH_STEPS")) { focusBacklashSpin->setValue(nvp->np[0].value); return; } if (!strcmp(nvp->name, "ABS_FOCUS_POSITION")) { INumber *pos = IUFindNumber(nvp, "FOCUS_ABSOLUTE_POSITION"); if (pos) { currentPosition = pos->value; qCDebug(KSTARS_EKOS_FOCUS) << QString("Abs Focuser position changed to %1").arg(currentPosition); absTicksLabel->setText(QString::number(static_cast(currentPosition))); emit absolutePositionChanged(currentPosition); } if (adjustFocus && nvp->s == IPS_OK) { adjustFocus = false; lastFocusDirection = FOCUS_NONE; emit focusPositionAdjusted(); return; } if (resetFocus && nvp->s == IPS_OK) { resetFocus = false; appendLogText(i18n("Restarting autofocus process...")); start(); } if (canAbsMove && inAutoFocus) { autoFocusProcessPositionChange(nvp->s); } else if (nvp->s == IPS_ALERT) appendLogText(i18n("Focuser error, check INDI panel.")); return; } if (canAbsMove) return; if (!strcmp(nvp->name, "manualfocusdrive")) { INumber *pos = IUFindNumber(nvp, "manualfocusdrive"); if (pos && nvp->s == IPS_OK) { currentPosition += pos->value; absTicksLabel->setText(QString::number(static_cast(currentPosition))); emit absolutePositionChanged(currentPosition); } if (adjustFocus && nvp->s == IPS_OK) { adjustFocus = false; lastFocusDirection = FOCUS_NONE; emit focusPositionAdjusted(); return; } if (resetFocus && nvp->s == IPS_OK) { resetFocus = false; appendLogText(i18n("Restarting autofocus process...")); start(); } if (canRelMove && inAutoFocus) { autoFocusProcessPositionChange(nvp->s); } else if (nvp->s == IPS_ALERT) appendLogText(i18n("Focuser error, check INDI panel.")); return; } if (!strcmp(nvp->name, "REL_FOCUS_POSITION")) { INumber *pos = IUFindNumber(nvp, "FOCUS_RELATIVE_POSITION"); if (pos && nvp->s == IPS_OK) { currentPosition += pos->value * (lastFocusDirection == FOCUS_IN ? -1 : 1); qCDebug(KSTARS_EKOS_FOCUS) << QString("Rel Focuser position changed by %1 to %2") .arg(pos->value).arg(currentPosition); absTicksLabel->setText(QString::number(static_cast(currentPosition))); emit absolutePositionChanged(currentPosition); } if (adjustFocus && nvp->s == IPS_OK) { adjustFocus = false; lastFocusDirection = FOCUS_NONE; emit focusPositionAdjusted(); return; } if (resetFocus && nvp->s == IPS_OK) { resetFocus = false; appendLogText(i18n("Restarting autofocus process...")); start(); } if (canRelMove && inAutoFocus) { autoFocusProcessPositionChange(nvp->s); } else if (nvp->s == IPS_ALERT) appendLogText(i18n("Focuser error, check INDI panel.")); return; } if (canRelMove) return; if (!strcmp(nvp->name, "FOCUS_TIMER")) { if (resetFocus && nvp->s == IPS_OK) { resetFocus = false; appendLogText(i18n("Restarting autofocus process...")); start(); } if (canAbsMove == false && canRelMove == false && inAutoFocus) { // Used by the linear focus algorithm. Ignored if that's not in use for the timer-focuser. INumber *pos = IUFindNumber(nvp, "FOCUS_TIMER_VALUE"); if (pos) { currentPosition += pos->value * (lastFocusDirection == FOCUS_IN ? -1 : 1); qCDebug(KSTARS_EKOS_FOCUS) << QString("Timer Focuser position changed by %1 to %2") .arg(pos->value).arg(currentPosition); } autoFocusProcessPositionChange(nvp->s); } else if (nvp->s == IPS_ALERT) appendLogText(i18n("Focuser error, check INDI panel.")); return; } } void Focus::appendLogText(const QString &text) { m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2", KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text)); qCInfo(KSTARS_EKOS_FOCUS) << text; emit newLog(text); } void Focus::clearLog() { m_LogText.clear(); emit newLog(QString()); } void Focus::startFraming() { if (currentCCD == nullptr) { appendLogText(i18n("No CCD connected.")); return; } waitStarSelectTimer.stop(); inFocusLoop = true; HFRFrames.clear(); clearDataPoints(); //emit statusUpdated(true); state = Ekos::FOCUS_FRAMING; qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state); emit newStatus(state); resetButtons(); appendLogText(i18n("Starting continuous exposure...")); capture(); } void Focus::resetButtons() { if (inFocusLoop) { startFocusB->setEnabled(false); startLoopB->setEnabled(false); stopFocusB->setEnabled(true); captureB->setEnabled(false); return; } if (inAutoFocus) { stopFocusB->setEnabled(true); startFocusB->setEnabled(false); startLoopB->setEnabled(false); captureB->setEnabled(false); focusOutB->setEnabled(false); focusInB->setEnabled(false); startGotoB->setEnabled(false); stopGotoB->setEnabled(false); resetFrameB->setEnabled(false); return; } if (currentFocuser) { focusOutB->setEnabled(true); focusInB->setEnabled(true); startFocusB->setEnabled(focusType == FOCUS_AUTO); startGotoB->setEnabled(canAbsMove); stopGotoB->setEnabled(true); } else { focusOutB->setEnabled(false); focusInB->setEnabled(false); startFocusB->setEnabled(false); startGotoB->setEnabled(false); stopGotoB->setEnabled(false); } stopFocusB->setEnabled(false); startLoopB->setEnabled(true); if (captureInProgress == false) { captureB->setEnabled(true); resetFrameB->setEnabled(true); } } void Focus::updateBoxSize(int value) { if (currentCCD == nullptr) return; ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); if (targetChip == nullptr) return; int subBinX, subBinY; targetChip->getBinning(&subBinX, &subBinY); QRect trackBox = focusView->getTrackingBox(); QPoint center(trackBox.x() + (trackBox.width() / 2), trackBox.y() + (trackBox.height() / 2)); trackBox = QRect(center.x() - value / (2 * subBinX), center.y() - value / (2 * subBinY), value / subBinX, value / subBinY); focusView->setTrackingBox(trackBox); } void Focus::focusStarSelected(int x, int y) { if (state == Ekos::FOCUS_PROGRESS) return; if (subFramed == false) { rememberStarCenter.setX(x); rememberStarCenter.setY(y); } ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); int subBinX, subBinY; targetChip->getBinning(&subBinX, &subBinY); // If binning was changed outside of the focus module, recapture if (subBinX != activeBin) { capture(); return; } int offset = (static_cast(focusBoxSize->value()) / subBinX) * 1.5; QRect starRect; bool squareMovedOutside = false; if (subFramed == false && useSubFrame->isChecked() && targetChip->canSubframe()) { int minX, maxX, minY, maxY, minW, maxW, minH, maxH; //, fx,fy,fw,fh; targetChip->getFrameMinMax(&minX, &maxX, &minY, &maxY, &minW, &maxW, &minH, &maxH); //targetChip->getFrame(&fx, &fy, &fw, &fy); x = (x - offset) * subBinX; y = (y - offset) * subBinY; int w = offset * 2 * subBinX; int h = offset * 2 * subBinY; if (x < minX) x = minX; if (y < minY) y = minY; if ((x + w) > maxW) w = maxW - x; if ((y + h) > maxH) h = maxH - y; //fx += x; //fy += y; //fw = w; //fh = h; //targetChip->setFocusFrame(fx, fy, fw, fh); //frameModified=true; QVariantMap settings = frameSettings[targetChip]; settings["x"] = x; settings["y"] = y; settings["w"] = w; settings["h"] = h; settings["binx"] = subBinX; settings["biny"] = subBinY; frameSettings[targetChip] = settings; subFramed = true; qCDebug(KSTARS_EKOS_FOCUS) << "Frame is subframed. X:" << x << "Y:" << y << "W:" << w << "H:" << h << "binX:" << subBinX << "binY:" << subBinY; focusView->setFirstLoad(true); capture(); //starRect = QRect((w-focusBoxSize->value())/(subBinX*2), (h-focusBoxSize->value())/(subBinY*2), focusBoxSize->value()/subBinX, focusBoxSize->value()/subBinY); starCenter.setX(w / (2 * subBinX)); starCenter.setY(h / (2 * subBinY)); } else { //starRect = QRect(x-focusBoxSize->value()/(subBinX*2), y-focusBoxSize->value()/(subBinY*2), focusBoxSize->value()/subBinX, focusBoxSize->value()/subBinY); double dist = sqrt((starCenter.x() - x) * (starCenter.x() - x) + (starCenter.y() - y) * (starCenter.y() - y)); squareMovedOutside = (dist > (static_cast(focusBoxSize->value()) / subBinX)); starCenter.setX(x); starCenter.setY(y); //starRect = QRect( starCenter.x()-focusBoxSize->value()/(2*subBinX), starCenter.y()-focusBoxSize->value()/(2*subBinY), focusBoxSize->value()/subBinX, focusBoxSize->value()/subBinY); starRect = QRect(starCenter.x() - focusBoxSize->value() / (2 * subBinX), starCenter.y() - focusBoxSize->value() / (2 * subBinY), focusBoxSize->value() / subBinX, focusBoxSize->value() / subBinY); focusView->setTrackingBox(starRect); } starsHFR.clear(); starCenter.setZ(subBinX); //starSelected=true; defaultScale = static_cast(filterCombo->currentIndex()); if (squareMovedOutside && inAutoFocus == false && useAutoStar->isChecked()) { useAutoStar->blockSignals(true); useAutoStar->setChecked(false); useAutoStar->blockSignals(false); appendLogText(i18n("Disabling Auto Star Selection as star selection box was moved manually.")); starSelected = false; } else if (starSelected == false) { appendLogText(i18n("Focus star is selected.")); starSelected = true; capture(); } waitStarSelectTimer.stop(); state = inAutoFocus ? FOCUS_PROGRESS : FOCUS_IDLE; qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state); emit newStatus(state); } void Focus::checkFocus(double requiredHFR) { qCDebug(KSTARS_EKOS_FOCUS) << "Check Focus requested with minimum required HFR" << requiredHFR; minimumRequiredHFR = requiredHFR; capture(); } void Focus::toggleSubframe(bool enable) { if (enable == false) resetFrame(); starSelected = false; starCenter = QVector3D(); if (useFullField->isChecked()) useFullField->setChecked(!enable); } void Focus::filterChangeWarning(int index) { // index = 4 is MEDIAN filter which helps reduce noise if (index != 0 && index != FITS_MEDIAN) appendLogText(i18n("Warning: Only use filters for preview as they may interface with autofocus operation.")); Options::setFocusEffect(index); defaultScale = static_cast(index); } void Focus::setExposure(double value) { exposureIN->setValue(value); } void Focus::setBinning(int subBinX, int subBinY) { INDI_UNUSED(subBinY); binningCombo->setCurrentIndex(subBinX - 1); } void Focus::setImageFilter(const QString &value) { for (int i = 0; i < filterCombo->count(); i++) if (filterCombo->itemText(i) == value) { filterCombo->setCurrentIndex(i); break; } } void Focus::setAutoStarEnabled(bool enable) { useAutoStar->setChecked(enable); Options::setFocusAutoStarEnabled(enable); } void Focus::setAutoSubFrameEnabled(bool enable) { useSubFrame->setChecked(enable); Options::setFocusSubFrame(enable); } void Focus::setAutoFocusParameters(int boxSize, int stepSize, int maxTravel, double tolerance) { focusBoxSize->setValue(boxSize); stepIN->setValue(stepSize); maxTravelIN->setValue(maxTravel); toleranceIN->setValue(tolerance); } void Focus::setAutoFocusResult(bool status) { qCDebug(KSTARS_EKOS_FOCUS) << "AutoFocus result:" << status; if (status) { // CR add auto focus position, temperature and filter to log in CSV format // this will help with setting up focus offsets and temperature compensation INDI::Property * np = currentFocuser->getProperty("TemperatureNP"); double temperature = -274; // impossible temperature as a signal that it isn't available if (np != nullptr) { INumberVectorProperty * tnp = np->getNumber(); temperature = tnp->np[0].value; } qCInfo(KSTARS_EKOS_FOCUS) << "Autofocus values: position, " << currentPosition << ", temperature, " << temperature << ", filter, " << filter(); } // In case of failure, go back to last position if the focuser is absolute if (status == false && canAbsMove && currentFocuser && currentFocuser->isConnected() && initialFocuserAbsPosition >= 0) { currentFocuser->moveAbs(initialFocuserAbsPosition); appendLogText(i18n("Autofocus failed, moving back to initial focus position %1.", initialFocuserAbsPosition)); // If we're doing in sequence focusing using an absolute focuser, let's retry focusing starting from last known good position before we give up if (inSequenceFocus && resetFocusIteration++ < MAXIMUM_RESET_ITERATIONS && resetFocus == false) { resetFocus = true; // Reset focus frame in case the star in subframe was lost resetFrame(); return; } } int settleTime = m_GuidingSuspended ? GuideSettleTime->value() : 0; // Always resume guiding if we suspended it before if (m_GuidingSuspended) { emit resumeGuiding(); m_GuidingSuspended = false; } resetFocusIteration = 0; if (settleTime > 0) appendLogText(i18n("Settling...")); QTimer::singleShot(settleTime * 1000, this, [ &, status, settleTime]() { if (settleTime > 0) appendLogText(i18n("Settling complete.")); if (status) { KSNotification::event(QLatin1String("FocusSuccessful"), i18n("Autofocus operation completed successfully")); state = Ekos::FOCUS_COMPLETE; } else { KSNotification::event(QLatin1String("FocusFailed"), i18n("Autofocus operation failed with errors"), KSNotification::EVENT_ALERT); state = Ekos::FOCUS_FAILED; } qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state); // Do not emit result back yet if we have a locked filter pending return to original filter if (fallbackFilterPending) { filterManager->setFilterPosition(fallbackFilterPosition, static_cast(FilterManager::CHANGE_POLICY | FilterManager::OFFSET_POLICY)); return; } emit newStatus(state); }); } void Focus::checkAutoStarTimeout() { //if (starSelected == false && inAutoFocus) if (starCenter.isNull() && (inAutoFocus || minimumRequiredHFR > 0)) { if (inAutoFocus) { if (rememberStarCenter.isNull() == false) { focusStarSelected(rememberStarCenter.x(), rememberStarCenter.y()); appendLogText(i18n("No star was selected. Using last known position...")); return; } } appendLogText(i18n("No star was selected. Aborting...")); initialFocuserAbsPosition = -1; abort(); setAutoFocusResult(false); } else if (state == FOCUS_WAITING) { state = FOCUS_IDLE; qCDebug(KSTARS_EKOS_FOCUS) << "State:" << Ekos::getFocusStatusString(state); emit newStatus(state); } } void Focus::setAbsoluteFocusTicks() { if (currentFocuser == nullptr) return; if (currentFocuser->isConnected() == false) { appendLogText(i18n("Error: Lost connection to Focuser.")); return; } qCDebug(KSTARS_EKOS_FOCUS) << "Setting focus ticks to " << absTicksSpin->value(); currentFocuser->moveAbs(absTicksSpin->value()); } //void Focus::setActiveBinning(int bin) //{ // activeBin = bin + 1; // Options::setFocusXBin(activeBin); //} // TODO remove from kstars.kcfg /*void Focus::setFrames(int value) { Options::setFocusFrames(value); }*/ void Focus::syncTrackingBoxPosition() { ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); Q_ASSERT(targetChip); int subBinX = 1, subBinY = 1; targetChip->getBinning(&subBinX, &subBinY); if (starCenter.isNull() == false) { double boxSize = focusBoxSize->value(); int x, y, w, h; targetChip->getFrame(&x, &y, &w, &h); // If box size is larger than image size, set it to lower index if (boxSize / subBinX >= w || boxSize / subBinY >= h) { focusBoxSize->setValue((boxSize / subBinX >= w) ? w : h); return; } // If binning changed, update coords accordingly if (subBinX != starCenter.z()) { if (starCenter.z() > 0) { starCenter.setX(starCenter.x() * (starCenter.z() / subBinX)); starCenter.setY(starCenter.y() * (starCenter.z() / subBinY)); } starCenter.setZ(subBinX); } QRect starRect = QRect(starCenter.x() - boxSize / (2 * subBinX), starCenter.y() - boxSize / (2 * subBinY), boxSize / subBinX, boxSize / subBinY); focusView->setTrackingBoxEnabled(true); focusView->setTrackingBox(starRect); } } void Focus::showFITSViewer() { FITSData *data = focusView->getImageData(); if (data) { QUrl url = QUrl::fromLocalFile(data->filename()); if (fv.isNull()) { if (Options::singleWindowCapturedFITS()) fv = KStars::Instance()->genericFITSViewer(); else { fv = new FITSViewer(Options::independentWindowFITS() ? nullptr : KStars::Instance()); KStars::Instance()->addFITSViewer(fv); } fv->addFITS(url); FITSView *currentView = fv->getCurrentView(); if (currentView) currentView->getImageData()->setAutoRemoveTemporaryFITS(false); } else fv->updateFITS(url, 0); fv->show(); } } void Focus::adjustFocusOffset(int value, bool useAbsoluteOffset) { adjustFocus = true; int relativeOffset = 0; if (useAbsoluteOffset == false) relativeOffset = value; else relativeOffset = value - currentPosition; changeFocus(relativeOffset); } void Focus::toggleFocusingWidgetFullScreen() { if (focusingWidget->parent() == nullptr) { focusingWidget->setParent(this); rightLayout->insertWidget(0, focusingWidget); focusingWidget->showNormal(); } else { focusingWidget->setParent(nullptr); focusingWidget->setWindowTitle(i18n("Focus Frame")); focusingWidget->setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::CustomizeWindowHint); focusingWidget->showMaximized(); focusingWidget->show(); } } void Focus::setMountStatus(ISD::Telescope::Status newState) { switch (newState) { case ISD::Telescope::MOUNT_PARKING: case ISD::Telescope::MOUNT_SLEWING: case ISD::Telescope::MOUNT_MOVING: captureB->setEnabled(false); startFocusB->setEnabled(false); startLoopB->setEnabled(false); // If mount is moved while we have a star selected and subframed // let us reset the frame. if (subFramed) resetFrame(); break; default: resetButtons(); break; } } void Focus::removeDevice(ISD::GDInterface *deviceRemoved) { // Check in Focusers for (ISD::GDInterface *focuser : Focusers) { if (focuser->getDeviceName() == deviceRemoved->getDeviceName()) { Focusers.removeAll(dynamic_cast(focuser)); focuserCombo->removeItem(focuserCombo->findText(focuser->getDeviceName())); checkFocuser(); resetButtons(); } } // Check in CCDs for (ISD::GDInterface *ccd : CCDs) { if (ccd->getDeviceName() == deviceRemoved->getDeviceName()) { CCDs.removeAll(dynamic_cast(ccd)); CCDCaptureCombo->removeItem(CCDCaptureCombo->findText(ccd->getDeviceName())); CCDCaptureCombo->removeItem(CCDCaptureCombo->findText(ccd->getDeviceName() + QString(" Guider"))); if (CCDs.empty()) { currentCCD = nullptr; CCDCaptureCombo->setCurrentIndex(-1); } else CCDCaptureCombo->setCurrentIndex(0); checkCCD(); resetButtons(); } } // Check in Filters for (ISD::GDInterface *filter : Filters) { if (filter->getDeviceName() == deviceRemoved->getDeviceName()) { Filters.removeAll(filter); FilterDevicesCombo->removeItem(FilterDevicesCombo->findText(filter->getDeviceName())); if (Filters.empty()) { currentFilter = nullptr; FilterDevicesCombo->setCurrentIndex(-1); } else FilterDevicesCombo->setCurrentIndex(0); checkFilter(); resetButtons(); } } } void Focus::setFilterManager(const QSharedPointer &manager) { filterManager = manager; connect(filterManagerB, &QPushButton::clicked, [this]() { filterManager->show(); filterManager->raise(); }); connect(filterManager.data(), &FilterManager::ready, [this]() { if (filterPositionPending) { filterPositionPending = false; capture(); } else if (fallbackFilterPending) { fallbackFilterPending = false; emit newStatus(state); } } ); connect(filterManager.data(), &FilterManager::failed, [this]() { appendLogText(i18n("Filter operation failed.")); abort(); } ); connect(this, &Focus::newStatus, [this](Ekos::FocusState state) { if (FilterPosCombo->currentIndex() != -1 && canAbsMove && state == Ekos::FOCUS_COMPLETE) { filterManager->setFilterAbsoluteFocusPosition(FilterPosCombo->currentIndex(), currentPosition); } }); connect(exposureIN, &QDoubleSpinBox::editingFinished, [this]() { if (currentFilter) filterManager->setFilterExposure(FilterPosCombo->currentIndex(), exposureIN->value()); else Options::setFocusExposure(exposureIN->value()); }); connect(filterManager.data(), &FilterManager::labelsChanged, this, [this]() { FilterPosCombo->clear(); FilterPosCombo->addItems(filterManager->getFilterLabels()); currentFilterPosition = filterManager->getFilterPosition(); FilterPosCombo->setCurrentIndex(currentFilterPosition - 1); //Options::setDefaultFocusFilterWheelFilter(FilterPosCombo->currentText()); }); connect(filterManager.data(), &FilterManager::positionChanged, this, [this]() { currentFilterPosition = filterManager->getFilterPosition(); FilterPosCombo->setCurrentIndex(currentFilterPosition - 1); //Options::setDefaultFocusFilterWheelFilter(FilterPosCombo->currentText()); }); connect(filterManager.data(), &FilterManager::exposureChanged, this, [this]() { exposureIN->setValue(filterManager->getFilterExposure()); }); connect(FilterPosCombo, static_cast(&QComboBox::currentIndexChanged), [ = ](const QString & text) { exposureIN->setValue(filterManager->getFilterExposure(text)); //Options::setDefaultFocusFilterWheelFilter(text); }); } void Focus::toggleVideo(bool enabled) { if (currentCCD == nullptr) return; if (currentCCD->isBLOBEnabled() == false) { if (Options::guiderType() != Ekos::Guide::GUIDE_INTERNAL) currentCCD->setBLOBEnabled(true); else { connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, enabled]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); currentCCD->setVideoStreamEnabled(enabled); }); KSMessageBox::Instance()->questionYesNo(i18n("Image transfer is disabled for this camera. Would you like to enable it?")); } } else currentCCD->setVideoStreamEnabled(enabled); } void Focus::setVideoStreamEnabled(bool enabled) { if (enabled) { liveVideoB->setChecked(true); liveVideoB->setIcon(QIcon::fromTheme("camera-on")); } else { liveVideoB->setChecked(false); liveVideoB->setIcon(QIcon::fromTheme("camera-ready")); } } void Focus::processCaptureTimeout() { captureTimeoutCounter++; if (captureTimeoutCounter >= 3) { captureTimeoutCounter = 0; appendLogText(i18n("Exposure timeout. Aborting...")); abort(); if (inAutoFocus) setAutoFocusResult(false); else if (m_GuidingSuspended) { emit resumeGuiding(); m_GuidingSuspended = false; } return; } appendLogText(i18n("Exposure timeout. Restarting exposure...")); ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); targetChip->abortExposure(); targetChip->capture(exposureIN->value()); captureTimeout.start(exposureIN->value() * 1000 + FOCUS_TIMEOUT_THRESHOLD); } void Focus::processCaptureFailure() { captureFailureCounter++; if (captureFailureCounter >= 3) { captureFailureCounter = 0; appendLogText(i18n("Exposure failure. Aborting...")); abort(); if (inAutoFocus) setAutoFocusResult(false); else if (m_GuidingSuspended) { emit resumeGuiding(); m_GuidingSuspended = false; } return; } appendLogText(i18n("Exposure failure. Restarting exposure...")); ISD::CCDChip *targetChip = currentCCD->getChip(ISD::CCDChip::PRIMARY_CCD); targetChip->abortExposure(); targetChip->capture(exposureIN->value()); } void Focus::syncSettings() { QDoubleSpinBox *dsb = nullptr; QSpinBox *sb = nullptr; QCheckBox *cb = nullptr; QComboBox *cbox = nullptr; if ( (dsb = qobject_cast(sender()))) { /////////////////////////////////////////////////////////////////////////// /// Focuser Group /////////////////////////////////////////////////////////////////////////// if (dsb == FocusSettleTime) Options::setFocusSettleTime(dsb->value()); /////////////////////////////////////////////////////////////////////////// /// CCD & Filter Wheel Group /////////////////////////////////////////////////////////////////////////// else if (dsb == gainIN) Options::setFocusGain(dsb->value()); /////////////////////////////////////////////////////////////////////////// /// Settings Group /////////////////////////////////////////////////////////////////////////// else if (dsb == fullFieldInnerRing) Options::setFocusFullFieldInnerRadius(dsb->value()); else if (dsb == fullFieldOuterRing) Options::setFocusFullFieldOuterRadius(dsb->value()); else if (dsb == GuideSettleTime) Options::setGuideSettleTime(dsb->value()); else if (dsb == maxTravelIN) Options::setFocusMaxTravel(dsb->value()); else if (dsb == toleranceIN) Options::setFocusTolerance(dsb->value()); else if (dsb == thresholdSpin) Options::setFocusThreshold(dsb->value()); } else if ( (sb = qobject_cast(sender()))) { /////////////////////////////////////////////////////////////////////////// /// Settings Group /////////////////////////////////////////////////////////////////////////// if (sb == focusBoxSize) Options::setFocusBoxSize(sb->value()); else if (sb == stepIN) Options::setFocusTicks(sb->value()); else if (sb == maxSingleStepIN) Options::setFocusMaxSingleStep(sb->value()); else if (sb == focusFramesSpin) Options::setFocusFramesCount(sb->value()); } else if ( (cb = qobject_cast(sender()))) { /////////////////////////////////////////////////////////////////////////// /// Settings Group /////////////////////////////////////////////////////////////////////////// if (cb == useAutoStar) Options::setFocusAutoStarEnabled(cb->isChecked()); else if (cb == useSubFrame) Options::setFocusSubFrame(cb->isChecked()); else if (cb == darkFrameCheck) Options::setUseFocusDarkFrame(cb->isChecked()); else if (cb == useFullField) Options::setFocusUseFullField(cb->isChecked()); else if (cb == suspendGuideCheck) Options::setSuspendGuiding(cb->isChecked()); } else if ( (cbox = qobject_cast(sender()))) { /////////////////////////////////////////////////////////////////////////// /// CCD & Filter Wheel Group /////////////////////////////////////////////////////////////////////////// if (cbox == focuserCombo) Options::setDefaultFocusFocuser(cbox->currentText()); else if (cbox == CCDCaptureCombo) Options::setDefaultFocusCCD(cbox->currentText()); else if (cbox == binningCombo) { activeBin = cbox->currentIndex() + 1; Options::setFocusXBin(activeBin); } else if (cbox == FilterDevicesCombo) Options::setDefaultFocusFilterWheel(cbox->currentText()); // Filter Effects already taken care of in filterChangeWarning /////////////////////////////////////////////////////////////////////////// /// Settings Group /////////////////////////////////////////////////////////////////////////// else if (cbox == focusAlgorithmCombo) Options::setFocusAlgorithm(cbox->currentIndex()); else if (cbox == focusDetectionCombo) Options::setFocusDetection(cbox->currentIndex()); } } void Focus::loadSettings() { /////////////////////////////////////////////////////////////////////////// /// Focuser Group /////////////////////////////////////////////////////////////////////////// // Focus settle time FocusSettleTime->setValue(Options::focusSettleTime()); /////////////////////////////////////////////////////////////////////////// /// CCD & Filter Wheel Group /////////////////////////////////////////////////////////////////////////// // Default Exposure exposureIN->setValue(Options::focusExposure()); // Binning activeBin = Options::focusXBin(); binningCombo->setCurrentIndex(activeBin - 1); // Gain gainIN->setValue(Options::focusGain()); /////////////////////////////////////////////////////////////////////////// /// Settings Group /////////////////////////////////////////////////////////////////////////// // Auto Star? useAutoStar->setChecked(Options::focusAutoStarEnabled()); // Subframe? useSubFrame->setChecked(Options::focusSubFrame()); // Dark frame? darkFrameCheck->setChecked(Options::useFocusDarkFrame()); // Use full field? useFullField->setChecked(Options::focusUseFullField()); // full field inner ring fullFieldInnerRing->setValue(Options::focusFullFieldInnerRadius()); // full field outer ring fullFieldOuterRing->setValue(Options::focusFullFieldOuterRadius()); // Suspend guiding? suspendGuideCheck->setChecked(Options::suspendGuiding()); // Guide Setting time GuideSettleTime->setValue(Options::guideSettleTime()); // Box Size focusBoxSize->setValue(Options::focusBoxSize()); // Max Travel if (Options::focusMaxTravel() > maxTravelIN->maximum()) maxTravelIN->setMaximum(Options::focusMaxTravel()); maxTravelIN->setValue(Options::focusMaxTravel()); // Step stepIN->setValue(Options::focusTicks()); // Single Max Step maxSingleStepIN->setValue(Options::focusMaxSingleStep()); // Tolerance toleranceIN->setValue(Options::focusTolerance()); // Threshold spin thresholdSpin->setValue(Options::focusThreshold()); // Focus Algorithm focusAlgorithm = static_cast(Options::focusAlgorithm()); focusAlgorithmCombo->setCurrentIndex(focusAlgorithm); // Frames Count focusFramesSpin->setValue(Options::focusFramesCount()); // Focus Detection focusDetection = static_cast(Options::focusDetection()); thresholdSpin->setEnabled(focusDetection == ALGORITHM_THRESHOLD); focusDetectionCombo->setCurrentIndex(focusDetection); } void Focus::initSettingsConnections() { /////////////////////////////////////////////////////////////////////////// /// Focuser Group /////////////////////////////////////////////////////////////////////////// connect(focuserCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::syncSettings); connect(FocusSettleTime, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); /////////////////////////////////////////////////////////////////////////// /// CCD & Filter Wheel Group /////////////////////////////////////////////////////////////////////////// connect(CCDCaptureCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::syncSettings); connect(binningCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::syncSettings); connect(gainIN, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(FilterDevicesCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::syncSettings); connect(FilterPosCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::syncSettings); /////////////////////////////////////////////////////////////////////////// /// Settings Group /////////////////////////////////////////////////////////////////////////// connect(useAutoStar, &QCheckBox::toggled, this, &Ekos::Focus::syncSettings); connect(useSubFrame, &QCheckBox::toggled, this, &Ekos::Focus::syncSettings); connect(darkFrameCheck, &QCheckBox::toggled, this, &Ekos::Focus::syncSettings); connect(useFullField, &QCheckBox::toggled, this, &Ekos::Focus::syncSettings); connect(fullFieldInnerRing, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(fullFieldOuterRing, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(suspendGuideCheck, &QCheckBox::toggled, this, &Ekos::Focus::syncSettings); connect(GuideSettleTime, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(focusBoxSize, static_cast(&QSpinBox::valueChanged), this, &Focus::syncSettings); connect(maxTravelIN, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(stepIN, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(maxSingleStepIN, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(toleranceIN, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(thresholdSpin, &QDoubleSpinBox::editingFinished, this, &Focus::syncSettings); connect(focusAlgorithmCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::syncSettings); connect(focusFramesSpin, static_cast(&QSpinBox::valueChanged), this, &Focus::syncSettings); connect(focusDetectionCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::syncSettings); } void Focus::initPlots() { connect(clearDataB, &QPushButton::clicked, this, &Ekos::Focus::clearDataPoints); profileDialog = new QDialog(this); profileDialog->setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); QVBoxLayout *profileLayout = new QVBoxLayout(profileDialog); profileDialog->setWindowTitle(i18n("Relative Profile")); profilePlot = new QCustomPlot(profileDialog); profilePlot->setBackground(QBrush(Qt::black)); profilePlot->xAxis->setBasePen(QPen(Qt::white, 1)); profilePlot->yAxis->setBasePen(QPen(Qt::white, 1)); profilePlot->xAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); profilePlot->yAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); profilePlot->xAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); profilePlot->yAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); profilePlot->xAxis->grid()->setZeroLinePen(Qt::NoPen); profilePlot->yAxis->grid()->setZeroLinePen(Qt::NoPen); profilePlot->xAxis->setBasePen(QPen(Qt::white, 1)); profilePlot->yAxis->setBasePen(QPen(Qt::white, 1)); profilePlot->xAxis->setTickPen(QPen(Qt::white, 1)); profilePlot->yAxis->setTickPen(QPen(Qt::white, 1)); profilePlot->xAxis->setSubTickPen(QPen(Qt::white, 1)); profilePlot->yAxis->setSubTickPen(QPen(Qt::white, 1)); profilePlot->xAxis->setTickLabelColor(Qt::white); profilePlot->yAxis->setTickLabelColor(Qt::white); profilePlot->xAxis->setLabelColor(Qt::white); profilePlot->yAxis->setLabelColor(Qt::white); profileLayout->addWidget(profilePlot); profileDialog->setLayout(profileLayout); profileDialog->resize(400, 300); connect(relativeProfileB, &QPushButton::clicked, profileDialog, &QDialog::show); currentGaus = profilePlot->addGraph(); currentGaus->setLineStyle(QCPGraph::lsLine); currentGaus->setPen(QPen(Qt::red, 2)); lastGaus = profilePlot->addGraph(); lastGaus->setLineStyle(QCPGraph::lsLine); QPen pen(Qt::darkGreen); pen.setStyle(Qt::DashLine); pen.setWidth(2); lastGaus->setPen(pen); HFRPlot->setBackground(QBrush(Qt::black)); HFRPlot->xAxis->setBasePen(QPen(Qt::white, 1)); HFRPlot->yAxis->setBasePen(QPen(Qt::white, 1)); HFRPlot->xAxis->setTickPen(QPen(Qt::white, 1)); HFRPlot->yAxis->setTickPen(QPen(Qt::white, 1)); HFRPlot->xAxis->setSubTickPen(QPen(Qt::white, 1)); HFRPlot->yAxis->setSubTickPen(QPen(Qt::white, 1)); HFRPlot->xAxis->setTickLabelColor(Qt::white); HFRPlot->yAxis->setTickLabelColor(Qt::white); HFRPlot->xAxis->setLabelColor(Qt::white); HFRPlot->yAxis->setLabelColor(Qt::white); HFRPlot->xAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); HFRPlot->yAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); HFRPlot->xAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); HFRPlot->yAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); HFRPlot->xAxis->grid()->setZeroLinePen(Qt::NoPen); HFRPlot->yAxis->grid()->setZeroLinePen(Qt::NoPen); HFRPlot->yAxis->setLabel(i18n("HFR")); HFRPlot->setInteractions(QCP::iRangeZoom); HFRPlot->setInteraction(QCP::iRangeDrag, true); polynomialGraph = HFRPlot->addGraph(); polynomialGraph->setLineStyle(QCPGraph::lsLine); polynomialGraph->setPen(QPen(QColor(140, 140, 140), 2, Qt::DotLine)); polynomialGraph->setScatterStyle(QCPScatterStyle::ssNone); connect(HFRPlot->xAxis, static_cast(&QCPAxis::rangeChanged), this, [this]() { drawHFRIndeces(); if (polynomialGraphIsShown) { if (focusAlgorithm == FOCUS_POLYNOMIAL) graphPolynomialFunction(); } }); connect(HFRPlot, &QCustomPlot::mouseMove, this, [this](QMouseEvent * event) { double key = HFRPlot->xAxis->pixelToCoord(event->localPos().x()); if (HFRPlot->xAxis->range().contains(key)) { QCPGraph *graph = qobject_cast(HFRPlot->plottableAt(event->pos(), false)); if (graph) { if(graph == v_graph) { int positionKey = v_graph->findBegin(key); double focusPosition = v_graph->dataMainKey(positionKey); double halfFluxRadius = v_graph->dataMainValue(positionKey); QToolTip::showText( event->globalPos(), i18nc("HFR graphics tooltip; %1 is the Focus Position; %2 is the Half Flux Radius;", "" "" "" "
POS: %1
HFR: %2
", QString::number(focusPosition, 'f', 0), QString::number(halfFluxRadius, 'f', 2))); } } } }); focusPoint = HFRPlot->addGraph(); focusPoint->setLineStyle(QCPGraph::lsImpulse); focusPoint->setPen(QPen(QColor(140, 140, 140), 2, Qt::SolidLine)); focusPoint->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, Qt::white, Qt::yellow, 10)); v_graph = HFRPlot->addGraph(); v_graph->setLineStyle(QCPGraph::lsNone); v_graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, Qt::white, Qt::white, 14)); } void Focus::initConnections() { // How long do we wait until the user select a star? waitStarSelectTimer.setInterval(AUTO_STAR_TIMEOUT); connect(&waitStarSelectTimer, &QTimer::timeout, this, &Ekos::Focus::checkAutoStarTimeout); connect(liveVideoB, &QPushButton::clicked, this, &Ekos::Focus::toggleVideo); // Show FITS Image in a new window showFITSViewerB->setIcon(QIcon::fromTheme("kstars_fitsviewer")); showFITSViewerB->setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(showFITSViewerB, &QPushButton::clicked, this, &Ekos::Focus::showFITSViewer); // Toggle FITS View to full screen toggleFullScreenB->setIcon(QIcon::fromTheme("view-fullscreen")); toggleFullScreenB->setShortcut(Qt::Key_F4); toggleFullScreenB->setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(toggleFullScreenB, &QPushButton::clicked, this, &Ekos::Focus::toggleFocusingWidgetFullScreen); // How long do we wait until an exposure times out and needs a retry? captureTimeout.setSingleShot(true); connect(&captureTimeout, &QTimer::timeout, this, &Ekos::Focus::processCaptureTimeout); // Start/Stop focus connect(startFocusB, &QPushButton::clicked, this, &Ekos::Focus::start); connect(stopFocusB, &QPushButton::clicked, this, &Ekos::Focus::checkStopFocus); // Focus IN/OUT connect(focusOutB, &QPushButton::clicked, [&]() { focusOut(); }); connect(focusInB, &QPushButton::clicked, [&]() { focusIn(); }); // Capture a single frame connect(captureB, &QPushButton::clicked, this, &Ekos::Focus::capture); // Start continuous capture connect(startLoopB, &QPushButton::clicked, this, &Ekos::Focus::startFraming); // Use a subframe when capturing connect(useSubFrame, &QCheckBox::toggled, this, &Ekos::Focus::toggleSubframe); // Reset frame dimensions to default connect(resetFrameB, &QPushButton::clicked, this, &Ekos::Focus::resetFrame); // Sync setting if full field setting is toggled. connect(useFullField, &QCheckBox::toggled, [&](bool toggled) { fullFieldInnerRing->setEnabled(toggled); fullFieldOuterRing->setEnabled(toggled); if (toggled) { useSubFrame->setChecked(false); useAutoStar->setChecked(false); } else { // Disable the overlay focusView->setStarFilterRange(0, 1); } }); // Sync settings if the CCD selection is updated. connect(CCDCaptureCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::checkCCD); // Sync settings if the Focuser selection is updated. connect(focuserCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::checkFocuser); // Sync settings if the filter selection is updated. connect(FilterDevicesCombo, static_cast(&QComboBox::activated), this, &Ekos::Focus::checkFilter); // Set focuser absolute position connect(startGotoB, &QPushButton::clicked, this, &Ekos::Focus::setAbsoluteFocusTicks); connect(stopGotoB, &QPushButton::clicked, [this]() { if (currentFocuser) currentFocuser->stop(); }); // Update the focuser box size used to enclose a star connect(focusBoxSize, static_cast(&QSpinBox::valueChanged), this, &Ekos::Focus::updateBoxSize); // Update the focuser star detection if the detection algorithm selection changes. connect(focusDetectionCombo, static_cast(&QComboBox::activated), this, [&](int index) { focusDetection = static_cast(index); thresholdSpin->setEnabled(focusDetection == ALGORITHM_THRESHOLD); }); // Update the focuser solution algorithm if the selection changes. connect(focusAlgorithmCombo, static_cast(&QComboBox::activated), this, [&](int index) { focusAlgorithm = static_cast(index); }); // Reset star center on auto star check toggle connect(useAutoStar, &QCheckBox::toggled, this, [&](bool enabled) { if (enabled) { starCenter = QVector3D(); starSelected = false; focusView->setTrackingBox(QRect()); } }); } void Focus::initView() { focusView = new FITSView(focusingWidget, FITS_FOCUS); focusView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); focusView->setBaseSize(focusingWidget->size()); focusView->createFloatingToolBar(); QVBoxLayout *vlayout = new QVBoxLayout(); vlayout->addWidget(focusView); focusingWidget->setLayout(vlayout); connect(focusView, &FITSView::trackingStarSelected, this, &Ekos::Focus::focusStarSelected, Qt::UniqueConnection); focusView->setStarsEnabled(true); focusView->setStarsHFREnabled(true); } } diff --git a/kstars/ekos/focus/focus.ui b/kstars/ekos/focus/focus.ui index 06f999941..3eda70149 100644 --- a/kstars/ekos/focus/focus.ui +++ b/kstars/ekos/focus/focus.ui @@ -1,1712 +1,1712 @@ Focus 0 0 790 468 3 3 3 3 3 Qt::Horizontal 1 0 0 Focuser 3 6 3 6 3 0 32 false 32 32 Focus Out .. 28 28 false 0 32 Stop Auto Focus process Stop false 0 0 0 32 Desired absolute focus position Focuser: false 32 32 Go to an absolute focus position .. 28 28 false 0 32 Current absolute focuser position QLineEdit[readOnly="true"] { color: gray } true Steps: false 32 32 Stop focuser motion .. 28 28 false 0 32 Start Auto Focus process Auto Focus true true false 29 29 32 32 Focus In .. 28 28 Start: false 32 32 32 32 Start framing .. 28 28 false 32 32 Capture image .. 28 28 0 0 CCD && Filter Wheel 3 3 3 3 3 false 0 32 1 0 32 3 0.001000000000000 300.000000000000000 0.100000000000000 0.500000000000000 32 32 32 32 Toggle Full Screen 28 28 32 32 32 32 Show in FITS Viewer 28 28 false Filter Wheel FW: Bin: false Number of images to capture Filter: false 0 32 -- Exposure time in seconds Exp: 0 32 Reset focus subframe to full capture Reset .. CCD: 1 false 0 32 Exposure time in seconds Gain: 0 32 false ISO: 1 false 0 32 false 32 32 32 32 Filter Settings .. 28 28 1 0 32 false 32 32 32 32 Live Video .. 28 28 true QTabWidget::Rounded 0 Settings 0 3 3 3 3 QLayout::SetDefaultConstraint 6 3 Box: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 0 0 Automatically select the best focus star from the image Auto Select Star 0 0 <html><head/><body><p>Measure average HFR from all stars combined in a full frame. This method defaults to the Centroid detection, but can use SEP detection too. Its performance decreases as the number of stars increases.</p></body></html> Full Field 0 0 <html><body><p>During full field focusing, stars which are inside this percentage of the frame are filtered out of HFR calculation (default 0%). Detection algorithms may also have an inherent filter.</p></body></html> % 1 10.000000000000000 0 0 Suspend Guiding while autofocus in progress Suspend Guiding 0 0 Use dark frames from the library. Dark Frame Settle: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Annulus: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 0 0 Wait this many seconds before resuming guiding. s 60.000000000000000 0 0 <html><body><p>During full field focusing, stars which are outside this percentage of the frame are filtered out of HFR calculation (default 100%). Detection algorithms may also have an inherent filter.</p></body></html> % 1 10.000000000000000 100.000000000000000 0 0 Subframe around the focus star during the autofocus procedure Sub Frame true 0 0 <html><body><p>Size of the subframe to constrain capture to, in pixels.</p></body></html> px 16 256 16 32 Qt::Vertical 20 40 Process 3 3 3 3 3 3 Tolerance: 0 0 Decrease value to narrow optimal focus point solution radius. Increase to expand solution radius % 0.010000000000000 20.000000000000000 0.100000000000000 1.000000000000000 Effect: 0 0 - <html><head/><body><p>Select star detection algorithm</p></body></html> + <html><head/><body><p>Star detection method:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">SEP:</span> Source Extractor and Photometry, an efficient source detection method based on Source Extractor (Bertin and Arnouts 1996; Bertin 2016). See <a href="https://joss.theoj.org/papers/10.21105/joss.00058.pdf"><span style=" text-decoration: underline; color:#0000ff;">SEP: Source Extractor as a library</span></a> in the Journal of Open Source Software.</li><li style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Centroid</span>: a source detection based on estimating star mass around signal peaks.</li><li style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Gradient</span>: a single source detection based on the Sobel filter. Initial or full-field analysis will use SEP instead of this method.</li><li style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Threshold</span>: a single source detection based on pixel values. Initial or full-field analysis will use SEP instead of this method.</li></ul></body></html> Gradient Centroid Threshold SEP Threshold: false 0 0 <html><body><p>Increase to restrict the centroid to bright cores. Decrease to enclose fuzzy stars.</p></body></html> % 90.000000000000000 500.000000000000000 10.000000000000000 150.000000000000000 0 0 <html><head/><body><p>Select focus process algorithm:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Iterative</span>: Moves focuser by discreet steps initially decided by the step size. Once a curve slope is calculated, further step sizes are calculated to reach optimal solution. The algorithm stops when the measured HFR is within percentage tolerance of the minimum HFR recorded in the procedure.</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Polynomial</span>: Starts with iterative method. Upon crossing to the other side of the V-Curve, polynomial fitting coefficients along with possible minimum solution are calculated. This algorithm can be faster than purely iterative approach given a good data set.</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Linear</span>: Samples focus inward in a regular fashion, using 2 passes. The algorithm can be slow, but it is more resilient to backlash. Start with the focuser positioned near good focus. Set Initial Step Size and Max Travel for the desired sampling interval and range around start focus position. Tolerance should be around 5%.</li></ul></body></html> Iterative Polynomial Linear Algorithm: Detection: 0 0 Apply filter to image after capture to enhance it -- Average over: 0 0 <html><body><p>Number of frames to capture in order to average the HFR value at the current focuser position.</p></body></html> frames 1 Qt::Vertical 20 40 Mechanics 3 3 3 3 3 3 0 0 <html><head/><body><p>Wait for this many seconds after moving the focuser before capturing the next image during Auto Focus.</p></body></html> s 3 30.000000000000000 Max Travel: 0 0 <b>Initial</b> step size in ticks to cause a noticeable change in HFR value. For timer based focuser, it is the initial time in milliseconds to move the focuser inward or outward 1 50000 10 250 Initial Step size: Settle: 0 0 <html><head/><body><p>Maximum travel in steps before the autofocus process aborts</p></body></html> 0 10.000000000000000 100000.000000000000000 1000.000000000000000 10000.000000000000000 Max Step size: <html><head/><body><p>The maximum single step size the algorithm is allowed to command as it searches for the critical focus zone. The calculated step size would be limited to this maximum value.</p></body></html> 10 100000 100000 Backlash: 0 0 <html><body><p>For backlash-aware focuser, the amount of backlash to apply when reversing movement direction.</p></body></html> Qt::Vertical 20 40 0 0 Qt::Vertical 0 0 320 240 40 30 0 0 0 200 V-Curve 5 3 3 3 3 0 0 200 100 1 HFR: 0 0 <html><body><p>HFR value in pixels consolidated at the current focuser position.</p></body></html> 32767 true true Qt::Horizontal 40 20 Stars: <html><body><p>Number of stars used for HFR computation at the current focuser position.</p></body></html> true Qt::Horizontal QSizePolicy::Expanding 40 13 Relative Profile... Clear Data QCustomPlot QWidget
auxiliary/qcustomplot.h
1
focuserCombo focusInB focusOutB absTicksLabel absTicksSpin startGotoB stopGotoB startFocusB stopFocusB captureB startLoopB CCDCaptureCombo liveVideoB exposureIN toggleFullScreenB showFITSViewerB binningCombo gainIN ISOCombo FilterDevicesCombo FilterPosCombo filterManagerB resetFrameB useAutoStar useFullField fullFieldInnerRing suspendGuideCheck GuideSettleTime maxTravelIN HFROut relativeProfileB clearDataB
diff --git a/kstars/fitsviewer/fpackutil.c b/kstars/fitsviewer/fpackutil.c index 965f5261b..6a66c5df4 100644 --- a/kstars/fitsviewer/fpackutil.c +++ b/kstars/fitsviewer/fpackutil.c @@ -1,2389 +1,2389 @@ /* FPACK utility routines R. Seaman, NOAO & W. Pence, NASA/GSFC */ #include #include #include #include /* #include "bzlib.h" only for experimental purposes */ #if defined(unix) || defined(__unix__) || defined(__unix) #include #endif #include #include #include #include "fpack.h" /* these filename buffer are used to delete temporary files */ /* in case the program is aborted */ char tempfilename[SZ_STR]; char tempfilename2[SZ_STR]; char tempfilename3[SZ_STR]; /* nearest integer function */ # define NINT(x) ((x >= 0.) ? (int) (x + 0.5) : (int) (x - 0.5)) # define NSHRT(x) ((x >= 0.) ? (short) (x + 0.5) : (short) (x - 0.5)) /* define variables for measuring elapsed time */ clock_t scpu, ecpu; long startsec; /* start of elapsed time interval */ int startmilli; /* start of elapsed time interval */ /* CLOCKS_PER_SEC should be defined by most compilers */ #if defined(CLOCKS_PER_SEC) #define CLOCKTICKS CLOCKS_PER_SEC #else /* on SUN OS machine, CLOCKS_PER_SEC is not defined, so set its value */ #define CLOCKTICKS 1000000 #endif FILE *outreport; /* dimension of central image area to be sampled for test statistics */ int XSAMPLE = 4100; int YSAMPLE = 4100; #define UNUSED(x) (void)(x) #define fp_tmpnam(suffix, rootname, tmpnam) _fp_tmpnam((char *)suffix, (char *)rootname, (char *)tmpnam) /*--------------------------------------------------------------------------*/ int fp_noop (void) { fp_msg ("Input and output files are unchanged.\n"); return(0); } /*--------------------------------------------------------------------------*/ void fp_abort_output (fitsfile *infptr, fitsfile *outfptr, int stat) { int status = 0, hdunum; char msg[SZ_STR]; if (infptr) { fits_file_name(infptr, tempfilename, &status); fits_get_hdu_num(infptr, &hdunum); fits_close_file (infptr, &status); snprintf(msg, SZ_STR,"Error processing file: %s\n", tempfilename); fp_msg (msg); snprintf(msg, SZ_STR," in HDU number %d\n", hdunum); fp_msg (msg); } else { snprintf(msg, SZ_STR,"Error: Unable to process input file\n"); fp_msg(msg); } fits_report_error (stderr, stat); if (outfptr) { fits_delete_file(outfptr, &status); fp_msg ("Input file is unchanged.\n"); } } /*--------------------------------------------------------------------------*/ int fp_version (void) { float version; char cfitsioversion[40]; fp_msg (FPACK_VERSION); fits_get_version(&version); snprintf(cfitsioversion, 40," CFITSIO version %5.3f", version); fp_msg(cfitsioversion); fp_msg ("\n"); return(0); } /*--------------------------------------------------------------------------*/ int fp_access (char *filename) { /* test if a file exists */ FILE *diskfile; diskfile = fopen(filename, "r"); if (diskfile) { fclose(diskfile); return(0); } else { return(-1); } } /*--------------------------------------------------------------------------*/ int _fp_tmpnam(char *suffix, char *rootname, char *tmpnam) { /* create temporary file name */ int maxtry = 30, ii; if (strlen(suffix) + strlen(rootname) > SZ_STR-5) { fp_msg ("Error: filename is too long to create tempory file\n"); exit (-1); } strcpy (tmpnam, rootname); /* start with rootname */ strcat(tmpnam, suffix); /* append the suffix */ maxtry = SZ_STR - strlen(tmpnam) - 1; for (ii = 0; ii < maxtry; ii++) { if (fp_access(tmpnam)) break; /* good, the file does not exist */ if (strlen(tmpnam) > SZ_STR-2) { fp_msg ("\nCould not create temporary file name:\n"); fp_msg (tmpnam); fp_msg ("\n"); exit (-1); } strcat(tmpnam, "x"); /* append an x to the name, and try again */ } if (ii == maxtry) { fp_msg ("\nCould not create temporary file name:\n"); fp_msg (tmpnam); fp_msg ("\n"); exit (-1); } return(0); } /*--------------------------------------------------------------------------*/ int fp_init (fpstate *fpptr) { int ii; fpptr->comptype = RICE_1; fpptr->quantize_level = DEF_QLEVEL; fpptr->no_dither = 0; fpptr->dither_method = 1; fpptr->dither_offset = 0; fpptr->int_to_float = 0; /* thresholds when using the -i2f flag */ fpptr->n3ratio = 2.0; /* minimum ratio of image noise sigma / q */ fpptr->n3min = 6.; /* minimum noise sigma. */ fpptr->scale = DEF_HCOMP_SCALE; fpptr->smooth = DEF_HCOMP_SMOOTH; fpptr->rescale_noise = DEF_RESCALE_NOISE; fpptr->ntile[0] = (long) -1; /* -1 means extent of axis */ for (ii=1; ii < MAX_COMPRESS_DIM; ii++) fpptr->ntile[ii] = (long) 1; fpptr->to_stdout = 0; fpptr->listonly = 0; fpptr->clobber = 0; fpptr->delete_input = 0; fpptr->do_not_prompt = 0; fpptr->do_checksums = 1; fpptr->do_gzip_file = 0; fpptr->do_tables = 0; /* this is intended for testing purposes */ fpptr->do_images = 1; /* can be turned off with -tableonly switch */ fpptr->test_all = 0; fpptr->verbose = 0; fpptr->prefix[0] = 0; fpptr->extname[0] = 0; fpptr->delete_suffix = 0; fpptr->outfile[0] = 0; fpptr->firstfile = 1; /* magic number for initialization check, boolean for preflight */ fpptr->initialized = FP_INIT_MAGIC; fpptr->preflight_checked = 0; return(0); } /*--------------------------------------------------------------------------*/ int fp_list (int argc, char *argv[], fpstate fpvar) { fitsfile *infptr; char infits[SZ_STR], msg[SZ_STR]; int hdunum, iarg, stat=0; LONGLONG sizell; if (fpvar.initialized != FP_INIT_MAGIC) { fp_msg ("Error: internal initialization error\n"); exit (-1); } for (iarg=fpvar.firstfile; iarg < argc; iarg++) { strncpy (infits, argv[iarg], SZ_STR-1); infits[SZ_STR-1]=0; if (strchr (infits, '[') || strchr (infits, ']')) { fp_msg ("Error: section/extension notation not supported: "); fp_msg (infits); fp_msg ("\n"); exit (-1); } if (fp_access (infits) != 0) { fp_msg ("Error: can't find or read input file "); fp_msg (infits); fp_msg ("\n"); fp_noop (); exit (-1); } fits_open_file (&infptr, infits, READONLY, &stat); if (stat) { fits_report_error (stderr, stat); exit (stat); } /* move to the end of file, to get the total size in bytes */ fits_get_num_hdus (infptr, &hdunum, &stat); fits_movabs_hdu (infptr, hdunum, NULL, &stat); fits_get_hduaddrll(infptr, NULL, NULL, &sizell, &stat); if (stat) { fp_abort_output(infptr, NULL, stat); } snprintf (msg, SZ_STR,"# %s (", infits); fp_msg (msg); #if defined(_MSC_VER) /* Microsoft Visual C++ 6.0 uses '%I64d' syntax for 8-byte integers */ snprintf(msg, SZ_STR,"%I64d bytes)\n", sizell); fp_msg (msg); #elif (USE_LL_SUFFIX == 1) snprintf(msg, SZ_STR,"%lld bytes)\n", sizell); fp_msg (msg); #else snprintf(msg, SZ_STR,"%ld bytes)\n", sizell); fp_msg (msg); #endif fp_info_hdu (infptr); fits_close_file (infptr, &stat); if (stat) { fits_report_error (stderr, stat); exit (stat); } } return(0); } /*--------------------------------------------------------------------------*/ int fp_info_hdu (fitsfile *infptr) { long naxes[9] = {1, 1, 1, 1, 1, 1, 1, 1, 1}; char msg[SZ_STR], val[SZ_CARD], com[SZ_CARD]; int naxis=0, hdutype, bitpix, hdupos, stat=0, ii; unsigned long datasum, hdusum; fits_movabs_hdu (infptr, 1, NULL, &stat); if (stat) { fp_abort_output(infptr, NULL, stat); } for (hdupos=1; ! stat; hdupos++) { fits_get_hdu_type (infptr, &hdutype, &stat); if (stat) { fp_abort_output(infptr, NULL, stat); } /* fits_get_hdu_type calls unknown extensions "IMAGE_HDU" * so consult XTENSION keyword itself */ fits_read_keyword (infptr, "XTENSION", val, com, &stat); if (stat == KEY_NO_EXIST) { /* in primary HDU which by definition is an "image" */ stat=0; /* clear for later error handling */ } else if (stat) { fp_abort_output(infptr, NULL, stat); } else if (hdutype == IMAGE_HDU) { /* that is, if XTENSION != "IMAGE" AND != "BINTABLE" */ if (strncmp (val+1, "IMAGE", 5) && strncmp (val+1, "BINTABLE", 5)) { /* assign something other than any of these */ hdutype = IMAGE_HDU + ASCII_TBL + BINARY_TBL; } } fits_get_chksum(infptr, &datasum, &hdusum, &stat); if (hdutype == IMAGE_HDU) { snprintf (msg, SZ_STR," %d IMAGE", hdupos); fp_msg (msg); snprintf (msg, SZ_STR," SUMS=%lu/%lu", (unsigned long) (~((int) hdusum)), datasum); fp_msg (msg); fits_get_img_param (infptr, 9, &bitpix, &naxis, naxes, &stat); snprintf (msg, SZ_STR," BITPIX=%d", bitpix); fp_msg (msg); if (naxis == 0) { snprintf (msg, SZ_STR," [no_pixels]"); fp_msg (msg); } else if (naxis == 1) { snprintf (msg, SZ_STR," [%ld]", naxes[1]); fp_msg (msg); } else { snprintf (msg, SZ_STR," [%ld", naxes[0]); fp_msg (msg); for (ii=1; ii < naxis; ii++) { snprintf (msg, SZ_STR,"x%ld", naxes[ii]); fp_msg (msg); } fp_msg ("]"); } if (fits_is_compressed_image (infptr, &stat)) { fits_read_keyword (infptr, "ZCMPTYPE", val, com, &stat); /* allow for quote in keyword value */ if (! strncmp (val+1, "RICE_1", 6)) fp_msg (" tiled_rice\n"); else if (! strncmp (val+1, "GZIP_1", 6)) fp_msg (" tiled_gzip_1\n"); else if (! strncmp (val+1, "GZIP_2", 6)) fp_msg (" tiled_gzip_2\n"); else if (! strncmp (val+1, "PLIO_1", 6)) fp_msg (" tiled_plio\n"); else if (! strncmp (val+1, "HCOMPRESS_1", 11)) fp_msg (" tiled_hcompress\n"); else fp_msg (" unknown\n"); } else fp_msg (" not_tiled\n"); } else if (hdutype == ASCII_TBL) { snprintf (msg, SZ_STR," %d ASCII_TBL", hdupos); fp_msg (msg); snprintf (msg, SZ_STR," SUMS=%lu/%lu\n", (unsigned long) (~((int) hdusum)), datasum); fp_msg (msg); } else if (hdutype == BINARY_TBL) { snprintf (msg, SZ_STR," %d BINARY_TBL", hdupos); fp_msg (msg); snprintf (msg, SZ_STR," SUMS=%lu/%lu\n", (unsigned long) (~((int) hdusum)), datasum); fp_msg (msg); } else { snprintf (msg, SZ_STR," %d OTHER", hdupos); fp_msg (msg); snprintf (msg, SZ_STR," SUMS=%lu/%lu", (unsigned long) (~((int) hdusum)), datasum); fp_msg (msg); snprintf (msg, SZ_STR," %s\n", val); fp_msg (msg); } fits_movrel_hdu (infptr, 1, NULL, &stat); } return(0); } /*--------------------------------------------------------------------------*/ int fp_preflight (int argc, char *argv[], int unpack, fpstate *fpptr) { char infits[SZ_STR], outfits[SZ_STR]; int iarg, namelen, nfiles = 0; if (fpptr->initialized != FP_INIT_MAGIC) { fp_msg ("Error: internal initialization error\n"); exit (-1); } for (iarg=fpptr->firstfile; iarg < argc; iarg++) { outfits[0] = '\0'; if (strlen(argv[iarg]) > SZ_STR - 4) { /* allow for .fz or .gz suffix */ fp_msg ("Error: input file name\n "); fp_msg (argv[iarg]); fp_msg ("\n is too long\n"); fp_noop (); exit (-1); } strncpy (infits, argv[iarg], SZ_STR); if (infits[0] == '-' && infits[1] != '\0') { /* don't interpret this as intending to read input file from stdin */ fp_msg ("Error: invalid input file name\n "); fp_msg (argv[iarg]); fp_msg ("\n"); fp_noop (); exit (-1); } if (strchr (infits, '[') || strchr (infits, ']')) { fp_msg ("Error: section/extension notation not supported: "); fp_msg (infits); fp_msg ("\n"); fp_noop (); exit (-1); } if (unpack) { /* ********** This section applies to funpack ************ */ /* check that input file exists */ if (infits[0] != '-') { /* if not reading from stdin stream */ if (fp_access (infits) != 0) { /* if not, then check if */ strcat(infits, ".fz"); /* a .fz version exsits */ if (fp_access (infits) != 0) { namelen = strlen(infits); infits[namelen - 3] = '\0'; /* remove the .fz suffix */ fp_msg ("Error: can't find or read input file "); fp_msg (infits); fp_msg ("\n"); fp_noop (); exit (-1); } } else { /* make sure a .fz version of the same file doesn't exist */ namelen = strlen(infits); strcat(infits, ".fz"); if (fp_access (infits) == 0) { infits[namelen] = '\0'; /* remove the .fz suffix */ fp_msg ("Error: ambiguous input file name. Which file should be unpacked?:\n "); fp_msg (infits); fp_msg ("\n "); fp_msg (infits); fp_msg (".fz\n"); fp_noop (); exit (-1); } else { infits[namelen] = '\0'; /* remove the .fz suffix */ } } } /* if writing to stdout, then we are all done */ if (fpptr->to_stdout) { continue; } if (fpptr->outfile[0]) { /* user specified output file name */ nfiles++; if (nfiles > 1) { fp_msg ("Error: cannot use same output file name for multiple files:\n "); fp_msg (fpptr->outfile); fp_msg ("\n"); fp_noop (); exit (-1); } /* check that output file doesn't exist */ if (fp_access (fpptr->outfile) == 0) { fp_msg ("Error: output file already exists:\n "); fp_msg (fpptr->outfile); fp_msg ("\n "); fp_noop (); exit (-1); } continue; } /* construct output file name to test */ if (fpptr->prefix[0]) { if (strlen(fpptr->prefix) + strlen(infits) > SZ_STR - 1) { fp_msg ("Error: output file name for\n "); fp_msg (infits); fp_msg ("\n is too long with the prefix\n"); fp_noop (); exit (-1); } strcat(outfits,fpptr->prefix); } /* construct output file name */ if (infits[0] == '-') { strcpy(outfits, "output.fits"); } else { strcpy(outfits, infits); } /* remove .gz suffix, if present (output is not gzipped) */ namelen = strlen(outfits); if ( !strcmp(".gz", outfits + namelen - 3) ) { outfits[namelen - 3] = '\0'; } /* check for .fz suffix that is sometimes required */ /* and remove it if present */ if (infits[0] != '-') { /* if not reading from stdin stream */ namelen = strlen(outfits); if ( !strcmp(".fz", outfits + namelen - 3) ) { /* suffix is present */ outfits[namelen - 3] = '\0'; } else if (fpptr->delete_suffix) { /* required suffix is missing */ fp_msg ("Error: input compressed file "); fp_msg (infits); fp_msg ("\n does not have the default .fz suffix.\n"); fp_noop (); exit (-1); } } /* if infits != outfits, make sure outfits doesn't already exist */ if (strcmp(infits, outfits)) { if (fp_access (outfits) == 0) { fp_msg ("Error: output file already exists:\n "); fp_msg (outfits); fp_msg ("\n "); fp_noop (); exit (-1); } } /* if gzipping the output, make sure .gz file doesn't exist */ if (fpptr->do_gzip_file) { if (strlen(outfits)+3 > SZ_STR-1) { fp_msg ("Error: output file name too long:\n "); fp_msg (outfits); fp_msg ("\n "); fp_noop (); exit (-1); } strcat(outfits, ".gz"); if (fp_access (outfits) == 0) { fp_msg ("Error: output file already exists:\n "); fp_msg (outfits); fp_msg ("\n "); fp_noop (); exit (-1); } namelen = strlen(outfits); outfits[namelen - 3] = '\0'; /* remove the .gz suffix again */ } } else { /* ********** This section applies to fpack ************ */ /* check that input file exists */ if (infits[0] != '-') { /* if not reading from stdin stream */ if (fp_access (infits) != 0) { /* if not, then check if */ if (strlen(infits)+3 > SZ_STR-1) { fp_msg ("Error: input file name too long:\n "); fp_msg (infits); fp_msg ("\n "); fp_noop (); exit (-1); } strcat(infits, ".gz"); /* a gzipped version exsits */ if (fp_access (infits) != 0) { namelen = strlen(infits); infits[namelen - 3] = '\0'; /* remove the .gz suffix */ fp_msg ("Error: can't find or read input file "); fp_msg (infits); fp_msg ("\n"); fp_noop (); exit (-1); } } } /* make sure the file to pack does not already have a .fz suffix */ namelen = strlen(infits); if ( !strcmp(".fz", infits + namelen - 3) ) { fp_msg ("Error: fpack input file already has '.fz' suffix\n" ); fp_msg (infits); fp_msg ("\n"); fp_noop (); exit (-1); } /* if writing to stdout, or just testing the files, then we are all done */ if (fpptr->to_stdout || fpptr->test_all) { continue; } /* construct output file name */ if (infits[0] == '-') { strcpy(outfits, "input.fits"); } else { strcpy(outfits, infits); } /* remove .gz suffix, if present (output is not gzipped) */ namelen = strlen(outfits); if ( !strcmp(".gz", outfits + namelen - 3) ) { outfits[namelen - 3] = '\0'; } /* remove .imh suffix (IRAF format image), and replace with .fits */ namelen = strlen(outfits); if ( !strcmp(".imh", outfits + namelen - 4) ) { outfits[namelen - 4] = '\0'; strcat(outfits, ".fits"); } /* If not clobbering the input file, add .fz suffix to output name */ if (! fpptr->clobber) strcat(outfits, ".fz"); /* if infits != outfits, make sure outfits doesn't already exist */ if (strcmp(infits, outfits)) { if (fp_access (outfits) == 0) { fp_msg ("Error: output file already exists:\n "); fp_msg (outfits); fp_msg ("\n "); fp_noop (); exit (-1); } } } /* end of fpack section */ } fpptr->preflight_checked++; return(0); } /*--------------------------------------------------------------------------*/ /* must run fp_preflight() before fp_loop() */ int fp_loop (int argc, char *argv[], int unpack, char *output_filename, fpstate fpvar) { char infits[SZ_STR], outfits[SZ_STR]; char temp[SZ_STR], answer[30]; char valchar[]="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz.#()+,-_@[]/^{}"; int ichar=0, outlen=0, iarg, islossless, namelen, iraf_infile = 0, status = 0, ifail; if (fpvar.initialized != FP_INIT_MAGIC) { fp_msg ("Error: internal initialization error\n"); exit (-1); } else if (! fpvar.preflight_checked) { fp_msg ("Error: internal preflight error\n"); exit (-1); } if (fpvar.test_all && fpvar.outfile[0]) { outreport = fopen(fpvar.outfile, "w"); fprintf(outreport," Filename Extension BITPIX NAXIS1 NAXIS2 Size N_nulls Minval Maxval Mean Sigm Noise1 Noise2 Noise3 Noise5 T_whole T_rowbyrow "); fprintf(outreport,"[Comp_ratio, Pack_cpu, Unpack_cpu, Lossless readtimes] (repeated for Rice, Hcompress, and GZIP)\n"); } tempfilename[0] = '\0'; tempfilename2[0] = '\0'; tempfilename3[0] = '\0'; /* set up signal handler to delete temporary file on abort */ #ifdef SIGINT if (signal(SIGINT, SIG_IGN) != SIG_IGN) { (void) signal(SIGINT, abort_fpack); } #endif #ifdef SIGTERM if (signal(SIGTERM, SIG_IGN) != SIG_IGN) { (void) signal(SIGTERM, abort_fpack); } #endif #ifdef SIGHUP if (signal(SIGHUP, SIG_IGN) != SIG_IGN) { (void) signal(SIGHUP, abort_fpack); } #endif for (iarg=fpvar.firstfile; iarg < argc; iarg++) { temp[0] = '\0'; outfits[0] = '\0'; islossless = 1; strncpy (infits, argv[iarg], SZ_STR - 1); infits[SZ_STR-1]=0; if (unpack) { /* ********** This section applies to funpack ************ */ /* find input file */ if (infits[0] != '-') { /* if not reading from stdin stream */ if (fp_access (infits) != 0) { /* if not, then */ strcat(infits, ".fz"); /* a .fz version must exsit */ } } if (fpvar.to_stdout) { strcpy(outfits, "-"); } else if (fpvar.outfile[0]) { /* user specified output file name */ strcpy(outfits, fpvar.outfile); } else { /* construct output file name */ if (fpvar.prefix[0]) { strcat(outfits,fpvar.prefix); } /* construct output file name */ if (infits[0] == '-') { strcpy(outfits, "output.fits"); } else { /*strcpy(outfits, infits);*/ strcpy(outfits, output_filename); } /* remove .gz suffix, if present (output is not gzipped) */ namelen = strlen(outfits); if ( !strcmp(".gz", outfits + namelen - 3) ) { outfits[namelen - 3] = '\0'; } /* check for .fz suffix that is sometimes required */ /* and remove it if present */ namelen = strlen(outfits); if ( !strcmp(".fz", outfits + namelen - 3) ) { /* suffix is present */ outfits[namelen - 3] = '\0'; } } } else { /* ********** This section applies to fpack ************ */ if (fpvar.to_stdout) { strcpy(outfits, "-"); } else if (! fpvar.test_all) { /* construct output file name */ if (infits[0] == '-') { strcpy(outfits, "input.fits"); } else { strcpy(outfits, output_filename); } /* remove .gz suffix, if present (output is not gzipped) */ namelen = strlen(outfits); if ( !strcmp(".gz", outfits + namelen - 3) ) { outfits[namelen - 3] = '\0'; } /* remove .imh suffix (IRAF format image), and replace with .fits */ namelen = strlen(outfits); if ( !strcmp(".imh", outfits + namelen - 4) ) { outfits[namelen - 4] = '\0'; strcat(outfits, ".fits"); iraf_infile = 1; /* this is an IRAF format input file */ /* change the output name to "NAME.fits.fz" */ } /* If not clobbering the input file, add .fz suffix to output name */ if (! fpvar.clobber) strcat(outfits, ".fz"); } } strncpy(temp, outfits, SZ_STR-1); temp[SZ_STR-1]=0; if (infits[0] != '-') { /* if not reading from stdin stream */ if (!strcmp(infits, outfits) ) { /* are input and output names the same? */ /* clobber the input file with the output file with the same name */ if (! fpvar.clobber) { fp_msg ("\nError: must use -F flag to clobber input file.\n"); exit (-1); } /* create temporary file name in the output directory (same as input directory)*/ fp_tmpnam("Tmp1", infits, outfits); strcpy(tempfilename, outfits); /* store temp file name, in case of abort */ } } /* *************** now do the real work ********************* */ if (fpvar.verbose && ! fpvar.to_stdout) printf("%s ", infits); if (fpvar.test_all) { /* compare all the algorithms */ /* create 2 temporary file names, in the CWD */ fp_tmpnam("Tmpfile1", "", tempfilename); fp_tmpnam("Tmpfile2", "", tempfilename2); fp_test (infits, tempfilename, tempfilename2, fpvar); remove(tempfilename); tempfilename[0] = '\0'; /* clear the temp file name */ remove(tempfilename2); tempfilename2[0] = '\0'; continue; } else if (unpack) { if (fpvar.to_stdout) { /* unpack the input file to the stdout stream */ fp_unpack (infits, outfits, fpvar); } else { /* unpack to temporary file, so other tasks can't open it until it is renamed */ /* create temporary file name, in the output directory */ fp_tmpnam("Tmp2", outfits, tempfilename2); /* unpack the input file to the temporary file */ fp_unpack (infits, tempfilename2, fpvar); /* rename the temporary file to it's real name */ ifail = rename(tempfilename2, outfits); if (ifail) { fp_msg("Failed to rename temporary file name:\n "); fp_msg(tempfilename2); fp_msg(" -> "); fp_msg(outfits); fp_msg("\n"); exit (-1); } else { tempfilename2[0] = '\0'; /* clear temporary file name */ } } } else { fp_pack (infits, outfits, fpvar, &islossless); } if (fpvar.to_stdout) { continue; } /* ********** clobber and/or delete files, if needed ************** */ if (!strcmp(infits, temp) && fpvar.clobber ) { if (!islossless && ! fpvar.do_not_prompt) { fp_msg ("\nFile "); fp_msg (infits); fp_msg ("\nwas compressed with a LOSSY method. Overwrite the\n"); fp_msg ("original file with the compressed version? (Y/N) "); if (fgets(answer, 29, stdin) && answer[0] != 'Y' && answer[0] != 'y') { fp_msg ("\noriginal file NOT overwritten!\n"); remove(outfits); continue; } } if (iraf_infile) { /* special case of deleting an IRAF format header and pixel file */ if (fits_delete_iraf_file(infits, &status)) { fp_msg("\nError deleting IRAF .imh and .pix files.\n"); fp_msg(infits); fp_msg ("\n"); exit (-1); } } #if defined(unix) || defined(__unix__) || defined(__unix) /* rename clobbers input on Unix platforms */ if (rename (outfits, temp) != 0) { fp_msg ("\nError renaming tmp file to "); fp_msg (temp); fp_msg ("\n"); exit (-1); } #else /* rename DOES NOT clobber existing files on Windows platforms */ /* so explicitly remove any existing file before renaming the file */ remove(temp); if (rename (outfits, temp) != 0) { fp_msg ("\nError renaming tmp file to "); fp_msg (temp); fp_msg ("\n"); exit (-1); } #endif tempfilename[0] = '\0'; /* clear temporary file name */ strcpy(outfits, temp); } else if (fpvar.clobber || fpvar.delete_input) { /* delete the input file */ if (!islossless && !fpvar.do_not_prompt) { /* user did not turn off delete prompt */ fp_msg ("\nFile "); fp_msg (infits); fp_msg ("\nwas compressed with a LOSSY method. \n"); fp_msg ("Delete the original file? (Y/N) "); if (fgets(answer, 29, stdin) && answer[0] != 'Y' && answer[0] != 'y') { /* user abort */ fp_msg ("\noriginal file NOT deleted!\n"); } else { if (iraf_infile) { /* special case of deleting an IRAF format header and pixel file */ if (fits_delete_iraf_file(infits, &status)) { fp_msg("\nError deleting IRAF .imh and .pix files.\n"); fp_msg(infits); fp_msg ("\n"); exit (-1); } } else if (remove(infits) != 0) { /* normal case of deleting input FITS file */ fp_msg ("\nError deleting input file "); fp_msg (infits); fp_msg ("\n"); exit (-1); } } } else { /* user said don't prompt, so just delete the input file */ if (iraf_infile) { /* special case of deleting an IRAF format header and pixel file */ if (fits_delete_iraf_file(infits, &status)) { fp_msg("\nError deleting IRAF .imh and .pix files.\n"); fp_msg(infits); fp_msg ("\n"); exit (-1); } } else if (remove(infits) != 0) { /* normal case of deleting input FITS file */ fp_msg ("\nError deleting input file "); fp_msg (infits); fp_msg ("\n"); exit (-1); } } } iraf_infile = 0; if (fpvar.do_gzip_file) { /* gzip the output file */ strcpy(temp, "gzip -1 "); outlen = strlen(outfits); if (outlen + 8 > SZ_STR-1) { fp_msg("\nError: Output file name is too long.\n"); exit(-1); } for (ichar=0; ichar < outlen; ++ichar) { if (!strchr(valchar, outfits[ichar])) { fp_msg("\n Error: Invalid characters in output file name.\n"); exit(-1); } } strcat(temp,outfits); int rc = system(temp); UNUSED(rc); strcat(outfits, ".gz"); /* only possibible with funpack */ } if (fpvar.verbose && ! fpvar.to_stdout) printf("-> %s\n", outfits); } if (fpvar.test_all && fpvar.outfile[0]) fclose(outreport); return(0); } /*--------------------------------------------------------------------------*/ /* fp_pack assumes the output file does not exist (checked by preflight) */ int fp_pack (char *infits, char *outfits, fpstate fpvar, int *islossless) { fitsfile *infptr, *outfptr; int stat=0; fits_open_file (&infptr, infits, READONLY, &stat); if (stat) { fits_report_error (stderr, stat); return -1; } fits_create_file (&outfptr, outfits, &stat); if (stat) { fp_abort_output(infptr, NULL, stat); return -1; } if (stat) { fp_abort_output(infptr, outfptr, stat); return -1; } while (! stat) { /* LOOP OVER EACH HDU */ fits_set_lossy_int (outfptr, fpvar.int_to_float, &stat); fits_set_compression_type (outfptr, fpvar.comptype, &stat); fits_set_tile_dim (outfptr, 6, fpvar.ntile, &stat); if (fpvar.no_dither) fits_set_quantize_method(outfptr, -1, &stat); else fits_set_quantize_method(outfptr, fpvar.dither_method, &stat); fits_set_quantize_level (outfptr, fpvar.quantize_level, &stat); fits_set_dither_offset(outfptr, fpvar.dither_offset, &stat); fits_set_hcomp_scale (outfptr, fpvar.scale, &stat); fits_set_hcomp_smooth (outfptr, fpvar.smooth, &stat); fp_pack_hdu (infptr, outfptr, fpvar, islossless, &stat); if (fpvar.do_checksums) { fits_write_chksum (outfptr, &stat); } fits_movrel_hdu (infptr, 1, NULL, &stat); } if (stat == END_OF_FILE) stat = 0; /* set checksum for case of newly created primary HDU */ if (fpvar.do_checksums) { fits_movabs_hdu (outfptr, 1, NULL, &stat); fits_write_chksum (outfptr, &stat); } if (stat) { fp_abort_output(infptr, outfptr, stat); } fits_close_file (outfptr, &stat); fits_close_file (infptr, &stat); return(0); } /*--------------------------------------------------------------------------*/ /* fp_unpack assumes the output file does not exist */ int fp_unpack (char *infits, char *outfits, fpstate fpvar) { - fitsfile *infptr, *outfptr; - int stat=0, hdutype, extnum, single = 0; + fitsfile *infptr, *outfptr; + int stat=0, hdutype, extnum, single = 0; char *loc, *hduloc, hduname[SZ_STR]; - fits_open_file (&infptr, infits, READONLY, &stat); - fits_create_file (&outfptr, outfits, &stat); + fits_open_file (&infptr, infits, READONLY, &stat); + fits_create_file (&outfptr, outfits, &stat); if (stat) { fp_abort_output(infptr, outfptr, stat); } - if (fpvar.extname[0]) { /* unpack a list of HDUs? */ + if (fpvar.extname[0]) { /* unpack a list of HDUs? */ - /* move to the first HDU in the list */ + /* move to the first HDU in the list */ hduloc = fpvar.extname; loc = strchr(hduloc, ','); /* look for 'comma' delimiter between names */ if (loc) *loc = '\0'; /* terminate the first name in the string */ strcpy(hduname, hduloc); /* copy the first name into temporary string */ if (loc) hduloc = loc + 1; /* advance to the beginning of the next name, if any */ - else { + else { hduloc += strlen(hduname); /* end of the list */ single = 1; /* only 1 HDU is being unpacked */ - } + } - if (isdigit( (int) hduname[0]) ) { - extnum = strtol(hduname, &loc, 10); /* read the string as an integer */ + if (isdigit( (int) hduname[0]) ) { + extnum = strtol(hduname, &loc, 10); /* read the string as an integer */ - /* check for junk following the integer */ - if (*loc == '\0' ) /* no junk, so move to this HDU number (+1) */ - { - fits_movabs_hdu(infptr, extnum + 1, &hdutype, &stat); /* move to HDU number */ - if (hdutype != IMAGE_HDU) - stat = NOT_IMAGE; + /* check for junk following the integer */ + if (*loc == '\0' ) /* no junk, so move to this HDU number (+1) */ + { + fits_movabs_hdu(infptr, extnum + 1, &hdutype, &stat); /* move to HDU number */ + if (hdutype != IMAGE_HDU) + stat = NOT_IMAGE; - } else { /* the string is not an integer, so must be the column name */ - hdutype = IMAGE_HDU; - fits_movnam_hdu(infptr, hdutype, hduname, 0, &stat); - } + } else { /* the string is not an integer, so must be the column name */ + hdutype = IMAGE_HDU; + fits_movnam_hdu(infptr, hdutype, hduname, 0, &stat); } + } else { - /* move to the named image extension */ + /* move to the named image extension */ hdutype = IMAGE_HDU; fits_movnam_hdu(infptr, hdutype, hduname, 0, &stat); - } } + } - if (stat) { - fp_msg ("Unable to find and move to extension '"); - fp_msg(hduname); - fp_msg("'\n"); - fp_abort_output(infptr, outfptr, stat); - } + if (stat) { + fp_msg ("Unable to find and move to extension '"); + fp_msg(hduname); + fp_msg("'\n"); + fp_abort_output(infptr, outfptr, stat); + } - while (! stat) { + while (! stat) { if (single) stat = -1; /* special status flag to force output primary array */ - fp_unpack_hdu (infptr, outfptr, fpvar, &stat); + fp_unpack_hdu (infptr, outfptr, fpvar, &stat); if (fpvar.do_checksums) { fits_write_chksum (outfptr, &stat); } /* move to the next HDU */ - if (fpvar.extname[0]) { /* unpack a list of HDUs? */ + if (fpvar.extname[0]) { /* unpack a list of HDUs? */ - if (!(*hduloc)) { - stat = END_OF_FILE; /* we reached the end of the list */ - } else { - /* parse the next HDU name and move to it */ + if (!(*hduloc)) { + stat = END_OF_FILE; /* we reached the end of the list */ + } else { + /* parse the next HDU name and move to it */ loc = strchr(hduloc, ','); if (loc) /* look for 'comma' delimiter between names */ - *loc = '\0'; /* terminate the first name in the string */ + *loc = '\0'; /* terminate the first name in the string */ strcpy(hduname, hduloc); /* copy the next name into temporary string */ if (loc) hduloc = loc + 1; /* advance to the beginning of the next name, if any */ - else - *hduloc = '\0'; /* end of the list */ + else + *hduloc = '\0'; /* end of the list */ - if (isdigit( (int) hduname[0]) ) { - extnum = strtol(hduname, &loc, 10); /* read the string as an integer */ + if (isdigit( (int) hduname[0]) ) { + extnum = strtol(hduname, &loc, 10); /* read the string as an integer */ - /* check for junk following the integer */ - if (*loc == '\0' ) /* no junk, so move to this HDU number (+1) */ - { + /* check for junk following the integer */ + if (*loc == '\0' ) /* no junk, so move to this HDU number (+1) */ + { fits_movabs_hdu(infptr, extnum + 1, &hdutype, &stat); /* move to HDU number */ if (hdutype != IMAGE_HDU) - stat = NOT_IMAGE; + stat = NOT_IMAGE; + + } else { /* the string is not an integer, so must be the column name */ + hdutype = IMAGE_HDU; + fits_movnam_hdu(infptr, hdutype, hduname, 0, &stat); + } - } else { /* the string is not an integer, so must be the column name */ + } else { + /* move to the named image extension */ hdutype = IMAGE_HDU; fits_movnam_hdu(infptr, hdutype, hduname, 0, &stat); - } - - } else { - /* move to the named image extension */ - hdutype = IMAGE_HDU; - fits_movnam_hdu(infptr, hdutype, hduname, 0, &stat); - } + } - if (stat) { - fp_msg ("Unable to find and move to extension '"); - fp_msg(hduname); - fp_msg("'\n"); - } + if (stat) { + fp_msg ("Unable to find and move to extension '"); + fp_msg(hduname); + fp_msg("'\n"); } - } else { - /* increment to the next HDU */ - fits_movrel_hdu (infptr, 1, NULL, &stat); } + } else { + /* increment to the next HDU */ + fits_movrel_hdu (infptr, 1, NULL, &stat); } + } - if (stat == END_OF_FILE) stat = 0; + if (stat == END_OF_FILE) stat = 0; - /* set checksum for case of newly created primary HDU + /* set checksum for case of newly created primary HDU */ if (fpvar.do_checksums) { fits_movabs_hdu (outfptr, 1, NULL, &stat); fits_write_chksum (outfptr, &stat); } if (stat) { fp_abort_output(infptr, outfptr, stat); } fits_close_file (outfptr, &stat); fits_close_file (infptr, &stat); return(0); } /*--------------------------------------------------------------------------*/ /* fp_test assumes the output files do not exist */ int fp_test (char *infits, char *outfits, char *outfits2, fpstate fpvar) { fitsfile *inputfptr, *infptr, *outfptr, *outfptr2, *tempfile; long naxes[9] = {1, 1, 1, 1, 1, 1, 1, 1, 1}; int stat=0, totpix=0, naxis=0, ii, hdutype, bitpix = 0, extnum = 0, len; int tstatus = 0, hdunum, rescale_flag, bpix = 8, ncols; char dtype[8], dimen[100]; double bscale, rescale, noisemin; long headstart, datastart, dataend; float origdata = 0., whole_cpu, whole_elapse, row_elapse, row_cpu, xbits; LONGLONG nrows; /* structure to hold image statistics (defined in fpack.h) */ imgstats imagestats; fits_open_file (&inputfptr, infits, READONLY, &stat); fits_create_file (&outfptr, outfits, &stat); fits_create_file (&outfptr2, outfits2, &stat); if (stat) { fits_report_error (stderr, stat); exit (stat); } while (! stat) { /* LOOP OVER EACH HDU */ rescale_flag = 0; fits_get_hdu_type (inputfptr, &hdutype, &stat); if (hdutype == IMAGE_HDU) { fits_get_img_param (inputfptr, 9, &bitpix, &naxis, naxes, &stat); for (totpix=1, ii=0; ii < 9; ii++) totpix *= naxes[ii]; } if (!fits_is_compressed_image (inputfptr, &stat) && hdutype == IMAGE_HDU && naxis != 0 && totpix != 0 && fpvar.do_images) { /* rescale a scaled integer image to reduce noise? */ if (fpvar.rescale_noise != 0. && bitpix > 0 && bitpix < LONGLONG_IMG) { tstatus = 0; fits_read_key(inputfptr, TDOUBLE, "BSCALE", &bscale, 0, &tstatus); if (tstatus == 0 && bscale != 1.0) { /* image must be scaled */ if (bitpix == LONG_IMG) fp_i4stat(inputfptr, naxis, naxes, &imagestats, &stat); else fp_i2stat(inputfptr, naxis, naxes, &imagestats, &stat); /* use the minimum of the MAD 2nd, 3rd, and 5th order noise estimates */ noisemin = imagestats.noise3; if (imagestats.noise2 != 0. && imagestats.noise2 < noisemin) noisemin = imagestats.noise2; if (imagestats.noise5 != 0. && imagestats.noise5 < noisemin) noisemin = imagestats.noise5; rescale = noisemin / fpvar.rescale_noise; if (rescale > 1.0) { /* all the criteria are met, so create a temporary file that */ /* contains a rescaled version of the image, in CWD */ /* create temporary file name */ fp_tmpnam("Tmpfile3", "", tempfilename3); fits_create_file(&tempfile, tempfilename3, &stat); fits_get_hdu_num(inputfptr, &hdunum); if (hdunum != 1) { /* the input hdu is an image extension, so create dummy primary */ fits_create_img(tempfile, 8, 0, naxes, &stat); } fits_copy_header(inputfptr, tempfile, &stat); /* copy the header */ /* rescale the data, so that it will compress more efficiently */ if (bitpix == LONG_IMG) fp_i4rescale(inputfptr, naxis, naxes, rescale, tempfile, &stat); else fp_i2rescale(inputfptr, naxis, naxes, rescale, tempfile, &stat); /* scale the BSCALE keyword by the inverse factor */ bscale = bscale * rescale; fits_update_key(tempfile, TDOUBLE, "BSCALE", &bscale, 0, &stat); /* rescan the header, to reset the actual scaling parameters */ fits_set_hdustruc(tempfile, &stat); infptr = tempfile; rescale_flag = 1; } } } if (!rescale_flag) /* just compress the input file, without rescaling */ infptr = inputfptr; /* compute basic statistics about the input image */ if (bitpix == BYTE_IMG) { bpix = 8; strcpy(dtype, "8 "); fp_i2stat(infptr, naxis, naxes, &imagestats, &stat); } else if (bitpix == SHORT_IMG) { bpix = 16; strcpy(dtype, "16 "); fp_i2stat(infptr, naxis, naxes, &imagestats, &stat); } else if (bitpix == LONG_IMG) { bpix = 32; strcpy(dtype, "32 "); fp_i4stat(infptr, naxis, naxes, &imagestats, &stat); } else if (bitpix == LONGLONG_IMG) { bpix = 64; strcpy(dtype, "64 "); } else if (bitpix == FLOAT_IMG) { bpix = 32; strcpy(dtype, "-32"); fp_r4stat(infptr, naxis, naxes, &imagestats, &stat); } else if (bitpix == DOUBLE_IMG) { bpix = 64; strcpy(dtype, "-64"); fp_r4stat(infptr, naxis, naxes, &imagestats, &stat); } /* use the minimum of the MAD 2nd, 3rd, and 5th order noise estimates */ noisemin = imagestats.noise3; if (imagestats.noise2 != 0. && imagestats.noise2 < noisemin) noisemin = imagestats.noise2; if (imagestats.noise5 != 0. && imagestats.noise5 < noisemin) noisemin = imagestats.noise5; xbits = (float) (log10(noisemin)/.301 + 1.792); printf("\n File: %s\n", infits); printf(" Ext BITPIX Dimens. Nulls Min Max Mean Sigma Noise2 Noise3 Noise5 Nbits MaxR\n"); printf(" %3d %s", extnum, dtype); snprintf(dimen,100," (%ld", naxes[0]); len =strlen(dimen); for (ii = 1; ii < naxis; ii++) { if (len < 99) snprintf(dimen+len,100-len,",%ld", naxes[ii]); len =strlen(dimen); } if (strlen(dimen)<99) strcat(dimen, ")"); printf("%-12s",dimen); fits_get_hduaddr(inputfptr, &headstart, &datastart, &dataend, &stat); origdata = (float) ((dataend - datastart)/1000000.); /* get elapsed and cpu times need to read the uncompressed image */ fits_read_image_speed (infptr, &whole_elapse, &whole_cpu, &row_elapse, &row_cpu, &stat); printf(" %5d %6.0f %6.0f %8.1f %#8.2g %#7.3g %#7.3g %#7.3g %#5.1f %#6.2f\n", imagestats.n_nulls, imagestats.minval, imagestats.maxval, imagestats.mean, imagestats.sigma, imagestats.noise2, imagestats.noise3, imagestats.noise5, xbits, bpix/xbits); printf("\n Type Ratio Size (MB) Pk (Sec) UnPk Exact ElpN CPUN Elp1 CPU1\n"); printf(" Native %5.3f %5.3f %5.3f %5.3f\n", whole_elapse, whole_cpu, row_elapse, row_cpu); if (fpvar.outfile[0]) { fprintf(outreport, " %s %d %d %ld %ld %#10.4g %d %#10.4g %#10.4g %#10.4g %#10.4g %#10.4g %#10.4g %#10.4g %#10.4g %#10.4g %#10.4g %#10.4g %#10.4g", infits, extnum, bitpix, naxes[0], naxes[1], origdata, imagestats.n_nulls, imagestats.minval, imagestats.maxval, imagestats.mean, imagestats.sigma, imagestats.noise1, imagestats.noise2, imagestats.noise3, imagestats.noise5, whole_elapse, whole_cpu, row_elapse, row_cpu); } fits_set_lossy_int (outfptr, fpvar.int_to_float, &stat); if ( (bitpix > 0) && (fpvar.int_to_float != 0) ) { if ( (noisemin < (fpvar.n3ratio * fpvar.quantize_level) ) || (noisemin < fpvar.n3min)) { /* image contains too little noise to quantize effectively */ fits_set_lossy_int (outfptr, 0, &stat); fits_get_hdu_num(infptr, &hdunum); printf(" HDU %d does not meet noise criteria to be quantized, so losslessly compressed.\n", hdunum); } } /* test compression ratio and speed for each algorithm */ if (fpvar.quantize_level != 0) { fits_set_compression_type (outfptr, RICE_1, &stat); fits_set_tile_dim (outfptr, 6, fpvar.ntile, &stat); if (fpvar.no_dither) fits_set_quantize_method(outfptr, -1, &stat); else fits_set_quantize_method(outfptr, fpvar.dither_method, &stat); fits_set_quantize_level (outfptr, fpvar.quantize_level, &stat); fits_set_dither_offset(outfptr, fpvar.dither_offset, &stat); fits_set_hcomp_scale (outfptr, fpvar.scale, &stat); fits_set_hcomp_smooth (outfptr, fpvar.smooth, &stat); fp_test_hdu(infptr, outfptr, outfptr2, fpvar, &stat); } if (fpvar.quantize_level != 0) { \ fits_set_compression_type (outfptr, HCOMPRESS_1, &stat); fits_set_tile_dim (outfptr, 6, fpvar.ntile, &stat); if (fpvar.no_dither) fits_set_quantize_method(outfptr, -1, &stat); else fits_set_quantize_method(outfptr, fpvar.dither_method, &stat); fits_set_quantize_level (outfptr, fpvar.quantize_level, &stat); fits_set_dither_offset(outfptr, fpvar.dither_offset, &stat); fits_set_hcomp_scale (outfptr, fpvar.scale, &stat); fits_set_hcomp_smooth (outfptr, fpvar.smooth, &stat); fp_test_hdu(infptr, outfptr, outfptr2, fpvar, &stat); } if (fpvar.comptype == GZIP_2) { fits_set_compression_type (outfptr, GZIP_2, &stat); } else { fits_set_compression_type (outfptr, GZIP_1, &stat); } fits_set_tile_dim (outfptr, 6, fpvar.ntile, &stat); if (fpvar.no_dither) fits_set_quantize_method(outfptr, -1, &stat); else fits_set_quantize_method(outfptr, fpvar.dither_method, &stat); fits_set_quantize_level (outfptr, fpvar.quantize_level, &stat); fits_set_dither_offset(outfptr, fpvar.dither_offset, &stat); fits_set_hcomp_scale (outfptr, fpvar.scale, &stat); fits_set_hcomp_smooth (outfptr, fpvar.smooth, &stat); fp_test_hdu(infptr, outfptr, outfptr2, fpvar, &stat); /* fits_set_compression_type (outfptr, BZIP2_1, &stat); fits_set_tile_dim (outfptr, 6, fpvar.ntile, &stat); fp_test_hdu(infptr, outfptr, outfptr2, fpvar, &stat); */ /* fits_set_compression_type (outfptr, PLIO_1, &stat); fits_set_tile_dim (outfptr, 6, fpvar.ntile, &stat); fp_test_hdu(infptr, outfptr, outfptr2, fpvar, &stat); */ /* if (bitpix == SHORT_IMG || bitpix == LONG_IMG) { fits_set_compression_type (outfptr, NOCOMPRESS, &stat); fits_set_tile_dim (outfptr, 6, fpvar.ntile, &stat); fp_test_hdu(infptr, outfptr, outfptr2, fpvar, &stat); } */ if (fpvar.outfile[0]) fprintf(outreport,"\n"); /* delete the temporary file */ if (rescale_flag) { fits_delete_file (infptr, &stat); tempfilename3[0] = '\0'; /* clear the temp filename */ } } else if ( (hdutype == BINARY_TBL) && fpvar.do_tables) { fits_get_num_rowsll(inputfptr, &nrows, &stat); fits_get_num_cols(inputfptr, &ncols, &stat); #if defined(_MSC_VER) /* Microsoft Visual C++ 6.0 uses '%I64d' syntax for 8-byte integers */ printf("\n File: %s, HDU %d, %d cols X %I64d rows\n", infits, extnum, ncols, nrows); #elif (USE_LL_SUFFIX == 1) printf("\n File: %s, HDU %d, %d cols X %lld rows\n", infits, extnum, ncols, nrows); #else printf("\n File: %s, HDU %d, %d cols X %ld rows\n", infits, extnum, ncols, nrows); #endif fp_test_table(inputfptr, outfptr, outfptr2, fpvar, &stat); } else { fits_copy_hdu (inputfptr, outfptr, 0, &stat); fits_copy_hdu (inputfptr, outfptr2, 0, &stat); } fits_movrel_hdu (inputfptr, 1, NULL, &stat); extnum++; } if (stat == END_OF_FILE) stat = 0; fits_close_file (outfptr2, &stat); fits_close_file (outfptr, &stat); fits_close_file (inputfptr, &stat); if (stat) { fits_report_error (stderr, stat); } return(0); } /*--------------------------------------------------------------------------*/ int fp_pack_hdu (fitsfile *infptr, fitsfile *outfptr, fpstate fpvar, int *islossless, int *status) { fitsfile *tempfile; long naxes[9] = {1, 1, 1, 1, 1, 1, 1, 1, 1}; int stat=0, totpix=0, naxis=0, ii, hdutype, bitpix; int tstatus, hdunum; double bscale, rescale; char outfits[SZ_STR], fzalgor[FLEN_VALUE]; long headstart, datastart, dataend, datasize; double noisemin; /* structure to hold image statistics (defined in fpack.h) */ imgstats imagestats; if (*status) return(0); fits_get_hdu_type (infptr, &hdutype, &stat); if (hdutype == IMAGE_HDU) { fits_get_img_param (infptr, 9, &bitpix, &naxis, naxes, &stat); for (totpix=1, ii=0; ii < 9; ii++) totpix *= naxes[ii]; } /* check directive keyword to see if this HDU should not be compressed */ tstatus = 0; if (!fits_read_key(infptr, TSTRING, "FZALGOR", fzalgor, NULL, &tstatus) ) { if (!strcmp(fzalgor, "NONE") || !strcmp(fzalgor, "none") ) { fits_copy_hdu (infptr, outfptr, 0, &stat); *status = stat; return(0); } } /* =============================================================== */ /* This block is only for binary table compression */ if (hdutype == BINARY_TBL && fpvar.do_tables) { fits_get_hduaddr(infptr, &headstart, &datastart, &dataend, status); datasize = dataend - datastart; if (datasize <= 2880) { /* data is less than 1 FITS block in size, so don't compress */ fits_copy_hdu (infptr, outfptr, 0, &stat); } else { fits_compress_table (infptr, outfptr, &stat); } *status = stat; return(0); } /* =============================================================== */ /* If this is not a non-null image HDU, just copy it verbatim */ if (fits_is_compressed_image (infptr, &stat) || hdutype != IMAGE_HDU || naxis == 0 || totpix == 0 || !fpvar.do_images) { fits_copy_hdu (infptr, outfptr, 0, &stat); } else { /* remaining code deals only with IMAGE HDUs */ /* special case: rescale a scaled integer image to reduce noise? */ if (fpvar.rescale_noise != 0. && bitpix > 0 && bitpix < LONGLONG_IMG) { tstatus = 0; fits_read_key(infptr, TDOUBLE, "BSCALE", &bscale, 0, &tstatus); if (tstatus == 0 && bscale != 1.0) { /* image must be scaled */ if (bitpix == LONG_IMG) fp_i4stat(infptr, naxis, naxes, &imagestats, &stat); else fp_i2stat(infptr, naxis, naxes, &imagestats, &stat); /* use the minimum of the MAD 2nd, 3rd, and 5th order noise estimates */ noisemin = imagestats.noise3; if (imagestats.noise2 != 0. && imagestats.noise2 < noisemin) noisemin = imagestats.noise2; if (imagestats.noise5 != 0. && imagestats.noise5 < noisemin) noisemin = imagestats.noise5; rescale = noisemin / fpvar.rescale_noise; if (rescale > 1.0) { /* all the criteria are met, so create a temporary file that */ /* contains a rescaled version of the image, in output directory */ /* create temporary file name */ fits_file_name(outfptr, outfits, &stat); /* get the output file name */ fp_tmpnam("Tmp3", outfits, tempfilename3); fits_create_file(&tempfile, tempfilename3, &stat); fits_get_hdu_num(infptr, &hdunum); if (hdunum != 1) { /* the input hdu is an image extension, so create dummy primary */ fits_create_img(tempfile, 8, 0, naxes, &stat); } fits_copy_header(infptr, tempfile, &stat); /* copy the header */ /* rescale the data, so that it will compress more efficiently */ if (bitpix == LONG_IMG) fp_i4rescale(infptr, naxis, naxes, rescale, tempfile, &stat); else fp_i2rescale(infptr, naxis, naxes, rescale, tempfile, &stat); /* scale the BSCALE keyword by the inverse factor */ bscale = bscale * rescale; fits_update_key(tempfile, TDOUBLE, "BSCALE", &bscale, 0, &stat); /* rescan the header, to reset the actual scaling parameters */ fits_set_hdustruc(tempfile, &stat); fits_img_compress (tempfile, outfptr, &stat); fits_delete_file (tempfile, &stat); tempfilename3[0] = '\0'; /* clear the temp filename */ *islossless = 0; /* used a lossy compression method */ *status = stat; return(0); } } } /* if requested to do lossy compression of integer images (by */ /* converting to float), then check if this HDU qualifies */ if ( (bitpix > 0) && (fpvar.int_to_float != 0) ) { if (bitpix >= LONG_IMG) fp_i4stat(infptr, naxis, naxes, &imagestats, &stat); else fp_i2stat(infptr, naxis, naxes, &imagestats, &stat); /* rescan the image header to reset scaling values (changed by fp_iNstat) */ ffrhdu(infptr, &hdutype, &stat); /* use the minimum of the MAD 2nd, 3rd, and 5th order noise estimates */ noisemin = imagestats.noise3; if (imagestats.noise2 != 0. && imagestats.noise2 < noisemin) noisemin = imagestats.noise2; if (imagestats.noise5 != 0. && imagestats.noise5 < noisemin) noisemin = imagestats.noise5; if ( (noisemin < (fpvar.n3ratio * fpvar.quantize_level) ) || (imagestats.noise3 < fpvar.n3min)) { /* image contains too little noise to quantize effectively */ fits_set_lossy_int (outfptr, 0, &stat); fits_get_hdu_num(infptr, &hdunum); printf(" HDU %d does not meet noise criteria to be quantized, so losslessly compressed.\n", hdunum); } else { /* compressed image is not identical to original */ *islossless = 0; } } /* finally, do the actual image compression */ fits_img_compress (infptr, outfptr, &stat); if (bitpix < 0 || (fpvar.comptype == HCOMPRESS_1 && fpvar.scale != 0.)) { /* compressed image is not identical to original */ *islossless = 0; } } *status = stat; return(0); } /*--------------------------------------------------------------------------*/ int fp_unpack_hdu (fitsfile *infptr, fitsfile *outfptr, fpstate fpvar, int *status) { UNUSED(fpvar); int hdutype, lval; if (*status > 0) return(0); fits_get_hdu_type (infptr, &hdutype, status); /* =============================================================== */ /* This block is only for beta testing of binary table compression */ if (hdutype == BINARY_TBL) { fits_read_key(infptr, TLOGICAL, "ZTABLE", &lval, NULL, status); if (*status == 0 && lval != 0) { /* uncompress the table */ fits_uncompress_table (infptr, outfptr, status); } else { if (*status == KEY_NO_EXIST) /* table is not compressed */ *status = 0; fits_copy_hdu (infptr, outfptr, 0, status); } return(0); /* =============================================================== */ } else if (fits_is_compressed_image (infptr, status)) { /* uncompress the compressed image HDU */ fits_img_decompress (infptr, outfptr, status); } else { /* not a compressed image HDU, so just copy it to the output */ fits_copy_hdu (infptr, outfptr, 0, status); } return(0); } /*--------------------------------------------------------------------------*/ int fits_read_image_speed (fitsfile *infptr, float *whole_elapse, float *whole_cpu, float *row_elapse, float *row_cpu, int *status) { unsigned char *carray, cnull = 0; short *sarray, snull=0; int bitpix, naxis, anynull, *iarray, inull = 0; long ii, naxes[9], fpixel[9]={1,1,1,1,1,1,1,1,1}, lpixel[9]={1,1,1,1,1,1,1,1,1}; long inc[9]={1,1,1,1,1,1,1,1,1} ; float *earray, enull = 0, filesize; double *darray, dnull = 0; if (*status) return(*status); fits_get_img_param (infptr, 9, &bitpix, &naxis, naxes, status); if (naxis != 2)return(*status); lpixel[0] = naxes[0]; lpixel[1] = naxes[1]; /* filesize in MB */ filesize = (float) (naxes[0] * abs(bitpix) / 8000000. * naxes[1]); /* measure time required to read the raw image */ fits_set_bscale(infptr, 1.0, 0.0, status); *whole_elapse = 0.; *whole_cpu = 0; if (bitpix == BYTE_IMG) { carray = calloc(naxes[1]*naxes[0], sizeof(char)); /* remove any cached uncompressed tile (dangerous to directly modify the structure!) */ /* (infptr->Fptr)->tilerow = 0; */ marktime(status); fits_read_subset(infptr, TBYTE, fpixel, lpixel, inc, &cnull, carray, &anynull, status); /* get elapsped times */ gettime(whole_elapse, whole_cpu, status); /* now read the image again, row by row */ if (row_elapse) { /* remove any cached uncompressed tile (dangerous to directly modify the structure!) */ /* (infptr->Fptr)->tilerow = 0; */ marktime(status); for (ii = 0; ii < naxes[1]; ii++) { fpixel[1] = ii+1; fits_read_pix(infptr, TBYTE, fpixel, naxes[0], &cnull, carray, &anynull, status); } /* get elapsped times */ gettime(row_elapse, row_cpu, status); } free(carray); } else if (bitpix == SHORT_IMG) { sarray = calloc(naxes[0]*naxes[1], sizeof(short)); marktime(status); fits_read_subset(infptr, TSHORT, fpixel, lpixel, inc, &snull, sarray, &anynull, status); gettime(whole_elapse, whole_cpu, status); /* get elapsped times */ /* now read the image again, row by row */ if (row_elapse) { marktime(status); for (ii = 0; ii < naxes[1]; ii++) { fpixel[1] = ii+1; fits_read_pix(infptr, TSHORT, fpixel, naxes[0], &snull, sarray, &anynull, status); } /* get elapsped times */ gettime(row_elapse, row_cpu, status); } free(sarray); } else if (bitpix == LONG_IMG) { iarray = calloc(naxes[0]*naxes[1], sizeof(int)); marktime(status); fits_read_subset(infptr, TINT, fpixel, lpixel, inc, &inull, iarray, &anynull, status); /* get elapsped times */ gettime(whole_elapse, whole_cpu, status); /* now read the image again, row by row */ if (row_elapse) { marktime(status); for (ii = 0; ii < naxes[1]; ii++) { fpixel[1] = ii+1; fits_read_pix(infptr, TINT, fpixel, naxes[0], &inull, iarray, &anynull, status); } /* get elapsped times */ gettime(row_elapse, row_cpu, status); } free(iarray); } else if (bitpix == FLOAT_IMG) { earray = calloc(naxes[1]*naxes[0], sizeof(float)); marktime(status); fits_read_subset(infptr, TFLOAT, fpixel, lpixel, inc, &enull, earray, &anynull, status); /* get elapsped times */ gettime(whole_elapse, whole_cpu, status); /* now read the image again, row by row */ if (row_elapse) { marktime(status); for (ii = 0; ii < naxes[1]; ii++) { fpixel[1] = ii+1; fits_read_pix(infptr, TFLOAT, fpixel, naxes[0], &enull, earray, &anynull, status); } /* get elapsped times */ gettime(row_elapse, row_cpu, status); } free(earray); } else if (bitpix == DOUBLE_IMG) { darray = calloc(naxes[1]*naxes[0], sizeof(double)); marktime(status); fits_read_subset(infptr, TDOUBLE, fpixel, lpixel, inc, &dnull, darray, &anynull, status); /* get elapsped times */ gettime(whole_elapse, whole_cpu, status); /* now read the image again, row by row */ if (row_elapse) { marktime(status); for (ii = 0; ii < naxes[1]; ii++) { fpixel[1] = ii+1; fits_read_pix(infptr, TDOUBLE, fpixel, naxes[0], &dnull, darray, &anynull, status); } /* get elapsped times */ gettime(row_elapse, row_cpu, status); } free(darray); } if (whole_elapse) *whole_elapse = *whole_elapse / filesize; if (row_elapse) *row_elapse = *row_elapse / filesize; if (whole_cpu) *whole_cpu = *whole_cpu / filesize; if (row_cpu) *row_cpu = *row_cpu / filesize; return(*status); } /*--------------------------------------------------------------------------*/ int fp_test_hdu (fitsfile *infptr, fitsfile *outfptr, fitsfile *outfptr2, fpstate fpvar, int *status) { /* This routine is only used for performance testing of image HDUs. */ /* Use fp_test_table for testing binary table HDUs. */ int stat = 0, hdutype, comptype; char ctype[20], lossless[4]; long headstart, datastart, dataend; float origdata = 0., compressdata = 0.; float compratio = 0., packcpu = 0., unpackcpu = 0.; float elapse, whole_elapse, row_elapse, whole_cpu, row_cpu; unsigned long datasum1, datasum2, hdusum; if (*status) return(0); origdata = 0; compressdata = 0; compratio = 0.; lossless[0] = '\0'; fits_get_compression_type(outfptr, &comptype, &stat); if (comptype == RICE_1) strcpy(ctype, "RICE"); else if (comptype == GZIP_1) strcpy(ctype, "GZIP1"); else if (comptype == GZIP_2) strcpy(ctype, "GZIP2");/* else if (comptype == BZIP2_1) strcpy(ctype, "BZIP2"); */ else if (comptype == PLIO_1) strcpy(ctype, "PLIO"); else if (comptype == HCOMPRESS_1) strcpy(ctype, "HCOMP"); else if (comptype == NOCOMPRESS) strcpy(ctype, "NONE"); else { fp_msg ("Error: unsupported image compression type "); *status = DATA_COMPRESSION_ERR; return(0); } /* -------------- COMPRESS the image ------------------ */ marktime(&stat); fits_img_compress (infptr, outfptr, &stat); /* get elapsped times */ gettime(&elapse, &packcpu, &stat); /* get elapsed and cpu times need to read the compressed image */ fits_read_image_speed (outfptr, &whole_elapse, &whole_cpu, &row_elapse, &row_cpu, &stat); if (!stat) { /* -------------- UNCOMPRESS the image ------------------ */ /* remove any cached uncompressed tile (dangerous to directly modify the structure!) */ /* (outfptr->Fptr)->tilerow = 0; */ marktime(&stat); fits_img_decompress (outfptr, outfptr2, &stat); /* get elapsped times */ gettime(&elapse, &unpackcpu, &stat); /* ----------------------------------------------------- */ /* get sizes of original and compressed images */ fits_get_hduaddr(infptr, &headstart, &datastart, &dataend, &stat); origdata = (float) ((dataend - datastart)/1000000.); fits_get_hduaddr(outfptr, &headstart, &datastart, &dataend, &stat); compressdata = (float) ((dataend - datastart)/1000000.); if (compressdata != 0) compratio = (float) origdata / (float) compressdata; /* is this uncompressed image identical to the original? */ fits_get_chksum(infptr, &datasum1, &hdusum, &stat); fits_get_chksum(outfptr2, &datasum2, &hdusum, &stat); if ( datasum1 == datasum2) { strcpy(lossless, "Yes"); } else { strcpy(lossless, "No"); } printf(" %-5s %6.2f %7.2f ->%7.2f %7.2f %7.2f %s %5.3f %5.3f %5.3f %5.3f\n", ctype, compratio, origdata, compressdata, packcpu, unpackcpu, lossless, whole_elapse, whole_cpu, row_elapse, row_cpu); if (fpvar.outfile[0]) { fprintf(outreport," %6.3f %5.2f %5.2f %s %7.3f %7.3f %7.3f %7.3f", compratio, packcpu, unpackcpu, lossless, whole_elapse, whole_cpu, row_elapse, row_cpu); } /* delete the output HDUs to concerve disk space */ fits_delete_hdu(outfptr, &hdutype, &stat); fits_delete_hdu(outfptr2, &hdutype, &stat); } else { printf(" %-5s (unable to compress image)\n", ctype); } /* try to recover from any compression errors */ if (stat == DATA_COMPRESSION_ERR) stat = 0; *status = stat; return(0); } /*--------------------------------------------------------------------------*/ int fp_test_table (fitsfile *infptr, fitsfile *outfptr, fitsfile *outfptr2, fpstate fpvar, int *status) { /* this routine is for performance testing of the table compression methods */ UNUSED(fpvar); UNUSED(outfptr2); int stat = 0, hdutype, tstatus = 0; char fzalgor[FLEN_VALUE]; LONGLONG headstart, datastart, dataend; float elapse, cpu; if (*status) return(0); /* check directive keyword to see if this HDU should not be compressed */ if (!fits_read_key(infptr, TSTRING, "FZALGOR", fzalgor, NULL, &tstatus) ) { if (!strcmp(fzalgor, "NONE") || !strcmp(fzalgor, "none")) { return(0); } } fits_get_hduaddrll(infptr, &headstart, &datastart, &dataend, status); /* can't compress small tables with less than 2880 bytes of data */ if (dataend - datastart <= 2880) { return(0); } marktime(&stat); stat= -999; /* set special flag value */ fits_compress_table (infptr, outfptr, &stat); /* get elapsped times */ gettime(&elapse, &cpu, &stat); fits_delete_hdu(outfptr, &hdutype, &stat); printf("\nElapsed time = %f, cpu = %f\n", elapse, cpu); fits_report_error (stderr, stat); return(0); } /*--------------------------------------------------------------------------*/ int marktime(int *status) { #if defined(unix) || defined(__unix__) || defined(__unix) struct timeval tv; /* struct timezone tz; */ /* gettimeofday (&tv, &tz); */ gettimeofday (&tv, NULL); startsec = tv.tv_sec; startmilli = tv.tv_usec/1000; scpu = clock(); #else /* don't support high timing precision on Windows machines */ startsec = 0; startmilli = 0; scpu = clock(); #endif return( *status ); } /*--------------------------------------------------------------------------*/ int gettime(float *elapse, float *elapscpu, int *status) { #if defined(unix) || defined(__unix__) || defined(__unix) struct timeval tv; /* struct timezone tz; */ int stopmilli; long stopsec; /* gettimeofday (&tv, &tz); */ gettimeofday (&tv, NULL); ecpu = clock(); stopmilli = tv.tv_usec/1000; stopsec = tv.tv_sec; *elapse = (stopsec - startsec) + (stopmilli - startmilli)/1000.; *elapscpu = (ecpu - scpu) * 1.0 / CLOCKTICKS; /* printf(" (start: %ld + %d), stop: (%ld + %d) elapse: %f\n ", startsec,startmilli,stopsec, stopmilli, *elapse); */ #else /* set the elapsed time the same as the CPU time on Windows machines */ *elapscpu = (float) ((ecpu - scpu) * 1.0 / CLOCKTICKS); *elapse = *elapscpu; #endif return( *status ); } /*--------------------------------------------------------------------------*/ int fp_i2stat(fitsfile *infptr, int naxis, long *naxes, imgstats *imagestats, int *status) { /* read the central XSAMPLE by YSAMPLE region of pixels in the int*2 image, and then compute basic statistics: min, max, mean, sigma, mean diff, etc. */ long fpixel[9] = {1,1,1,1,1,1,1,1,1}; long lpixel[9] = {1,1,1,1,1,1,1,1,1}; long inc[9] = {1,1,1,1,1,1,1,1,1}; long i1, i2, npix, ngood, nx, ny; short *intarray, minvalue, maxvalue, nullvalue; int anynul, tstatus, checknull = 1; double mean, sigma, noise1, noise2, noise3, noise5; /* select the middle XSAMPLE by YSAMPLE area of the image */ i1 = naxes[0]/2 - (XSAMPLE/2 - 1); i2 = naxes[0]/2 + (XSAMPLE/2); if (i1 < 1) i1 = 1; if (i2 > naxes[0]) i2 = naxes[0]; fpixel[0] = i1; lpixel[0] = i2; nx = i2 - i1 +1; if (naxis > 1) { i1 = naxes[1]/2 - (YSAMPLE/2 - 1); i2 = naxes[1]/2 + (YSAMPLE/2); if (i1 < 1) i1 = 1; if (i2 > naxes[1]) i2 = naxes[1]; fpixel[1] = i1; lpixel[1] = i2; } ny = i2 - i1 +1; npix = nx * ny; /* if there are higher dimensions, read the middle plane of the cube */ if (naxis > 2) { fpixel[2] = naxes[2]/2 + 1; lpixel[2] = naxes[2]/2 + 1; } intarray = calloc(npix, sizeof(short)); if (!intarray) { *status = MEMORY_ALLOCATION; return(*status); } /* turn off any scaling of the integer pixel values */ fits_set_bscale(infptr, 1.0, 0.0, status); fits_read_subset_sht(infptr, 0, naxis, naxes, fpixel, lpixel, inc, 0, intarray, &anynul, status); /* read the null value keyword (BLANK) if present */ tstatus = 0; fits_read_key(infptr, TSHORT, "BLANK", &nullvalue, 0, &tstatus); if (tstatus) { nullvalue = 0; checknull = 0; } /* compute statistics of the image */ fits_img_stats_short(intarray, nx, ny, checknull, nullvalue, &ngood, &minvalue, &maxvalue, &mean, &sigma, &noise1, &noise2, &noise3, &noise5, status); imagestats->n_nulls = npix - ngood; imagestats->minval = minvalue; imagestats->maxval = maxvalue; imagestats->mean = mean; imagestats->sigma = sigma; imagestats->noise1 = noise1; imagestats->noise2 = noise2; imagestats->noise3 = noise3; imagestats->noise5 = noise5; free(intarray); return(*status); } /*--------------------------------------------------------------------------*/ int fp_i4stat(fitsfile *infptr, int naxis, long *naxes, imgstats *imagestats, int *status) { /* read the central XSAMPLE by YSAMPLE region of pixels in the int*2 image, and then compute basic statistics: min, max, mean, sigma, mean diff, etc. */ long fpixel[9] = {1,1,1,1,1,1,1,1,1}; long lpixel[9] = {1,1,1,1,1,1,1,1,1}; long inc[9] = {1,1,1,1,1,1,1,1,1}; long i1, i2, npix, ngood, nx, ny; int *intarray, minvalue, maxvalue, nullvalue; int anynul, tstatus, checknull = 1; double mean, sigma, noise1, noise2, noise3, noise5; /* select the middle XSAMPLE by YSAMPLE area of the image */ i1 = naxes[0]/2 - (XSAMPLE/2 - 1); i2 = naxes[0]/2 + (XSAMPLE/2); if (i1 < 1) i1 = 1; if (i2 > naxes[0]) i2 = naxes[0]; fpixel[0] = i1; lpixel[0] = i2; nx = i2 - i1 +1; if (naxis > 1) { i1 = naxes[1]/2 - (YSAMPLE/2 - 1); i2 = naxes[1]/2 + (YSAMPLE/2); if (i1 < 1) i1 = 1; if (i2 > naxes[1]) i2 = naxes[1]; fpixel[1] = i1; lpixel[1] = i2; } ny = i2 - i1 +1; npix = nx * ny; /* if there are higher dimensions, read the middle plane of the cube */ if (naxis > 2) { fpixel[2] = naxes[2]/2 + 1; lpixel[2] = naxes[2]/2 + 1; } intarray = calloc(npix, sizeof(int)); if (!intarray) { *status = MEMORY_ALLOCATION; return(*status); } /* turn off any scaling of the integer pixel values */ fits_set_bscale(infptr, 1.0, 0.0, status); fits_read_subset_int(infptr, 0, naxis, naxes, fpixel, lpixel, inc, 0, intarray, &anynul, status); /* read the null value keyword (BLANK) if present */ tstatus = 0; fits_read_key(infptr, TINT, "BLANK", &nullvalue, 0, &tstatus); if (tstatus) { nullvalue = 0; checknull = 0; } /* compute statistics of the image */ fits_img_stats_int(intarray, nx, ny, checknull, nullvalue, &ngood, &minvalue, &maxvalue, &mean, &sigma, &noise1, &noise2, &noise3, &noise5, status); imagestats->n_nulls = npix - ngood; imagestats->minval = minvalue; imagestats->maxval = maxvalue; imagestats->mean = mean; imagestats->sigma = sigma; imagestats->noise1 = noise1; imagestats->noise2 = noise2; imagestats->noise3 = noise3; imagestats->noise5 = noise5; free(intarray); return(*status); } /*--------------------------------------------------------------------------*/ int fp_r4stat(fitsfile *infptr, int naxis, long *naxes, imgstats *imagestats, int *status) { /* read the central XSAMPLE by YSAMPLE region of pixels in the int*2 image, and then compute basic statistics: min, max, mean, sigma, mean diff, etc. */ long fpixel[9] = {1,1,1,1,1,1,1,1,1}; long lpixel[9] = {1,1,1,1,1,1,1,1,1}; long inc[9] = {1,1,1,1,1,1,1,1,1}; long i1, i2, npix, ngood, nx, ny; float *array, minvalue, maxvalue, nullvalue = FLOATNULLVALUE; int anynul,checknull = 1; double mean, sigma, noise1, noise2, noise3, noise5; /* select the middle XSAMPLE by YSAMPLE area of the image */ i1 = naxes[0]/2 - (XSAMPLE/2 - 1); i2 = naxes[0]/2 + (XSAMPLE/2); if (i1 < 1) i1 = 1; if (i2 > naxes[0]) i2 = naxes[0]; fpixel[0] = i1; lpixel[0] = i2; nx = i2 - i1 +1; if (naxis > 1) { i1 = naxes[1]/2 - (YSAMPLE/2 - 1); i2 = naxes[1]/2 + (YSAMPLE/2); if (i1 < 1) i1 = 1; if (i2 > naxes[1]) i2 = naxes[1]; fpixel[1] = i1; lpixel[1] = i2; } ny = i2 - i1 +1; npix = nx * ny; /* if there are higher dimensions, read the middle plane of the cube */ if (naxis > 2) { fpixel[2] = naxes[2]/2 + 1; lpixel[2] = naxes[2]/2 + 1; } array = calloc(npix, sizeof(float)); if (!array) { *status = MEMORY_ALLOCATION; return(*status); } fits_read_subset_flt(infptr, 0, naxis, naxes, fpixel, lpixel, inc, nullvalue, array, &anynul, status); /* are there any null values in the array? */ if (!anynul) { nullvalue = 0.; checknull = 0; } /* compute statistics of the image */ fits_img_stats_float(array, nx, ny, checknull, nullvalue, &ngood, &minvalue, &maxvalue, &mean, &sigma, &noise1, &noise2, &noise3, &noise5, status); imagestats->n_nulls = npix - ngood; imagestats->minval = minvalue; imagestats->maxval = maxvalue; imagestats->mean = mean; imagestats->sigma = sigma; imagestats->noise1 = noise1; imagestats->noise2 = noise2; imagestats->noise3 = noise3; imagestats->noise5 = noise5; free(array); return(*status); } /*--------------------------------------------------------------------------*/ int fp_i2rescale(fitsfile *infptr, int naxis, long *naxes, double rescale, fitsfile *outfptr, int *status) { /* divide the integer pixel values in the input file by rescale, and write back out to the output file.. */ long ii, jj, nelem = 1, nx, ny; short *intarray, nullvalue; int anynul, tstatus, checknull = 1; nx = naxes[0]; ny = 1; for (ii = 1; ii < naxis; ii++) { ny = ny * naxes[ii]; } intarray = calloc(nx, sizeof(short)); if (!intarray) { *status = MEMORY_ALLOCATION; return(*status); } /* read the null value keyword (BLANK) if present */ tstatus = 0; fits_read_key(infptr, TSHORT, "BLANK", &nullvalue, 0, &tstatus); if (tstatus) { checknull = 0; } /* turn off any scaling of the integer pixel values */ fits_set_bscale(infptr, 1.0, 0.0, status); fits_set_bscale(outfptr, 1.0, 0.0, status); for (ii = 0; ii < ny; ii++) { fits_read_img_sht(infptr, 1, nelem, nx, 0, intarray, &anynul, status); if (checknull) { for (jj = 0; jj < nx; jj++) { if (intarray[jj] != nullvalue) intarray[jj] = NSHRT( (intarray[jj] / rescale) ); } } else { for (jj = 0; jj < nx; jj++) intarray[jj] = NSHRT( (intarray[jj] / rescale) ); } fits_write_img_sht(outfptr, 1, nelem, nx, intarray, status); nelem += nx; } free(intarray); return(*status); } /*--------------------------------------------------------------------------*/ int fp_i4rescale(fitsfile *infptr, int naxis, long *naxes, double rescale, fitsfile *outfptr, int *status) { /* divide the integer pixel values in the input file by rescale, and write back out to the output file.. */ long ii, jj, nelem = 1, nx, ny; int *intarray, nullvalue; int anynul, tstatus, checknull = 1; nx = naxes[0]; ny = 1; for (ii = 1; ii < naxis; ii++) { ny = ny * naxes[ii]; } intarray = calloc(nx, sizeof(int)); if (!intarray) { *status = MEMORY_ALLOCATION; return(*status); } /* read the null value keyword (BLANK) if present */ tstatus = 0; fits_read_key(infptr, TINT, "BLANK", &nullvalue, 0, &tstatus); if (tstatus) { checknull = 0; } /* turn off any scaling of the integer pixel values */ fits_set_bscale(infptr, 1.0, 0.0, status); fits_set_bscale(outfptr, 1.0, 0.0, status); for (ii = 0; ii < ny; ii++) { fits_read_img_int(infptr, 1, nelem, nx, 0, intarray, &anynul, status); if (checknull) { for (jj = 0; jj < nx; jj++) { if (intarray[jj] != nullvalue) intarray[jj] = NINT( (intarray[jj] / rescale) ); } } else { for (jj = 0; jj < nx; jj++) intarray[jj] = NINT( (intarray[jj] / rescale) ); } fits_write_img_int(outfptr, 1, nelem, nx, intarray, status); nelem += nx; } free(intarray); return(*status); } /* ======================================================================== * Signal and error handler. */ void abort_fpack(int sig) { /* clean up by deleting temporary files */ UNUSED(sig); if (tempfilename[0]) { remove(tempfilename); } if (tempfilename2[0]) { remove(tempfilename2); } if (tempfilename3[0]) { remove(tempfilename3); } exit(-1); } diff --git a/kstars/printing/legend.cpp b/kstars/printing/legend.cpp index 6accf24e6..3cbcbc328 100644 --- a/kstars/printing/legend.cpp +++ b/kstars/printing/legend.cpp @@ -1,596 +1,618 @@ /*************************************************************************** legend.cpp - K Desktop Planetarium ------------------- begin : Thu May 26 2011 copyright : (C) 2011 by Rafał Kułaga email : rl.kulaga@gmail.com ***************************************************************************/ /*************************************************************************** * * * 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. * * * ***************************************************************************/ #include "legend.h" #include "colorscheme.h" #include "kstarsdata.h" #include "skymap.h" #include "skyqpainter.h" #include "Options.h" #include namespace { const int symbolSize = 15; const int bRectWidth = 100; const int bRectHeight = 45; const int maxHScalePixels = 200; const int maxVScalePixels = 100; const int xSymbolSpacing = 100; const int ySymbolSpacing = 70; } Legend::Legend(LEGEND_ORIENTATION orientation, LEGEND_POSITION pos) : m_SkyMap(SkyMap::Instance()), m_Orientation(orientation), m_Position(pos), m_PositionFloating(QPoint(0, 0)), m_cScheme(KStarsData::Instance()->colorScheme()), m_SymbolSize(symbolSize), m_BRectWidth(bRectWidth), m_BRectHeight(bRectHeight), m_MaxHScalePixels(maxHScalePixels), m_MaxVScalePixels(maxVScalePixels), m_XSymbolSpacing(xSymbolSpacing), m_YSymbolSpacing(ySymbolSpacing) { m_BgColor = m_cScheme->colorNamed("SkyColor"); } Legend::~Legend() { if (m_Painter != nullptr && m_DeletePainter) { delete m_Painter; } } QSize Legend::calculateSize() { int width = 0; int height = 0; switch (m_Orientation) { case LO_HORIZONTAL: { switch (m_Type) { case LT_SCALE_ONLY: { width = 40 + m_MaxHScalePixels; height = 60; break; } case LT_MAGNITUDES_ONLY: { width = 140; height = 70; break; } case LT_SYMBOLS_ONLY: { width = 7 * m_XSymbolSpacing; height = 20 + m_SymbolSize + m_BRectHeight; break; } case LT_SCALE_MAGNITUDES: { width = 160 + m_MaxHScalePixels; height = 70; break; } case LT_FULL: { width = 7 * m_XSymbolSpacing; height = 20 + m_SymbolSize + m_BRectHeight + 70; break; } default: break; // should never happen } } break; case LO_VERTICAL: { switch (m_Type) { case LT_SCALE_ONLY: { width = 120; height = 40 + m_MaxVScalePixels; break; } case LT_MAGNITUDES_ONLY: { width = 140; height = 70; break; } case LT_SYMBOLS_ONLY: { width = 120; height = 7 * m_YSymbolSpacing; break; } case LT_SCALE_MAGNITUDES: { width = 120; height = 100 + m_MaxVScalePixels; break; } case LT_FULL: { width = 120; height = 100 + 7 * m_YSymbolSpacing + m_MaxVScalePixels; break; } default: break; // should never happen } break; } default: { return QSize(); } } return QSize(width, height); } void Legend::paintLegend(QPaintDevice *pd) { if (m_Painter) { delete m_Painter; } m_Painter = new SkyQPainter(m_SkyMap, pd); m_DeletePainter = true; m_Painter->begin(); paintLegend(m_Painter); m_Painter->end(); } void Legend::paintLegend(SkyQPainter *painter) { if (!m_DeletePainter) { m_Painter = painter; } if (m_Position != LP_FLOATING) { m_PositionFloating = positionToDeviceCoord(painter->device()); } m_Painter->translate(m_PositionFloating.x(), m_PositionFloating.y()); m_Painter->setFont(m_Font); QBrush backgroundBrush(m_BgColor, Qt::SolidPattern); QPen backgroundPen(m_cScheme->colorNamed("SNameColor")); backgroundPen.setStyle(Qt::SolidLine); // set brush & pen m_Painter->setBrush(backgroundBrush); m_Painter->setPen(backgroundPen); QSize size = calculateSize(); if (m_DrawFrame) { m_Painter->drawRect(1, 1, size.width(), size.height()); } else { QPen noLinePen; noLinePen.setStyle(Qt::NoPen); m_Painter->setPen(noLinePen); m_Painter->drawRect(1, 1, size.width(), size.height()); m_Painter->setPen(backgroundPen); } switch (m_Orientation) { case LO_HORIZONTAL: { switch (m_Type) { case LT_SCALE_ONLY: { paintScale(QPointF(20, 20)); break; } case LT_MAGNITUDES_ONLY: { paintMagnitudes(QPointF(20, 20)); break; } case LT_SYMBOLS_ONLY: { paintSymbols(QPointF(20, 20)); break; } case LT_SCALE_MAGNITUDES: { paintMagnitudes(QPointF(20, 20)); paintScale(QPointF(150, 20)); break; } case LT_FULL: { paintSymbols(QPointF(20, 20)); paintMagnitudes(QPointF(10, 40 + m_SymbolSize + m_BRectHeight)); paintScale(QPointF(200, 40 + m_SymbolSize + m_BRectHeight)); break; } default: break; // should never happen } break; } case LO_VERTICAL: { switch (m_Type) { case LT_SCALE_ONLY: { paintScale(QPointF(20, 20)); break; } case LT_MAGNITUDES_ONLY: { paintMagnitudes(QPointF(20, 20)); break; } case LT_SYMBOLS_ONLY: { paintSymbols(QPointF(20, 20)); break; } case LT_SCALE_MAGNITUDES: { paintMagnitudes(QPointF(7, 20)); paintScale(QPointF(20, 80)); break; } case LT_FULL: { paintSymbols(QPointF(30, 20)); paintMagnitudes(QPointF(7, 30 + 7 * m_YSymbolSpacing)); paintScale(QPointF(20, 90 + 7 * m_YSymbolSpacing)); break; } default: break; // should never happen } break; } default: break; // should never happen } } void Legend::paintLegend(QPaintDevice *pd, LEGEND_TYPE type, LEGEND_POSITION pos) { LEGEND_TYPE prevType = m_Type; LEGEND_POSITION prevPos = m_Position; m_Type = type; m_Position = pos; paintLegend(pd); m_Type = prevType; m_Position = prevPos; } void Legend::paintLegend(SkyQPainter *painter, LEGEND_TYPE type, LEGEND_POSITION pos) { LEGEND_TYPE prevType = m_Type; LEGEND_POSITION prevPos = m_Position; m_Type = type; m_Position = pos; paintLegend(painter); m_Type = prevType; m_Position = prevPos; } void Legend::paintSymbols(QPointF pos) { qreal x = pos.x(); qreal y = pos.y(); x += 30; switch (m_Orientation) { case Legend::LO_HORIZONTAL: { // paint Open Cluster/Asterism symbol QString label1 = i18n("Open Cluster") + '\n' + i18n("Asterism"); paintSymbol(QPointF(x, y), 3, 1, 0, label1); x += m_XSymbolSpacing; // paint Globular Cluster symbol paintSymbol(QPointF(x, y), 4, 1, 0, i18n("Globular Cluster")); x += m_XSymbolSpacing; // paint Gaseous Nebula/Dark Nebula symbol QString label3 = i18n("Gaseous Nebula") + '\n' + i18n("Dark Nebula"); paintSymbol(QPointF(x, y), 5, 1, 0, label3); x += m_XSymbolSpacing; // paint Planetary Nebula symbol paintSymbol(QPointF(x, y), 6, 1, 0, i18n("Planetary Nebula")); x += m_XSymbolSpacing; // paint Supernova Remnant paintSymbol(QPointF(x, y), 7, 1, 0, i18n("Supernova Remnant")); x += m_XSymbolSpacing; // paint Galaxy/Quasar QString label6 = i18n("Galaxy") + '\n' + i18n("Quasar"); paintSymbol(QPointF(x, y), 8, 0.5, 60, label6); x += m_XSymbolSpacing; // paint Galaxy Cluster paintSymbol(QPointF(x, y), 14, 1, 0, i18n("Galactic Cluster")); break; } case Legend::LO_VERTICAL: { // paint Open Cluster/Asterism symbol QString label1 = i18n("Open Cluster") + '\n' + i18n("Asterism"); paintSymbol(QPointF(x, y), 3, 1, 0, label1); y += m_YSymbolSpacing; // paint Globular Cluster symbol paintSymbol(QPointF(x, y), 4, 1, 0, i18n("Globular Cluster")); y += m_YSymbolSpacing; // paint Gaseous Nebula/Dark Nebula symbol QString label3 = i18n("Gaseous Nebula") + '\n' + i18n("Dark Nebula"); paintSymbol(QPointF(x, y), 5, 1, 0, label3); y += m_YSymbolSpacing; // paint Planetary Nebula symbol paintSymbol(QPointF(x, y), 6, 1, 0, i18n("Planetary Nebula")); y += m_YSymbolSpacing; // paint Supernova Remnant paintSymbol(QPointF(x, y), 7, 1, 0, i18n("Supernova Remnant")); y += m_YSymbolSpacing; // paint Galaxy/Quasar QString label6 = i18n("Galaxy") + '\n' + i18n("Quasar"); paintSymbol(QPointF(x, y), 8, 0.5, 60, label6); y += m_YSymbolSpacing; // paint Galaxy Cluster paintSymbol(QPointF(x, y), 14, 1, 0, i18n("Galactic Cluster")); break; } default: return; // should never happen } } void Legend::paintSymbol(QPointF pos, int type, float e, float angle, QString label) { qreal x = pos.x(); qreal y = pos.y(); qreal bRectHalfWidth = (qreal)m_BRectWidth / 2; // paint symbol m_Painter->drawDeepSkySymbol(pos, type, m_SymbolSize, e, angle); QRectF bRect(QPoint(x - bRectHalfWidth, y + m_SymbolSize), QPoint(x + bRectHalfWidth, y + m_SymbolSize + m_BRectHeight)); //m_Painter->drawRect(bRect); // paint label m_Painter->drawText(bRect, label, QTextOption(Qt::AlignHCenter)); } void Legend::paintMagnitudes(QPointF pos) { qreal x = pos.x(); qreal y = pos.y(); m_Painter->drawText(x, y, i18n("Star Magnitudes:")); y += 15; for (int i = 1; i <= 9; i += 2) { m_Painter->drawPointSource(QPointF(x + i * 10, y), m_Painter->starWidth(i)); m_Painter->drawText(x + i * 10 - 4, y + 20, QString::number(i)); } } void Legend::paintScale(QPointF pos) { qreal maxScalePixels; switch (m_Orientation) { case LO_HORIZONTAL: { maxScalePixels = m_MaxHScalePixels; break; } case LO_VERTICAL: { maxScalePixels = m_MaxVScalePixels; break; } default: return; // should never happen } qreal maxArcsec = maxScalePixels * 57.3 * 3600 / Options::zoomFactor(); int deg = 0; int arcmin = 0; int arcsec = 0; QString lab; if (maxArcsec >= 3600) { deg = maxArcsec / 3600; lab = QString::number(deg) + QString::fromWCharArray(L"\u00B0"); } else if (maxArcsec >= 60) { arcmin = maxArcsec / 60; lab = QString::number(arcmin) + '\''; } else { arcsec = maxArcsec; lab = QString::number(arcsec) + "\""; } int actualArcsec = 3600 * deg + 60 * arcmin + arcsec; qreal size = actualArcsec * Options::zoomFactor() / 57.3 / 3600; qreal x = pos.x(); qreal y = pos.y(); switch (m_Orientation) { case LO_HORIZONTAL: { m_Painter->drawText(pos, i18n("Chart Scale:")); y += 15; m_Painter->drawLine(x, y, x + size, y); // paint line endings m_Painter->drawLine(x, y - 5, x, y + 5); m_Painter->drawLine(x + size, y - 5, x + size, y + 5); // paint scale text QRectF bRect(QPoint(x, y), QPoint(x + size, y + 20)); m_Painter->drawText(bRect, lab, QTextOption(Qt::AlignHCenter)); break; } case LO_VERTICAL: { m_Painter->drawText(pos, i18n("Chart Scale:")); y += 10; x += 40; m_Painter->drawLine(x, y, x, y + size); // paint line endings m_Painter->drawLine(x - 5, y, x + 5, y); m_Painter->drawLine(x - 5, y + size, x + 5, y + size); // paint scale text QRectF bRect(QPoint(x + 5, y), QPoint(x + 20, y + size)); //m_Painter->drawRect(bRect); m_Painter->drawText(bRect, lab, QTextOption(Qt::AlignVCenter)); break; } default: return; // should never happen } } QPoint Legend::positionToDeviceCoord(QPaintDevice *pd) { QSize legendSize = calculateSize(); switch (m_Position) { case LP_UPPER_LEFT: // position: upper left corner { return QPoint(0, 0); } case LP_UPPER_RIGHT: // position: upper right corner { return QPoint(pd->width() - legendSize.width(), 0); } case LP_LOWER_LEFT: // position: lower left corner { return QPoint(0, pd->height() - legendSize.height()); } case LP_LOWER_RIGHT: // position: lower right corner { return QPoint(pd->width() - legendSize.width(), pd->height() - legendSize.height()); } default: // legend is floating { return QPoint(); } } } Legend::Legend(const Legend &o) : m_Painter(nullptr), m_SkyMap(o.m_SkyMap), m_DeletePainter(o.m_DeletePainter), m_Type(o.m_Type), m_Orientation(o.m_Orientation), m_Position(o.m_Position), m_PositionFloating(o.m_PositionFloating), m_cScheme(o.m_cScheme), m_Font(o.m_Font), m_BgColor(o.m_BgColor), m_DrawFrame(o.m_DrawFrame), m_SymbolSize(o.m_SymbolSize), m_BRectWidth(o.m_BRectWidth), m_BRectHeight(o.m_BRectHeight), m_MaxHScalePixels(o.m_MaxHScalePixels), m_MaxVScalePixels(o.m_MaxVScalePixels), m_XSymbolSpacing(o.m_XSymbolSpacing), m_YSymbolSpacing(o.m_YSymbolSpacing) { } + +Legend& Legend::operator=(const Legend &o) noexcept +{ + m_SkyMap = o.m_SkyMap; + m_DeletePainter = o.m_DeletePainter; + m_Type = o.m_Type; + m_Orientation = o.m_Orientation; + m_Position = o.m_Position; + m_PositionFloating = o.m_PositionFloating; + m_cScheme = o.m_cScheme; + m_Font = o.m_Font; + m_BgColor = o.m_BgColor; + m_DrawFrame = o.m_DrawFrame; + m_SymbolSize = o.m_SymbolSize; + m_BRectWidth = o.m_BRectWidth; + m_BRectHeight = o.m_BRectHeight; + m_MaxHScalePixels = o.m_MaxHScalePixels; + m_MaxVScalePixels = o.m_MaxVScalePixels; + m_XSymbolSpacing = o.m_XSymbolSpacing; + m_YSymbolSpacing = o.m_YSymbolSpacing; + return *this; +} diff --git a/kstars/printing/legend.h b/kstars/printing/legend.h index 5222ffc77..239a8505e 100644 --- a/kstars/printing/legend.h +++ b/kstars/printing/legend.h @@ -1,369 +1,373 @@ /*************************************************************************** legend.h - K Desktop Planetarium ------------------- begin : Thu May 26 2011 copyright : (C) 2011 by Rafał Kułaga email : rl.kulaga@gmail.com ***************************************************************************/ /*************************************************************************** * * * 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. * * * ***************************************************************************/ #pragma once #include #include #include class QPaintDevice; class QPointF; class QSize; class ColorScheme; class SkyMap; class SkyQPainter; /** * \class Legend * \brief Legend class is used for painting legends on class inheriting QPaintDevice. * Its methods enable changing settings of legend such as legend type (scale only/full legend), * symbol size, sizes for symbol description's bounding rectangles, symbol spacing etc. * Typical use of this class would be to create instance of Legend class, set all properties * using appropriate methods and paint it by calling paintLegend() method, passing QPaintDevice * or QPainter subclass (useful eg. with QSvgGenerator class, which can't be painted by two QPainter * classes). * \author Rafał Kułaga */ class Legend { public: /** * \brief Legend type enumeration. */ enum LEGEND_TYPE { LT_FULL, LT_SCALE_MAGNITUDES, LT_SCALE_ONLY, LT_MAGNITUDES_ONLY, LT_SYMBOLS_ONLY, }; /** * \brief Legend orientation enumeration. */ enum LEGEND_ORIENTATION { LO_HORIZONTAL, LO_VERTICAL }; /** * \brief Legend position enumeration. */ enum LEGEND_POSITION { LP_UPPER_LEFT, LP_UPPER_RIGHT, LP_LOWER_LEFT, LP_LOWER_RIGHT, LP_FLOATING }; /** * \brief Constructor. * \param orientation Legend orientation. * \param pos Legend position. */ explicit Legend(LEGEND_ORIENTATION orientation = LO_HORIZONTAL, LEGEND_POSITION pos = LP_FLOATING); /** * \brief copy constructor * \note This class needs to be explicitly copied because of the m_Painter pointer */ + /** @{ */ explicit Legend(const Legend &o); + Legend& operator=(const Legend &o) noexcept; + /** @} */ + /** * \brief Destructor. */ ~Legend(); /** * \brief Get legend type. * \return Current legend type value. */ inline LEGEND_TYPE getType() const { return m_Type; } /** * \brief Get legend orientation. * \return Current legend orientation value. */ inline LEGEND_ORIENTATION getOrientation() const { return m_Orientation; } /** * \brief Get legend position. * \return Current legend position value. */ inline LEGEND_POSITION getPosition() const { return m_Position; } /** * \brief Get position of the floating legend. * \return Current position of the floating legend. */ inline QPoint getFloatingPosition() const { return m_PositionFloating; } /** * \brief Get symbol size. * \return Current symbol size value. */ inline int getSymbolSize() const { return m_SymbolSize; } /** * \brief Get symbol description's bounding rectangle width. * \return Current bounding rectangle width. */ inline int getBRectWidth() const { return m_BRectWidth; } /** * \brief Get symbol description's bounding rectangle height. * \return Current bounding rectangle height. */ inline int getBRectHeight() const { return m_BRectHeight; } /** * \brief Get maximal horizontal scale size in pixels. * \return Current maximal horizontal scale size in pixels. */ inline int getMaxHScalePixels() const { return m_MaxHScalePixels; } /** * \brief Get maximal vertical scale size in pixels. * \brief Current maximal vertical scale size in pixels. */ inline int getMaxVScalePixels() const { return m_MaxVScalePixels; } /** * \brief Get symbol image X spacing. * \return Current symbol image X spacing. */ inline int getXSymbolSpacing() const { return m_XSymbolSpacing; } /** * \brief Get symbol image Y spacing. * \return Current symbol image Y spacing. */ inline int getYSymbolSpacing() const { return m_YSymbolSpacing; } /** * \brief Get font. * \return Current font. */ inline QFont getFont() const { return m_Font; } /** * \brief Get background color. * \return Current background color. */ inline QColor getBgColor() const { return m_BgColor; } /** * \brief Check if frame around legend is drawn. * \return True if frame is drawn. */ inline bool getDrawFrame() const { return m_DrawFrame; } /** * \brief Set legend type. * \param type New legend type. */ inline void setType(LEGEND_TYPE type) { m_Type = type; } /** * \brief Set legend orientation. * \param orientation New legend orientation. */ inline void setOrientation(LEGEND_ORIENTATION orientation) { m_Orientation = orientation; } /** * \brief Set legend position. * \param pos New legend position enumeration value. */ inline void setPosition(LEGEND_POSITION pos) { m_Position = pos; } /** * \brief Set floating legend position. * \param pos New floating legend position. */ inline void setFloatingPosition(QPoint pos) { m_PositionFloating = pos; } /** * \brief Set symbol size. * \param size New symbol size. */ inline void setSymbolSize(int size) { m_SymbolSize = size; } /** * \brief Set symbol description's bounding rectangle width. * \param width New bounding rectangle width. */ inline void setBRectWidth(int width) { m_BRectWidth = width; } /** * \brief Set symbol description's bounding rectangle height. * \param height New bounding rectangle height. */ inline void setBRectHeight(int height) { m_BRectHeight = height; } /** * \brief Set maximal horizontal scale size in pixels. * \param pixels New maximal horizontal scale size in pixels. */ inline void setMaxHScalePixels(int pixels) { m_MaxHScalePixels = pixels; } /** * \brief Set maximal vertical scale size in pixels. * \param pixels New maximal vertical scale size in pixels. */ inline void setMaxVScalePixels(int pixels) { m_MaxVScalePixels = pixels; } /** * \brief Set symbol X spacing in pixels. * \param spacing New symbol X spacing in pixels. */ inline void setXSymbolSpacing(int spacing) { m_XSymbolSpacing = spacing; } /** * \brief Set symbol Y spacing in pixels. * \param spacing New symbol Y spacing in pixels. */ inline void setYSymbolSpacing(int spacing) { m_YSymbolSpacing = spacing; } /** * \brief Set font. * \param font New font. */ inline void setFont(const QFont &font) { m_Font = font; } /** * \brief Set background color. * \param color New background color. */ inline void setBgColor(const QColor &color) { m_BgColor = color; } /** * \brief Set if frame is drawn. * \param draw True if frame should be drawn. */ inline void setDrawFrame(bool draw) { m_DrawFrame = draw; } /** * \brief Calculates size of legend that will be painted using current settings. * \return Size of legend. */ QSize calculateSize(); /** * \brief Paint legend on passed QPaintDevice at selected position. * \param pd QPaintDevice on which legend will be painted. */ void paintLegend(QPaintDevice *pd); /** * \brief Paint legend using passed SkyQPainter. * This method is used to enable painting on QPaintDevice subclasses that can't be * painted by multiple QPainter subclasses (e.g. QSvgGenerator). * \param painter that will be used to paint legend. * \note Passed SkyQPainter should be already set up to paint at specific QPaintDevice * subclass and should be initialized by its begin() method. After legend is painted, SkyQPainter * instance _will not_ be finished, so it's necessary to call end() method manually. */ void paintLegend(SkyQPainter *painter); /** * \brief Paint legend on passed QPaintDevice at selected position. * \param pd QPaintDevice on which legend will be painted. * \param type the legend type. * \param pos LEGEND_POSITION enum value. */ void paintLegend(QPaintDevice *pd, LEGEND_TYPE type, LEGEND_POSITION pos); /** * \brief Paint legend using passed SkyQPainter. * This method is used to enable painting on QPaintDevice subclasses that can't be painted * by multiple QPainter subclasses (eg. QSvgGenerator). * \param painter that will be used to paint legend. * \param type the legend type. * \param pos LEGEND_POSITION enum value. * \note Passed SkyQPainter should be already set up to paint at specific QPaintDevice * subclass and should be initialized by its begin() method. After legend is painted, SkyQPainter * instance _will not_ be finished, so it's necessary to call end() method manually. */ void paintLegend(SkyQPainter *painter, LEGEND_TYPE type, LEGEND_POSITION pos); private: /** * \brief Paint all symbols at passed position. * \param pos position at which symbols will be painted (upper left corner). * \note Orientation of the symbols group is determined by current legend orientation. */ void paintSymbols(QPointF pos); /** * \brief Paint single symbol with specified parameters. * \param pos position at which symbol will be painted (center). * \param type symbol type (see SkyQPainter class for symbol types list). * \param e e parameter of symbol. * \param angle angle of symbol (in degrees). * \param label symbol label. */ void paintSymbol(QPointF pos, int type, float e, float angle, QString label); /** * \brief Paint 'Star Magnitudes' group at passed position. * \param pos position at which 'Star Magnitudes' group will be painted (upper left corner). */ void paintMagnitudes(QPointF pos); /** * \brief Paint chart scale bar at passed position. * \param pos position at which chart scale bar will be painted. * \note Orientation of chart scale bar is determined by current legend orientation. Maximal * bar size is determined by current values set by setMaxHScalePixels()/setMaxVScalePixels() method. * Exact size is adjusted to full deg/min/sec. */ void paintScale(QPointF pos); /** * \brief Calculates legend position (upper left corner) based on LEGEND_POSITION enum value, paint device size and calculated legend size. * \param pos LEGEND_POSITION enum value. */ QPoint positionToDeviceCoord(QPaintDevice *pd); SkyQPainter *m_Painter { nullptr }; SkyMap *m_SkyMap { nullptr }; bool m_DeletePainter { false }; LEGEND_TYPE m_Type { LT_FULL }; LEGEND_ORIENTATION m_Orientation; LEGEND_POSITION m_Position; QPoint m_PositionFloating; ColorScheme *m_cScheme { nullptr }; QFont m_Font; QColor m_BgColor; bool m_DrawFrame { false }; int m_SymbolSize { 0 }; int m_BRectWidth { 0 }; int m_BRectHeight { 0 }; int m_MaxHScalePixels { 0 }; int m_MaxVScalePixels { 0 }; int m_XSymbolSpacing { 0 }; int m_YSymbolSpacing { 0 }; }; diff --git a/kstars/time/kstarsdatetime.cpp b/kstars/time/kstarsdatetime.cpp index 60f553b65..298035e1a 100644 --- a/kstars/time/kstarsdatetime.cpp +++ b/kstars/time/kstarsdatetime.cpp @@ -1,313 +1,319 @@ /*************************************************************************** kstarsdatetime.cpp - K Desktop Planetarium ------------------- begin : Tue 05 May 2004 copyright : (C) 2004 by Jason Harris email : jharris@30doradus.org ***************************************************************************/ /*************************************************************************** * * * 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. * * * ***************************************************************************/ #include "kstarsdatetime.h" #include "dms.h" #include "ksnumbers.h" #include #include KStarsDateTime::KStarsDateTime() : QDateTime() { setTimeSpec(Qt::UTC); setDJD(J2000); } KStarsDateTime::KStarsDateTime(const KStarsDateTime &kdt) : QDateTime() +{ + *this = kdt; +} + +KStarsDateTime& KStarsDateTime::operator=(const KStarsDateTime &kdt) noexcept { setDJD(kdt.djd()); setTimeSpec(kdt.timeSpec()); //utcoffset deprecated //setUtcOffset(kdt.utcOffset()); + return *this; } /*KStarsDateTime::KStarsDateTime( const QDateTime &kdt ) : QDateTime( kdt ) { //don't call setDJD() because we don't need to compute the time; just set DJD directly QTime _t = kdt.time(); QDate _d = kdt.date(); long double jdFrac = ( _t.hour()-12 + ( _t.minute() + ( _t.second() + _t.msec()/1000.)/60.)/60.)/24.; DJD = static_cast( _d.toJulianDay() ) + jdFrac; }*/ KStarsDateTime::KStarsDateTime(const QDateTime &qdt) : QDateTime(qdt) //, QDateTime::Spec::UTC() ) { // FIXME: This method might be buggy. Need to write some tests -- asimha (Oct 2016) QTime _t = qdt.time(); QDate _d = qdt.date(); long double jdFrac = (_t.hour() - 12 + (_t.minute() + (_t.second() + _t.msec() / 1000.) / 60.) / 60.) / 24.; DJD = static_cast(_d.toJulianDay()) + jdFrac; setTimeSpec(qdt.timeSpec()); //setUtcOffset(qdt.utcOffset()); } KStarsDateTime::KStarsDateTime(const QDate &_d, const QTime &_t, Qt::TimeSpec timeSpec) : //QDateTime( _d, _t, QDateTime::Spec::UTC() ) QDateTime(_d, _t, timeSpec) { //don't call setDJD() because we don't need to compute the time; just set DJD directly long double jdFrac = (_t.hour() - 12 + (_t.minute() + (_t.second() + _t.msec() / 1000.) / 60.) / 60.) / 24.; DJD = static_cast(_d.toJulianDay()) + jdFrac; } KStarsDateTime::KStarsDateTime(long double _jd) : QDateTime() { setTimeSpec(Qt::UTC); setDJD(_jd); } //KStarsDateTime KStarsDateTime::currentDateTime( QDateTime::Spec spec ) { KStarsDateTime KStarsDateTime::currentDateTime() { KStarsDateTime dt(QDateTime::currentDateTime()); // if ( dt.time().hour()==0 && dt.time().minute()==0 ) // midnight or right after? // dt.setDate( QDateTime::currentDateTime(spec).date() ); // fetch date again return dt; } KStarsDateTime KStarsDateTime::currentDateTimeUtc() { KStarsDateTime dt(QDateTime::currentDateTimeUtc()); //if ( dt.time().hour()==0 && dt.time().minute()==0 ) // midnight or right after? // dt.setDate( QDateTime::currentDateTime(spec).date() ); // fetch date again return dt; } KStarsDateTime KStarsDateTime::fromString(const QString &s) { //DEBUG qCDebug(KSTARS) << "Date string: " << s; KStarsDateTime dtResult(QDateTime::fromString(s, Qt::TextDate)); if (dtResult.isValid()) return dtResult; dtResult = KStarsDateTime(QDateTime::fromString(s, Qt::ISODate)); if (dtResult.isValid()) return dtResult; //dtResult = QDateTime::fromString( s, QDateTime::RFCDate ); dtResult = KStarsDateTime(QDateTime::fromString(s, Qt::RFC2822Date)); if (dtResult.isValid()) return dtResult; qCWarning(KSTARS) << "Could not parse Date/Time string:" << s; qCWarning(KSTARS) << "Valid date formats:"; qCWarning(KSTARS) << " 1950-02-25 ; 1950-02-25T05:30:00"; qCWarning(KSTARS) << " 25 Feb 1950 ; 25 Feb 1950 05:30:00"; qCWarning(KSTARS) << " Sat Feb 25 1950 ; Sat Feb 25 05:30:00 1950"; return KStarsDateTime(QDateTime()); //invalid } void KStarsDateTime::setDJD(long double _jd) { //QDateTime::setTimeSpec( QDateTime::Spec::UTC() ); //QDateTime::setTimeSpec(Qt::UTC); DJD = _jd; long int ijd = (long int)_jd; double dayfrac = _jd - (double)ijd + 0.5; if (dayfrac > 1.0) { ijd++; dayfrac -= 1.0; } QDate dd = QDate::fromJulianDay(ijd); QDateTime::setDate(dd); double hour = 24. * dayfrac; int h = int(hour); int m = int(60. * (hour - h)); int s = int(60. * (60. * (hour - h) - m)); int ms = int(1000. * (60. * (60. * (hour - h) - m) - s)); QDateTime::setTime(QTime(h, m, s, ms)); } void KStarsDateTime::setDate(const QDate &_d) { //Save the JD fraction long double jdFrac = djd() - static_cast(date().toJulianDay()); //set the integer portion of the JD and add back the JD fraction: setDJD(static_cast(_d.toJulianDay()) + jdFrac); } KStarsDateTime KStarsDateTime::addSecs(double s) const { long double ds = static_cast(s) / 86400.; KStarsDateTime kdt(djd() + ds); kdt.setTimeSpec(timeSpec()); return kdt; } void KStarsDateTime::setTime(const QTime &_t) { KStarsDateTime _dt(date(), _t, timeSpec()); setDJD(_dt.djd()); } dms KStarsDateTime::gst() const { dms gst0 = GSTat0hUT(); double hr = double(time().hour() - offsetFromUtc() / 3600.0); double mn = double(time().minute()); double sc = double(time().second()) + double(0.001 * time().msec()); double st = (hr + (mn + sc / 60.0) / 60.0) * SIDEREALSECOND; dms gst = dms(gst0.Degrees() + st * 15.0).reduce(); return gst; } dms KStarsDateTime::GSTat0hUT() const { double sinOb, cosOb; // Mean greenwich sidereal time KStarsDateTime t0(date(), QTime(0, 0, 0)); long double s = t0.djd() - J2000; double t = s / 36525.0; double t1 = 6.697374558 + 2400.051336 * t + 0.000025862 * t * t + 0.000000002 * t * t * t; // To obtain the apparent sidereal time, we have to correct the // mean greenwich sidereal time with nutation in longitude multiplied // by the cosine of the obliquity of the ecliptic. This correction // is called nutation in right ascention, and may amount to 0.3 secs. KSNumbers num(t0.djd()); num.obliquity()->SinCos(sinOb, cosOb); // nutLong has to be in hours of time since t1 is hours of time. double nutLong = num.dEcLong() * cosOb / 15.0; t1 += nutLong; dms gst; gst.setH(t1); return gst.reduce(); } QTime KStarsDateTime::GSTtoUT(dms GST) const { dms gst0 = GSTat0hUT(); //dt is the number of sidereal hours since UT 0h. double dt = GST.Hours() - gst0.Hours(); while (dt < 0.0) dt += 24.0; while (dt >= 24.0) dt -= 24.0; //convert to solar time. dt is now the number of hours since 0h UT. dt /= SIDEREALSECOND; int hr = int(dt); int mn = int(60.0 * (dt - double(hr))); int sc = int(60.0 * (60.0 * (dt - double(hr)) - double(mn))); int ms = int(1000.0 * (60.0 * (60.0 * (dt - double(hr)) - double(mn)) - double(sc))); return (QTime(hr, mn, sc, ms)); } void KStarsDateTime::setFromEpoch(double epoch) { if (epoch == 1950.0) // Assume Besselian setFromEpoch(epoch, BESSELIAN); else setFromEpoch(epoch, JULIAN); // Assume Julian } bool KStarsDateTime::setFromEpoch(double epoch, EpochType type) { if (type != JULIAN && type != BESSELIAN) return false; else setDJD(epochToJd(epoch, type)); return true; } bool KStarsDateTime::setFromEpoch(const QString &eName) { bool result; double epoch; epoch = stringToEpoch(eName, result); if (!result) return false; return setFromEpoch(epoch, JULIAN); // We've already converted } long double KStarsDateTime::epochToJd(double epoch, EpochType type) { switch (type) { case BESSELIAN: return B1900 + (epoch - 1900.0) * JD_PER_BYEAR; case JULIAN: return J2000 + (epoch - 2000.0) * 365.25; default: return NaN::d; } } double KStarsDateTime::jdToEpoch(long double jd, KStarsDateTime::EpochType type) { // Definitions for conversion formulas are from: // // * http://scienceworld.wolfram.com/astronomy/BesselianEpoch.html // * http://scienceworld.wolfram.com/astronomy/JulianEpoch.html // switch (type) { case KStarsDateTime::BESSELIAN: return 1900.0 + (jd - KStarsDateTime::B1900) / KStarsDateTime::JD_PER_BYEAR; case KStarsDateTime::JULIAN: return 2000.0 + (jd - J2000) / 365.24; default: return NaN::d; } } double KStarsDateTime::stringToEpoch(const QString &eName, bool &ok) { double epoch = J2000; ok = false; if (eName.isEmpty()) // By default, assume J2000 return epoch; if (eName.startsWith('J')) epoch = eName.midRef(1).toDouble(&ok); else if (eName.startsWith('B')) { epoch = eName.midRef(1).toDouble(&ok); epoch = jdToEpoch(epochToJd(epoch, BESSELIAN), JULIAN); // Convert Besselian epoch to Julian epoch } // Assume it's Julian else epoch = eName.toDouble(&ok); return epoch; } diff --git a/kstars/time/kstarsdatetime.h b/kstars/time/kstarsdatetime.h index 835c9d6c2..508ea76a6 100644 --- a/kstars/time/kstarsdatetime.h +++ b/kstars/time/kstarsdatetime.h @@ -1,266 +1,269 @@ /*************************************************************************** kstarsdatetime.h - K Desktop Planetarium ------------------- begin : Tue 05 May 2004 copyright : (C) 2001 by Jason Harris email : jharris@30doradus.org ***************************************************************************/ /*************************************************************************** * * * 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. * * * ***************************************************************************/ #pragma once #include #define J2000 2451545.0 //Julian Date for noon on Jan 1, 2000 (epoch J2000) #define B1950 2433282.4235 // Julian date for Jan 0.9235, 1950 #define SIDEREALSECOND 1.002737909 //number of sidereal seconds in one solar second class dms; /** @class KStarsDateTime *@short Extension of QDateTime for KStars *KStarsDateTime can represent the date/time as a Julian Day, using a long double, *in which the fractional portion encodes the time of day to a precision of a less than a second. *Also adds Greenwich Sidereal Time and "epoch", which is just the date expressed as a floating *point number representing the year, with the fractional part representing the date and time *(with poor time resolution; typically the Epoch is only taken to the hundredths place, which is *a few days). *@note Local time and Local sideral time are not handled here. Because they depend on the *geographic location, they are part of the GeoLocation class. *@note The default timespec is UTC unless the passed value has different timespec value. *@sa GeoLocation::GSTtoLST() *@sa GeoLocation::UTtoLT() *@author Jason Harris *@author Jasem Mutlaq *@version 1.1 */ class KStarsDateTime : public QDateTime { public: /** *@short Default constructor *Creates a date/time at J2000 (noon on Jan 1, 200) *@note This sets the timespec to UTC. */ KStarsDateTime(); /** *@short Constructor *Creates a date/time at the specified Julian Day. *@p jd The Julian Day *@note This sets the timespec to UTC. */ explicit KStarsDateTime(long double djd); /** *@short Copy constructor *@p kdt The KStarsDateTime object to copy. *@note The timespec is copied from kdt. */ + /** @{ */ KStarsDateTime(const KStarsDateTime &kdt); + KStarsDateTime& operator=(const KStarsDateTime &kdt) noexcept; + /** @} */ /** *@short Copy constructor *@p qdt The QDateTime object to copy. *@note The timespec is copied from qdt. */ explicit KStarsDateTime(const QDateTime &qdt); /** *@short Constructor *Create a KStarsDateTimne based on the specified Date and Time. *@p _d The QDate to assign *@p _t The QTime to assign *@p timespec The desired timespec, UTC by default. */ KStarsDateTime(const QDate &_d, const QTime &_t, Qt::TimeSpec timeSpec = Qt::UTC); /** *Assign the static_cast Julian Day value, which includes the time of day *encoded in the fractional portion. *@p jd the Julian Day value to assign. */ void setDJD(long double jd); /** *Assign the Date according to a QDate object. *@p d the QDate to assign */ void setDate(const QDate &d); /** *Assign the Time according to a QTime object. *@p t the QTime to assign *@note timespec is NOT changed even if the passed QTime has a different timespec than current. */ void setTime(const QTime &t); /** *@return a KStarsDateTime that is the given number of seconds later *than this KStarsDateTime. *@p s the number of seconds to add. The number can be negative. */ KStarsDateTime addSecs(double s) const; /** *Modify the Date/Time by adding a number of days. *@p nd the number of days to add. The number can be negative. */ inline KStarsDateTime addDays(int nd) const { return KStarsDateTime(djd() + static_cast(nd)); } inline bool operator==(const KStarsDateTime &d) const { return DJD == d.djd(); } inline bool operator!=(const KStarsDateTime &d) const { return DJD != d.djd(); } inline bool operator<(const KStarsDateTime &d) const { return DJD < d.djd(); } inline bool operator<=(const KStarsDateTime &d) const { return DJD <= d.djd(); } inline bool operator>(const KStarsDateTime &d) const { return DJD > d.djd(); } inline bool operator>=(const KStarsDateTime &d) const { return DJD >= d.djd(); } /** *@return the date and time according to the CPU clock */ static KStarsDateTime currentDateTime(); /** *@return the UTC date and time according to the CPU clock */ static KStarsDateTime currentDateTimeUtc(); /** *@return a KStarsDateTime object parsed from the given string. *@note This function is format-agnostic; it will try several formats *when parsing the string. *@param s the string expressing the date/time to be parsed. */ static KStarsDateTime fromString(const QString &s); /** *@return the julian day as a long double, including the time as the fractional portion. */ inline long double djd() const { return DJD; } /** *@return The Greenwich Sidereal Time *The Greenwich sidereal time is the Right Ascension coordinate that is currently transiting *the Prime Meridian at the Royal Observatory in Greenwich, UK (longitude=0.0) */ dms gst() const; /** *Convert a given Greenwich Sidereal Time to Universal Time (=Greenwich Mean Time). *@p GST the Greenwich Sidereal Time to convert to Universal Time. */ QTime GSTtoUT(dms GST) const; // FIXME: Shouldn't this be static? /** *@enum EpochType description options *@note After 1976, the IAU standard for epochs is Julian Years. */ enum EpochType { JULIAN, /**< Julian epoch (see http://scienceworld.wolfram.com/astronomy/JulianEpoch.html) */ BESSELIAN, /**< Besselian epoch (see http://scienceworld.wolfram.com/astronomy/BesselianEpoch.html) */ }; /** *@return the (Julian) epoch value of the Date/Time. *@short This is (approximately) the year expressed as a floating-point value *@sa setFromEpoch() *@note The definition of Julian Epoch used here comes from http://scienceworld.wolfram.com/astronomy/JulianEpoch.html */ inline double epoch() const { return 2000.0 + (djd() - J2000) / 365.25; } /** *Set the Date/Time from an epoch value, represented as a double. *@p e the epoch value *@sa epoch() */ bool setFromEpoch(double e, EpochType type); /** *Set the Date/Time from an epoch value, represented as a string. *@p e the epoch value *@return true if date set successfully *@sa epoch() */ bool setFromEpoch(const QString &e); /** *Set the Date/Time from an epoch value, represented as a double. *@p e the epoch value *@note This method assumes that the epoch 1950.0 is Besselian, otherwise assumes that the epoch is a Julian epoch. This is provided for backward compatibility, and because custom catalogs may still use 1950.0 to mean B1950.0 despite the IAU standard for epochs being Julian. *@sa epoch() */ void setFromEpoch(double e); /** *@short Takes in an epoch and returns a Julian Date *@return the Julian Date (date with fraction) *@param epoch A floating-point year value specifying the Epoch *@param type JULIAN or BESSELIAN depending on what convention the epoch is specified in */ static long double epochToJd(double epoch, EpochType type = JULIAN); /** *@short Takes in a Julian Date and returns the corresponding epoch year in the given system *@return the epoch as a floating-point year value *@param jd Julian date *@param type Epoch system (KStarsDateTime::JULIAN or KStarsDateTime::BESSELIAN) */ static double jdToEpoch(long double jd, EpochType type = JULIAN); /** *@short Takes in a string and returns a Julian epoch */ static double stringToEpoch(const QString &eName, bool &ok); /** * The following values were obtained from Eric Weisstein's world of science: * http://scienceworld.wolfram.com/astronomy/BesselianEpoch.html */ constexpr static const double B1900 = 2415020.31352; // Julian date of B1900 epoch constexpr static const double JD_PER_BYEAR = 365.242198781; // Julian days in a Besselian year private: /** *@return the Greenwich Sidereal Time at 0h UT on this object's Date *@note used internally by gst() and GSTtoUT() */ dms GSTat0hUT() const; long double DJD { 0 }; }; diff --git a/kstars/tools/modcalcvizequinox.cpp b/kstars/tools/modcalcvizequinox.cpp index dc1a90517..503301e4e 100644 --- a/kstars/tools/modcalcvizequinox.cpp +++ b/kstars/tools/modcalcvizequinox.cpp @@ -1,425 +1,425 @@ /*************************************************************************** modcalcvizequinox.cpp - description ------------------- begin : Thu 22 Feb 2007 copyright : (C) 2007 by Jason Harris email : kstars@30doradus.org ***************************************************************************/ /*************************************************************************** * * * 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. * * * ***************************************************************************/ #include "modcalcvizequinox.h" #include "dms.h" #include "kstarsdata.h" #include "ksnotification.h" #include "skyobjects/kssun.h" #include "widgets/dmsbox.h" #include #include #include #include #include modCalcEquinox::modCalcEquinox(QWidget *parentSplit) : QFrame(parentSplit), dSpring(), dSummer(), dAutumn(), dWinter() { setupUi(this); connect(Year, SIGNAL(valueChanged(int)), this, SLOT(slotCompute())); connect(InputFileBatch, SIGNAL(urlSelected(QUrl)), this, SLOT(slotCheckFiles())); connect(OutputFileBatch, SIGNAL(urlSelected(QUrl)), this, SLOT(slotCheckFiles())); connect(RunButtonBatch, SIGNAL(clicked()), this, SLOT(slotRunBatch())); connect(ViewButtonBatch, SIGNAL(clicked()), this, SLOT(slotViewBatch())); Plot->axis(KPlotWidget::LeftAxis)->setLabel(i18n("Sun's Declination")); Plot->setTopPadding(40); //Don't draw Top & Bottom axes; custom axes drawn as plot objects Plot->axis(KPlotWidget::BottomAxis)->setVisible(false); Plot->axis(KPlotWidget::TopAxis)->setVisible(false); //This will call slotCompute(): Year->setValue(KStarsData::Instance()->lt().date().year()); RunButtonBatch->setEnabled(false); ViewButtonBatch->setEnabled(false); show(); } double modCalcEquinox::dmonth(int i) { Q_ASSERT(i >= 0 && i < 12 && "Month must be in 0 .. 11 range"); return DMonth[i]; } void modCalcEquinox::slotCheckFiles() { RunButtonBatch->setEnabled(!InputFileBatch->lineEdit()->text().isEmpty() && !OutputFileBatch->lineEdit()->text().isEmpty()); } void modCalcEquinox::slotRunBatch() { QString inputFileName = InputFileBatch->url().toLocalFile(); if (QFile::exists(inputFileName)) { QFile f(inputFileName); if (!f.open(QIODevice::ReadOnly)) { KSNotification::sorry(i18n("Could not open file %1.", f.fileName()), i18n("Could Not Open File")); inputFileName.clear(); return; } QTextStream istream(&f); processLines(istream); ViewButtonBatch->setEnabled(true); f.close(); } else { KSNotification::sorry(i18n("Invalid file: %1", inputFileName), i18n("Invalid file")); inputFileName.clear(); return; } } void modCalcEquinox::processLines(QTextStream &istream) { QFile fOut(OutputFileBatch->url().toLocalFile()); fOut.open(QIODevice::WriteOnly); QTextStream ostream(&fOut); int originalYear = Year->value(); //Write header to output file ostream << i18n("# Timing of Equinoxes and Solstices\n") << i18n("# computed by KStars\n#\n") << i18n("# Vernal Equinox\t\tSummer Solstice\t\t\tAutumnal Equinox\t\tWinter Solstice\n#\n"); while (!istream.atEnd()) { QString line = istream.readLine(); bool ok = false; int year = line.toInt(&ok); //for now I will simply change the value of the Year widget to trigger //computation of the Equinoxes and Solstices. if (ok) { //triggers slotCompute(), which sets values of dSpring et al.: Year->setValue(year); //Write to output file ostream << QLocale().toString(dSpring.date(), QLocale::LongFormat) << "\t" << QLocale().toString(dSummer.date(), QLocale::LongFormat) << "\t" << QLocale().toString(dAutumn.date(), QLocale::LongFormat) << "\t" << QLocale().toString(dWinter.date(), QLocale::LongFormat) << '\n'; } } if (Year->value() != originalYear) Year->setValue(originalYear); } void modCalcEquinox::slotViewBatch() { QFile fOut(OutputFileBatch->url().toLocalFile()); fOut.open(QIODevice::ReadOnly); QTextStream istream(&fOut); QStringList text; while (!istream.atEnd()) text.append(istream.readLine()); fOut.close(); KMessageBox::informationList(nullptr, i18n("Results of Sidereal time calculation"), text, OutputFileBatch->url().toLocalFile()); } void modCalcEquinox::slotCompute() { KStarsData *data = KStarsData::Instance(); KSSun Sun; int year0 = Year->value(); KStarsDateTime dt(QDate(year0, 1, 1), QTime(0, 0, 0)); long double jd0 = dt.djd(); //save JD on Jan 1st for (int imonth = 0; imonth < 12; imonth++) { KStarsDateTime kdt(QDate(year0, imonth + 1, 1), QTime(0, 0, 0)); DMonth[imonth] = kdt.djd() - jd0; } Plot->removeAllPlotObjects(); //Add the celestial equator, just a single line bisecting the plot horizontally KPlotObject *ce = new KPlotObject(data->colorScheme()->colorNamed("EqColor"), KPlotObject::Lines, 2.0); ce->addPoint(0.0, 0.0); ce->addPoint(366.0, 0.0); Plot->addPlotObject(ce); // Tropic of cancer KPlotObject *tcan = new KPlotObject(data->colorScheme()->colorNamed("EqColor"), KPlotObject::Lines, 2.0); tcan->addPoint(0.0, 23.5); tcan->addPoint(366.0, 23.5); Plot->addPlotObject(tcan); // Tropic of capricorn KPlotObject *tcap = new KPlotObject(data->colorScheme()->colorNamed("EqColor"), KPlotObject::Lines, 2.0); tcap->addPoint(0.0, -23.5); tcap->addPoint(366.0, -23.5); Plot->addPlotObject(tcap); //Add Ecliptic. This is more complicated than simply incrementing the //ecliptic longitude, because we want the x-axis to be time, not RA. //For each day in the year, compute the Sun's position. KPlotObject *ecl = new KPlotObject(data->colorScheme()->colorNamed("EclColor"), KPlotObject::Lines, 2); ecl->setLinePen(QPen(ecl->pen().color(), 4)); Plot->setLimits(1.0, double(dt.date().daysInYear()), -30.0, 30.0); //Add top and bottom axis lines, and custom tickmarks at each month addDateAxes(); for (int i = 1; i <= dt.date().daysInYear(); i++) { KSNumbers num(dt.djd()); Sun.findPosition(&num); ecl->addPoint(double(i), Sun.dec().Degrees()); dt = dt.addDays(1); } Plot->addPlotObject(ecl); // Calculate dates for all solstices and equinoxes findSolsticeAndEquinox(Year->value()); //Display the Date/Time of each event in the text fields VEquinox->setText(QLocale().toString(dSpring, QLocale::LongFormat)); SSolstice->setText(QLocale().toString(dSummer, QLocale::LongFormat)); AEquinox->setText(QLocale().toString(dAutumn, QLocale::LongFormat)); WSolstice->setText(QLocale().toString(dWinter, QLocale::LongFormat)); //Add vertical dotted lines at times of the equinoxes and solstices KPlotObject *poSpring = new KPlotObject(Qt::white, KPlotObject::Lines, 1); poSpring->setLinePen(QPen(Qt::white, 1.0, Qt::DotLine)); poSpring->addPoint(dSpring.djd() - jd0, Plot->dataRect().top()); poSpring->addPoint(dSpring.djd() - jd0, Plot->dataRect().bottom()); Plot->addPlotObject(poSpring); KPlotObject *poSummer = new KPlotObject(Qt::white, KPlotObject::Lines, 1); poSummer->setLinePen(QPen(Qt::white, 1.0, Qt::DotLine)); poSummer->addPoint(dSummer.djd() - jd0, Plot->dataRect().top()); poSummer->addPoint(dSummer.djd() - jd0, Plot->dataRect().bottom()); Plot->addPlotObject(poSummer); KPlotObject *poAutumn = new KPlotObject(Qt::white, KPlotObject::Lines, 1); poAutumn->setLinePen(QPen(Qt::white, 1.0, Qt::DotLine)); poAutumn->addPoint(dAutumn.djd() - jd0, Plot->dataRect().top()); poAutumn->addPoint(dAutumn.djd() - jd0, Plot->dataRect().bottom()); Plot->addPlotObject(poAutumn); KPlotObject *poWinter = new KPlotObject(Qt::white, KPlotObject::Lines, 1); poWinter->setLinePen(QPen(Qt::white, 1.0, Qt::DotLine)); poWinter->addPoint(dWinter.djd() - jd0, Plot->dataRect().top()); poWinter->addPoint(dWinter.djd() - jd0, Plot->dataRect().bottom()); Plot->addPlotObject(poWinter); } //Add custom top/bottom axes with tickmarks for each month void modCalcEquinox::addDateAxes() { KPlotObject *poTopAxis = new KPlotObject(Qt::white, KPlotObject::Lines, 1); poTopAxis->addPoint(0.0, Plot->dataRect().bottom()); //y-axis is reversed! poTopAxis->addPoint(366.0, Plot->dataRect().bottom()); Plot->addPlotObject(poTopAxis); KPlotObject *poBottomAxis = new KPlotObject(Qt::white, KPlotObject::Lines, 1); poBottomAxis->addPoint(0.0, Plot->dataRect().top() + 0.02); poBottomAxis->addPoint(366.0, Plot->dataRect().top() + 0.02); Plot->addPlotObject(poBottomAxis); //Tick mark for each month for (int imonth = 0; imonth < 12; imonth++) { KPlotObject *poMonth = new KPlotObject(Qt::white, KPlotObject::Lines, 1); poMonth->addPoint(dmonth(imonth), Plot->dataRect().top()); poMonth->addPoint(dmonth(imonth), Plot->dataRect().top() + 1.4); Plot->addPlotObject(poMonth); poMonth = new KPlotObject(Qt::white, KPlotObject::Lines, 1); poMonth->addPoint(dmonth(imonth), Plot->dataRect().bottom()); poMonth->addPoint(dmonth(imonth), Plot->dataRect().bottom() - 1.4); Plot->addPlotObject(poMonth); } } // Calculate and store dates for all solstices and equinoxes void modCalcEquinox::findSolsticeAndEquinox(uint32_t year) { // All the magic numbers are taken from Meeus - 1991 chapter 27 qreal m, jdme[4]; if(year > 1000) { m = (year-2000.0) / 1000.0; jdme[0] = (-0.00057 * qPow(m, 4)) + (-0.00411 * qPow(m, 3)) + (0.05169 * qPow(m, 2)) + (365242.37404 * m) + 2451623.80984; jdme[1] = (-0.0003 * qPow(m, 4)) + (0.00888 * qPow(m, 3)) + (0.00325 * qPow(m, 2)) + (365241.62603 * m) + 2451716.56767; jdme[2] = (0.00078 * qPow(m, 4)) + (0.00337 * qPow(m, 3)) + (-0.11575 * qPow(m, 2)) + (365242.01767 * m) + 2451810.21715; jdme[3] = (0.00032 * qPow(m, 4)) + (-0.00823 * qPow(m, 3)) + (-0.06223 * qPow(m, 2)) + (365242.74049 * m) + 2451900.05952; } else { m = year / 1000.0; jdme[0] = (-0.00071 * qPow(m, 4)) + (0.00111 * qPow(m, 3)) + (0.06134 * qPow(m, 2)) + (365242.13740 * m) + 1721139.29189; jdme[1] = (0.00025 * qPow(m, 4)) + (0.00907 * qPow(m, 3)) + (-0.05323 * qPow(m, 2)) + (365241.72562 * m) + 1721233.25401; jdme[2] = (0.00074 * qPow(m, 4)) + (-0.00297 * qPow(m, 3)) + (-0.11677 * qPow(m, 2)) + (365242.49558 * m) + 1721325.70455; jdme[3] = (-0.00006 * qPow(m, 4)) + (-0.00933 * qPow(m, 3)) + (-0.00769 * qPow(m, 2)) + (365242.88257 * m) + 1721414.39987; } qreal t[4]; for(int i = 0; i < 4; i++) { t[i] = (jdme[i] - 2451545)/36525; } qreal a[4][24] = {{485, 203, 199, 182, 156, 136, 77, 74, 70, 58, 52, 50, 45, 44, 29, 18, 17, 16, 14, 12, 12, 12, 9, 8}, {485, 203, 199, 182, 156, 136, 77, 74, 70, 58, 52, 50, 45, 44, 29, 18, 17, 16, 14, 12, 12, 12, 9, 8}, {485, 203, 199, 182, 156, 136, 77, 74, 70, 58, 52, 50, 45, 44, 29, 18, 17, 16, 14, 12, 12, 12, 9, 8}, {485, 203, 199, 182, 156, 136, 77, 74, 70, 58, 52, 50, 45, 44, 29, 18, 17, 16, 14, 12, 12, 12, 9, 8}}; qreal b[4][24] = {{324.96, 337.23, 342.08, 27.85, 73.14, 171.52, 222.54, 296.72, 243.58, 119.81, 297.17, 21.02, 247.54, 325.15, 60.93, 155.12, 288.79, 198.04, 199.76, 95.39, 287.11, 320.81, 227.73, 15.45}, {324.96, 337.23, 342.08, 27.85, 73.14, 171.52, 222.54, 296.72, 243.58, 119.81, 297.17, 21.02, 247.54, 325.15, 60.93, 155.12, 288.79, 198.04, 199.76, 95.39, 287.11, 320.81, 227.73, 15.45}, {324.96, 337.23, 342.08, 27.85, 73.14, 171.52, 222.54, 296.72, 243.58, 119.81, 297.17, 21.02, 247.54, 325.15, 60.93, 155.12, 288.79, 198.04, 199.76, 95.39, 287.11, 320.81, 227.73, 15.45}, {324.96, 337.23, 342.08, 27.85, 73.14, 171.52, 222.54, 296.72, 243.58, 119.81, 297.17, 21.02, 247.54, 325.15, 60.93, 155.12, 288.79, 198.04, 199.76, 95.39, 287.11, 320.81, 227.73, 15.45}}; qreal c[] = {1934.136, 32964.467, 20.186, 445267.112, 45036.886, 22518.443, 65928.934, 3034.906, 9037.513, 33718.147, 150.678, 2281.226, 29929.562, 31555.956, 4443.417, 67555.328, 4562.452, 62894.029, 31436.921, 14577.848, 31931.756, 34777.259, 1222.114, 16859.074}; qreal d[4][24]; for (int i = 0; i < 4; i++) { for (int j = 0; j < 24; j++) { d[i][j] = c[j] * t[i]; } } for (int i = 0; i < 4; i++) { for (int j = 0; j < 24; j++) { d[i][j] = qCos(qDegreesToRadians(b[i][j] + d[i][j])); } } for (int i = 0; i < 4; i++) { for (int j = 0; j < 24; j++) { d[i][j] = d[i][j] * a[i][j]; } } qreal e[4], f[4], g[4], jd[4]; for (int i = 0; i < 4; i++) { e[i] = 0; for (int j = 0; j < 24; j++) { e[i] += d[i][j]; } } for (int i = 0; i < 4; i++) { f[i] = (35999.373*t[i]-2.47); } for (int i = 0; i < 4; i++) { g[i] = 1 + (0.0334 * qCos(qDegreesToRadians(f[i]))) + (0.0007 * qCos(qDegreesToRadians(2*f[i]))); } for (int i = 0; i < 4; i++) { jd[i] = jdme[i] + (0.00001*e[i]/g[i]); } // Get correction qreal correction = FindCorrection(year); for (int i = 0; i < 4; i++) { KStarsDateTime dt(jd[i]); // Apply correction dt = dt.addSecs(correction); switch(i) { case 0 : dSpring = dt; break; case 1 : dSummer = dt; break; case 2 : dAutumn = dt; break; case 3 : dWinter = dt; break; } } } qreal modCalcEquinox::FindCorrection(uint32_t year) { - int tblFirst = 1620, tblLast = 2002; + uint32_t tblFirst = 1620, tblLast = 2002; // Corrections taken from Meeus -1991 chapter 10 qreal tbl[] = {/*1620*/ 121,112,103, 95, 88, 82, 77, 72, 68, 63, 60, 56, 53, 51, 48, 46, 44, 42, 40, 38, /*1660*/ 35, 33, 31, 29, 26, 24, 22, 20, 18, 16, 14, 12, 11, 10, 9, 8, 7, 7, 7, 7, /*1700*/ 7, 7, 8, 8, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, /*1740*/ 11, 11, 12, 12, 12, 12, 13, 13, 13, 14, 14, 14, 14, 15, 15, 15, 15, 15, 16, 16, /*1780*/ 16, 16, 16, 16, 16, 16, 15, 15, 14, 13, /*1800*/ 13.1, 12.5, 12.2, 12.0, 12.0, 12.0, 12.0, 12.0, 12.0, 11.9, 11.6, 11.0, 10.2, 9.2, 8.2, /*1830*/ 7.1, 6.2, 5.6, 5.4, 5.3, 5.4, 5.6, 5.9, 6.2, 6.5, 6.8, 7.1, 7.3, 7.5, 7.6, /*1860*/ 7.7, 7.3, 6.2, 5.2, 2.7, 1.4, -1.2, -2.8, -3.8, -4.8, -5.5, -5.3, -5.6, -5.7, -5.9, /*1890*/ -6.0, -6.3, -6.5, -6.2, -4.7, -2.8, -0.1, 2.6, 5.3, 7.7, 10.4, 13.3, 16.0, 18.2, 20.2, /*1920*/ 21.1, 22.4, 23.5, 23.8, 24.3, 24.0, 23.9, 23.9, 23.7, 24.0, 24.3, 25.3, 26.2, 27.3, 28.2, /*1950*/ 29.1, 30.0, 30.7, 31.4, 32.2, 33.1, 34.0, 35.0, 36.5, 38.3, 40.2, 42.2, 44.5, 46.5, 48.5, /*1980*/ 50.5, 52.5, 53.8, 54.9, 55.8, 56.9, 58.3, 60.0, 61.6, 63.0, 63.8, 64.3}; qreal deltaT = 0; qreal t = (year - 2000.0)/100.0; if(year >= tblFirst && year <= tblLast) { if(year % 2) { deltaT = (tbl[(year-tblFirst-1)/2] + tbl[(year-tblFirst+1)/2]) / 2; } else { deltaT = tbl[(year-tblFirst)/2]; } } else if(year < 948) { deltaT = 2177 + 497*t + 44.1*qPow(t, 2); } else if(year >= 948) { deltaT = 102 + 102*t + 25.3*qPow(t, 2); if (year>=2000 && year <=2100) { // Special correction to avoid discontinuity in 2000 deltaT += 0.37 * ( year - 2100.0 ); } } return -deltaT; }