diff --git a/kstars/ekos/guide/guide.h b/kstars/ekos/guide/guide.h index 2db4d655c..9fb46accf 100644 --- a/kstars/ekos/guide/guide.h +++ b/kstars/ekos/guide/guide.h @@ -1,642 +1,642 @@ /* Ekos guide tool 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. */ #pragma once #include "ui_guide.h" #include "ekos/ekos.h" #include "indi/indiccd.h" #include "indi/inditelescope.h" #include #include #include class QProgressIndicator; class QTabWidget; class FITSView; class FITSViewer; class ScrollGraph; namespace Ekos { class GuideInterface; class OpsCalibration; class OpsGuide; class InternalGuider; class PHD2; class LinGuider; /** * @class Guide * @short Performs calibration and autoguiding using an ST4 port or directly via the INDI driver. Can be used with the following external guiding applications: * PHD2 * LinGuider * * @author Jasem Mutlaq * @version 1.4 */ class Guide : public QWidget, public Ui::Guide { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.kstars.Ekos.Guide") Q_PROPERTY(Ekos::GuideState status READ status NOTIFY newStatus) Q_PROPERTY(QStringList logText READ logText NOTIFY newLog) Q_PROPERTY(QString camera READ camera WRITE setCamera) Q_PROPERTY(QString st4 READ st4 WRITE setST4) Q_PROPERTY(double exposure READ exposure WRITE setExposure) Q_PROPERTY(QList axisDelta READ axisDelta NOTIFY newAxisDelta) Q_PROPERTY(QList axisSigma READ axisSigma NOTIFY newAxisSigma) public: Guide(); ~Guide(); enum GuiderStage { CALIBRATION_STAGE, GUIDE_STAGE }; enum GuiderType { GUIDE_INTERNAL, GUIDE_PHD2, GUIDE_LINGUIDER }; /** @defgroup GuideDBusInterface Ekos DBus Interface - Capture Module * Ekos::Guide interface provides advanced scripting capabilities to calibrate and guide a mount via a CCD camera. */ /*@{*/ /** DBUS interface function. * select the CCD device from the available CCD drivers. * @param device The CCD device name * @return Returns true if CCD device is found and set, false otherwise. */ Q_SCRIPTABLE bool setCamera(const QString &device); Q_SCRIPTABLE QString camera(); /** DBUS interface function. * select the ST4 device from the available ST4 drivers. * @param device The ST4 device name * @return Returns true if ST4 device is found and set, false otherwise. */ Q_SCRIPTABLE bool setST4(const QString &device); Q_SCRIPTABLE QString st4(); /** DBUS interface function. * @return Returns List of registered ST4 devices. */ Q_SCRIPTABLE QStringList getST4Devices(); /** DBUS interface function. * @brief connectGuider Establish connection to guider application. For internal guider, this always returns true. * @return True if successfully connected, false otherwise. */ Q_SCRIPTABLE bool connectGuider(); /** DBUS interface function. * @brief disconnectGuider Disconnect from guider application. For internal guider, this always returns true. * @return True if successfully disconnected, false otherwise. */ Q_SCRIPTABLE bool disconnectGuider(); /** * @brief getStatus Return guide module status * @return state of guide module from Ekos::GuideState */ Q_SCRIPTABLE Ekos::GuideState status() { return state; } /** DBUS interface function. * Set CCD exposure value * @param value exposure value in seconds. */ Q_SCRIPTABLE Q_NOREPLY void setExposure(double value); double exposure() { return exposureIN->value(); } /** DBUS interface function. * Set image filter to apply to the image after capture. * @param value Image filter (Auto Stretch, High Contrast, Equalize, High Pass) */ Q_SCRIPTABLE Q_NOREPLY void setImageFilter(const QString &value); /** DBUS interface function. * Set calibration Use Two Axis option. The options must be set before starting the calibration operation. If no options are set, the options loaded from the user configuration are used. * @param enable if true, calibration will be performed in both RA and DEC axis. Otherwise, only RA axis will be calibrated. */ Q_SCRIPTABLE Q_NOREPLY void setCalibrationTwoAxis(bool enable); /** DBUS interface function. * Set auto star calibration option. The options must be set before starting the calibration operation. If no options are set, the options loaded from the user configuration are used. * @param enable if true, Ekos will attempt to automatically select the best guide star and proceed with the calibration procedure. */ Q_SCRIPTABLE Q_NOREPLY void setCalibrationAutoStar(bool enable); /** DBUS interface function. * In case of automatic star selection, calculate the appropriate square size given the selected star width. The options must be set before starting the calibration operation. If no options are set, the options loaded from the user configuration are used. * @param enable if true, Ekos will attempt to automatically select the best square size for calibration and guiding phases. */ Q_SCRIPTABLE Q_NOREPLY void setCalibrationAutoSquareSize(bool enable); /** DBUS interface function. * Set calibration dark frame option. The options must be set before starting the calibration operation. If no options are set, the options loaded from the user configuration are used. * @param enable if true, a dark frame will be captured to subtract from the light frame. */ Q_SCRIPTABLE Q_NOREPLY void setDarkFrameEnabled(bool enable); /** DBUS interface function. * Set calibration parameters. * @param pulseDuration Pulse duration in milliseconds to use in the calibration steps. */ Q_SCRIPTABLE Q_NOREPLY void setCalibrationPulseDuration(int pulseDuration); /** DBUS interface function. * Set guiding box size. The options must be set before starting the guiding operation. If no options are set, the options loaded from the user configuration are used. * @param index box size index (0 to 4) for box size from 8 to 128 pixels. The box size should be suitable for the size of the guide star selected. The boxSize is also used to select the subframe size around the guide star. Default is 16 pixels */ Q_SCRIPTABLE Q_NOREPLY void setGuideBoxSizeIndex(int index); /** DBUS interface function. * Set guiding algorithm. The options must be set before starting the guiding operation. If no options are set, the options loaded from the user configuration are used. * @param index Select the algorithm used to calculate the centroid of the guide star (0 --> Smart, 1 --> Fast, 2 --> Auto, 3 --> No thresh). */ Q_SCRIPTABLE Q_NOREPLY void setGuideAlgorithmIndex(int index); /** DBUS interface function. * Set rapid guiding option. The options must be set before starting the guiding operation. If no options are set, the options loaded from the user configuration are used. * @param enable if true, it will activate RapidGuide in the CCD driver. When Rapid Guide is used, no frames are sent to Ekos for analysis and the centeroid calculations are done in the CCD driver. */ //Q_SCRIPTABLE Q_NOREPLY void setGuideRapidEnabled(bool enable); /** DBUS interface function. * Enable or disables dithering. Set dither range * @param enable if true, dithering is enabled and is performed after each exposure is complete. Otherwise, dithering is disabled. * @param value dithering range in pixels. Ekos will move the guide star in a random direction for the specified dithering value in pixels. */ Q_SCRIPTABLE Q_NOREPLY void setDitherSettings(bool enable, double value); /** @}*/ void addCamera(ISD::GDInterface *newCCD); void configurePHD2Camera(); void setTelescope(ISD::GDInterface *newTelescope); void addST4(ISD::ST4 *setST4); void setAO(ISD::ST4 *newAO); void removeDevice(ISD::GDInterface *device); bool isDithering(); void addGuideHead(ISD::GDInterface *newCCD); void syncTelescopeInfo(); void syncCCDInfo(); /** * @brief clearLog As the name suggests */ void clearLog(); QStringList logText() { return m_LogText; } /** * @return Return current log text of guide module */ QString getLogText() { return m_LogText.join("\n"); } /** * @brief getStarPosition Return star center as selected by the user or auto-detected by KStars * @return QVector3D of starCenter. The 3rd parameter is used to store current bin settings and in unrelated to the star position. */ QVector3D getStarPosition() { return starCenter; } // Tracking Box int getTrackingBoxSize() { return boxSizeCombo->currentText().toInt(); } //void startRapidGuide(); //void stopRapidGuide(); GuideInterface *getGuider() { return guider; } public slots: /** DBUS interface function. * Start the autoguiding operation. * @return Returns true if guiding started successfully, false otherwise. */ Q_SCRIPTABLE bool guide(); /** DBUS interface function. * Stop any active calibration, guiding, or dithering operation * @return Returns true if operation is stopped successfully, false otherwise. */ Q_SCRIPTABLE bool abort(); /** DBUS interface function. * Start the calibration operation. Note that this will not start guiding automatically. * @return Returns true if calibration started successfully, false otherwise. */ Q_SCRIPTABLE bool calibrate(); /** DBUS interface function. * Clear calibration data. Next time any guide operation is performed, a calibration is first started. */ Q_SCRIPTABLE Q_NOREPLY void clearCalibration(); /** DBUS interface function. * @brief dither Starts dithering process in a random direction restricted by the number of pixels specified in dither options * @return True if dither started successfully, false otherwise. */ Q_SCRIPTABLE bool dither(); /** DBUS interface function. * @brief suspend Suspend autoguiding * @return True if successful, false otherwise. */ Q_SCRIPTABLE bool suspend(); /** DBUS interface function. * @brief resume Resume autoguiding * @return True if successful, false otherwise. */ Q_SCRIPTABLE bool resume(); /** DBUS interface function. * Capture a guide frame * @return Returns true if capture command is sent successfully to INDI server. */ Q_SCRIPTABLE bool capture(); /** DBUS interface function. * Set guiding options. The options must be set before starting the guiding operation. If no options are set, the options loaded from the user configuration are used. * @param enable if true, it will select a subframe around the guide star depending on the boxSize size. */ Q_SCRIPTABLE Q_NOREPLY void setSubFrameEnabled(bool enable); /** DBUS interface function. * Selects which guiding process to utilize for calibration & guiding. * @param type Type of guider process to use. 0 for internal guider, 1 for external PHD2, 2 for external lin_guider. Pass -1 to select default guider in options. * @return True if guiding is switched to the new requested type. False otherwise. */ Q_SCRIPTABLE bool setGuiderType(int type); /** DBUS interface function. * @brief axisDelta returns the last immediate axis delta deviation in arcseconds. This is the deviation of locked star position when guiding started. * @return List of doubles. First member is RA deviation. Second member is DE deviation. */ Q_SCRIPTABLE QList axisDelta(); /** DBUS interface function. * @brief axisSigma return axis sigma deviation in arcseconds RMS. This is the RMS deviation of locked star position when guiding started. * @return List of doubles. First member is RA deviation. Second member is DE deviation. */ Q_SCRIPTABLE QList axisSigma(); /** * @brief checkCCD Check all CCD parameters and ensure all variables are updated to reflect the selected CCD * @param ccdNum CCD index number in the CCD selection combo box */ void checkCCD(int ccdNum = -1); /** * @brief checkExposureValue This function is called by the INDI framework whenever there is a new exposure value. We use it to know if there is a problem with the exposure * @param targetChip Chip for which the exposure is undergoing * @param exposure numbers of seconds left in the exposure * @param expState State of the exposure property */ void checkExposureValue(ISD::CCDChip *targetChip, double exposure, IPState expState); /** * @brief newFITS is called by the INDI framework whenever there is a new BLOB arriving */ void newFITS(IBLOB *); /** * @brief setST4 Sets a new ST4 device from the combobox index * @param index Index of selected ST4 in the combobox */ void setST4(int index); /* * @brief processRapidStarData is called by INDI framework when we receive new Rapid Guide data * @param targetChip target Chip for which rapid guide is enabled * @param dx Deviation in X * @param dy Deviation in Y * @param fit fitting score */ //void processRapidStarData(ISD::CCDChip *targetChip, double dx, double dy, double fit); /** * @brief Set telescope and guide scope info. All measurements is in millimeters. * @param primaryFocalLength Primary Telescope Focal Length. Set to 0 to skip setting this value. * @param primaryAperture Primary Telescope Aperture. Set to 0 to skip setting this value. * @param guideFocalLength Guide Telescope Focal Length. Set to 0 to skip setting this value. * @param guideAperture Guide Telescope Aperture. Set to 0 to skip setting this value. */ void setTelescopeInfo(double primaryFocalLength, double primaryAperture, double guideFocalLength, double guideAperture); // This Function will allow PHD2 to update the exposure values to the recommended ones. QString setRecommendedExposureValues(QList values); // Append Log entry void appendLogText(const QString &); // Update Guide module status void setStatus(Ekos::GuideState newState); // Update Capture Module status void setCaptureStatus(Ekos::CaptureState newState); // Update Mount module status void setMountStatus(ISD::Telescope::Status newState); // Update Pier Side void setPierSide(ISD::Telescope::PierSide newSide); // Star Position void setStarPosition(const QVector3D &newCenter, bool updateNow); // Capture void setCaptureComplete(); // Send pulse to ST4 driver bool sendPulse(GuideDirection ra_dir, int ra_msecs, GuideDirection dec_dir, int dec_msecs); bool sendPulse(GuideDirection dir, int msecs); /** * @brief setDECSwap Change ST4 declination pulse direction. +DEC pulses increase DEC if swap is OFF. When on +DEC pulses result in decreasing DEC. * @param enable True to enable DEC swap. Off to disable it. */ void setDECSwap(bool enable); void refreshColorScheme(); void setupNSEWLabels(); //plot slots void handleVerticalPlotSizeChange(); void handleHorizontalPlotSizeChange(); void clearGuideGraphs(); void slotAutoScaleGraphs(); void buildTarget(); void guideHistory(); void setLatestGuidePoint(bool isChecked); void toggleShowRAPlot(bool isChecked); void toggleShowDEPlot(bool isChecked); void toggleRACorrectionsPlot(bool isChecked); void toggleDECorrectionsPlot(bool isChecked); void exportGuideData(); void setCorrectionGraphScale(); void updateCorrectionsScaleVisibility(); void updateDirectionsFromPHD2(QString mode); void guideAfterMeridianFlip(); protected slots: void updateTelescopeType(int index); void updateCCDBin(int index); /** * @brief processCCDNumber Process number properties arriving from CCD. Currently, binning changes are processed. * @param nvp pointer to number property. */ void processCCDNumber(INumberVectorProperty *nvp); /** * @brief setTrackingStar Gets called when the user select a star in the guide frame * @param x X coordinate of star * @param y Y coordinate of star */ void setTrackingStar(int x, int y); void saveDefaultGuideExposure(); void updateTrackingBoxSize(int currentIndex); // Display guide information when hovering over the drift graph void driftMouseOverLine(QMouseEvent *event); // Reset graph if right clicked void driftMouseClicked(QMouseEvent *event); //void onXscaleChanged( int i ); //void onYscaleChanged( int i ); void onThresholdChanged(int i); void onInfoRateChanged(double val); void onEnableDirRA(bool enable); void onEnableDirDEC(bool enable); void syncSettings(); //void onRapidGuideChanged(bool enable); void setAxisDelta(double ra, double de); void setAxisSigma(double ra, double de); void setAxisPulse(double ra, double de); void processGuideOptions(); void onControlDirectionChanged(bool enable); void updatePHD2Directions(); void showFITSViewer(); void processCaptureTimeout(); void ditherDirectly(); signals: void newLog(const QString &text); void newStatus(Ekos::GuideState status); void newStarPixmap(QPixmap &); void newProfilePixmap(QPixmap &); // Immediate deviations in arcsecs void newAxisDelta(double ra, double de); // Sigma deviations in arcsecs RMS void newAxisSigma(double ra, double de); void guideChipUpdated(ISD::CCDChip *); private slots: void setDefaultST4(const QString &driver); void setDefaultCCD(const QString &ccd); private: void resizeEvent(QResizeEvent *event) override; /** * @brief updateGuideParams Update the guider and frame parameters due to any changes in the mount and/or ccd frame */ void updateGuideParams(); /** * @brief syncTrackingBoxPosition Sync the tracking box to the current selected star center */ void syncTrackingBoxPosition(); /** * @brief loadSettings Loads and applies all settings from KStars options */ void loadSettings(); /** * @brief saveSettings Saves all current settings into KStars options */ void saveSettings(); /** * @brief setBusy Indicate busy status within the module visually * @param enable True if module is busy, false otherwise */ void setBusy(bool enable); /** * @brief setBLOBEnabled Enable or disable BLOB reception from current CCD if using external guider * @param enable True to enable BLOB reception, false to disable BLOB reception * @param name CCD to enable to disable. If empty (default), then action is applied to all CCDs. */ void setExternalGuiderBLOBEnabled(bool enable); void handleManualDither(); // Operation stack void buildOperationStack(GuideState operation); bool executeOperationStack(); bool executeOneOperation(GuideState operation); // Init Functions void initPlots(); void initView(); void initConnections(); bool captureOneFrame(); // Operation Stack QStack operationStack; // Devices ISD::CCD *currentCCD { nullptr }; QString lastPHD2CameraName; //This is for the configure PHD2 camera method. ISD::Telescope *currentTelescope { nullptr }; ISD::ST4 *ST4Driver { nullptr }; ISD::ST4 *AODriver { nullptr }; ISD::ST4 *GuideDriver { nullptr }; // Device Containers QList ST4List; QList CCDs; // Guider process GuideInterface *guider { nullptr }; - GuiderType guiderType; + GuiderType guiderType { GUIDE_INTERNAL }; // Star QVector3D starCenter; // Guide Params double ccdPixelSizeX { -1 }; double ccdPixelSizeY { -1 }; double aperture { -1 }; double focal_length { -1 }; double guideDeviationRA { 0 }; double guideDeviationDEC { 0 }; double pixScaleX { -1 }; double pixScaleY { -1 }; // Rapid Guide //bool rapidGuideReticleSet; // State GuideState state { GUIDE_IDLE }; // Guide timer QTime guideTimer; // Capture timeout timer QTimer captureTimeout; uint8_t captureTimeoutCounter { 0 }; // Pulse Timer QTimer pulseTimer; // Log QStringList m_LogText; // Misc bool useGuideHead { false }; // Progress Activity Indicator QProgressIndicator *pi { nullptr }; // Options OpsCalibration *opsCalibration { nullptr }; OpsGuide *opsGuide { nullptr }; // Guide Frame FITSView *guideView { nullptr }; // Calibration done already? bool calibrationComplete { false }; // Was the modified frame subFramed? bool subFramed { false }; // CCD Chip frame settings QMap frameSettings; // Profile Pixmap QPixmap profilePixmap; // Flag to start auto calibration followed immediately by guiding //bool autoCalibrateGuide { false }; // Pointers of guider processes QPointer internalGuider; QPointer phd2Guider; QPointer linGuider; QPointer fv; double primaryFL = -1, primaryAperture = -1, guideFL = -1, guideAperture = -1; QCPCurve *centralTarget { nullptr }; QCPCurve *yellowTarget { nullptr }; QCPCurve *redTarget { nullptr }; QCPCurve *concentricRings { nullptr }; bool graphOnLatestPt = true; QUrl guideURLPath; //This is for enforcing the PHD2 Star lock when Guide is pressed, //autostar is not selected, and the user has chosen a star. //This connection storage is so that the connection can be disconnected after enforcement QMetaObject::Connection guideConnect; }; } diff --git a/kstars/ekos/manager.cpp b/kstars/ekos/manager.cpp index a8e924540..82503df07 100644 --- a/kstars/ekos/manager.cpp +++ b/kstars/ekos/manager.cpp @@ -1,3390 +1,3392 @@ /* 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 "manager.h" #include "ekosadaptor.h" #include "kstars.h" #include "kstarsdata.h" #include "opsekos.h" #include "Options.h" #include "profileeditor.h" #include "profilewizard.h" #include "skymap.h" #include "auxiliary/darklibrary.h" #include "auxiliary/QProgressIndicator.h" #include "auxiliary/ksmessagebox.h" #include "capture/sequencejob.h" #include "fitsviewer/fitstab.h" #include "fitsviewer/fitsview.h" #include "fitsviewer/fitsdata.h" #include "indi/clientmanager.h" #include "indi/driverinfo.h" #include "indi/drivermanager.h" #include "indi/guimanager.h" #include "indi/indielement.h" #include "indi/indilistener.h" #include "indi/indiproperty.h" #include "indi/indiwebmanager.h" #include "ekoslive/ekosliveclient.h" #include "ekoslive/message.h" #include "ekoslive/media.h" #include #include #include #include #include #include #include #define MAX_REMOTE_INDI_TIMEOUT 15000 #define MAX_LOCAL_INDI_TIMEOUT 5000 namespace Ekos { Manager::Manager(QWidget * parent) : QDialog(parent) { #ifdef Q_OS_OSX if (Options::independentWindowEkos()) setWindowFlags(Qt::Window); else { setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint); connect(QApplication::instance(), SIGNAL(applicationStateChanged(Qt::ApplicationState)), this, SLOT(changeAlwaysOnTop(Qt::ApplicationState))); } #else // if (Options::independentWindowEkos()) // setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint); #endif setupUi(this); qRegisterMetaType("Ekos::CommunicationStatus"); qDBusRegisterMetaType(); new EkosAdaptor(this); QDBusConnection::sessionBus().registerObject("/KStars/Ekos", this); setWindowIcon(QIcon::fromTheme("kstars_ekos")); profileModel.reset(new QStandardItemModel(0, 4)); profileModel->setHorizontalHeaderLabels(QStringList() << "id" << "name" << "host" << "port"); captureProgress->setValue(0); sequenceProgress->setValue(0); sequenceProgress->setDecimals(0); sequenceProgress->setFormat("%v"); imageProgress->setValue(0); imageProgress->setDecimals(1); imageProgress->setFormat("%v"); imageProgress->setBarStyle(QRoundProgressBar::StyleLine); countdownTimer.setInterval(1000); connect(&countdownTimer, &QTimer::timeout, this, &Ekos::Manager::updateCaptureCountDown); toolsWidget->setIconSize(QSize(48, 48)); connect(toolsWidget, &QTabWidget::currentChanged, this, &Ekos::Manager::processTabChange, Qt::UniqueConnection); // Enable scheduler Tab toolsWidget->setTabEnabled(1, false); // Start/Stop INDI Server connect(processINDIB, &QPushButton::clicked, this, &Ekos::Manager::processINDI); processINDIB->setIcon(QIcon::fromTheme("media-playback-start")); processINDIB->setToolTip(i18n("Start")); // Connect/Disconnect INDI devices connect(connectB, &QPushButton::clicked, this, &Ekos::Manager::connectDevices); connect(disconnectB, &QPushButton::clicked, this, &Ekos::Manager::disconnectDevices); ekosLiveB->setAttribute(Qt::WA_LayoutUsesWidgetRect); ekosLiveClient.reset(new EkosLive::Client(this)); connect(ekosLiveClient.get(), &EkosLive::Client::connected, [this]() { emit ekosLiveStatusChanged(true); }); connect(ekosLiveClient.get(), &EkosLive::Client::disconnected, [this]() { emit ekosLiveStatusChanged(false); }); // INDI Control Panel //connect(controlPanelB, &QPushButton::clicked, GUIManager::Instance(), SLOT(show())); connect(ekosLiveB, &QPushButton::clicked, [&]() { ekosLiveClient.get()->show(); ekosLiveClient.get()->raise(); }); connect(this, &Manager::ekosStatusChanged, ekosLiveClient.get()->message(), &EkosLive::Message::setEkosStatingStatus); connect(ekosLiveClient.get()->message(), &EkosLive::Message::connected, [&]() { ekosLiveB->setIcon(QIcon(":/icons/cloud-online.svg")); }); connect(ekosLiveClient.get()->message(), &EkosLive::Message::disconnected, [&]() { ekosLiveB->setIcon(QIcon::fromTheme("folder-cloud")); }); connect(ekosLiveClient.get()->media(), &EkosLive::Media::newBoundingRect, ekosLiveClient.get()->message(), &EkosLive::Message::setBoundingRect); connect(ekosLiveClient.get()->message(), &EkosLive::Message::resetPolarView, ekosLiveClient.get()->media(), &EkosLive::Media::resetPolarView); connect(ekosLiveClient.get()->message(), &EkosLive::Message::previewJPEGGenerated, ekosLiveClient.get()->media(), &EkosLive::Media::sendPreviewJPEG); connect(KSMessageBox::Instance(), &KSMessageBox::newMessage, ekosLiveClient.get()->message(), &EkosLive::Message::sendDialog); // Serial Port Assistant connect(serialPortAssistantB, &QPushButton::clicked, [&]() { serialPortAssistant->show(); serialPortAssistant->raise(); }); connect(this, &Ekos::Manager::ekosStatusChanged, [&](Ekos::CommunicationStatus status) { indiControlPanelB->setEnabled(status == Ekos::Success); }); connect(indiControlPanelB, &QPushButton::clicked, [&]() { KStars::Instance()->actionCollection()->action("show_control_panel")->trigger(); }); connect(optionsB, &QPushButton::clicked, [&]() { KStars::Instance()->actionCollection()->action("configure")->trigger(); }); // Save as above, but it appears in all modules connect(ekosOptionsB, &QPushButton::clicked, this, &Ekos::Manager::showEkosOptions); // Clear Ekos Log connect(clearB, &QPushButton::clicked, this, &Ekos::Manager::clearLog); // Logs KConfigDialog * dialog = new KConfigDialog(this, "logssettings", Options::self()); opsLogs = new Ekos::OpsLogs(); KPageWidgetItem * page = dialog->addPage(opsLogs, i18n("Logging")); page->setIcon(QIcon::fromTheme("configure")); connect(logsB, &QPushButton::clicked, dialog, &KConfigDialog::show); connect(dialog->button(QDialogButtonBox::Apply), &QPushButton::clicked, this, &Ekos::Manager::updateDebugInterfaces); connect(dialog->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, &Ekos::Manager::updateDebugInterfaces); // Summary // previewPixmap = new QPixmap(QPixmap(":/images/noimage.png")); // Profiles connect(addProfileB, &QPushButton::clicked, this, &Ekos::Manager::addProfile); connect(editProfileB, &QPushButton::clicked, this, &Ekos::Manager::editProfile); connect(deleteProfileB, &QPushButton::clicked, this, &Ekos::Manager::deleteProfile); connect(profileCombo, static_cast(&QComboBox::currentTextChanged), [ = ](const QString & text) { Options::setProfile(text); if (text == "Simulators") { editProfileB->setEnabled(false); deleteProfileB->setEnabled(false); } else { editProfileB->setEnabled(true); deleteProfileB->setEnabled(true); } }); // Ekos Wizard connect(wizardProfileB, &QPushButton::clicked, this, &Ekos::Manager::wizardProfile); addProfileB->setAttribute(Qt::WA_LayoutUsesWidgetRect); editProfileB->setAttribute(Qt::WA_LayoutUsesWidgetRect); deleteProfileB->setAttribute(Qt::WA_LayoutUsesWidgetRect); // Set Profile icons addProfileB->setIcon(QIcon::fromTheme("list-add")); addProfileB->setAttribute(Qt::WA_LayoutUsesWidgetRect); editProfileB->setIcon(QIcon::fromTheme("document-edit")); editProfileB->setAttribute(Qt::WA_LayoutUsesWidgetRect); deleteProfileB->setIcon(QIcon::fromTheme("list-remove")); deleteProfileB->setAttribute(Qt::WA_LayoutUsesWidgetRect); wizardProfileB->setIcon(QIcon::fromTheme("tools-wizard")); wizardProfileB->setAttribute(Qt::WA_LayoutUsesWidgetRect); customDriversB->setIcon(QIcon::fromTheme("roll")); customDriversB->setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(customDriversB, &QPushButton::clicked, DriverManager::Instance(), &DriverManager::showCustomDrivers); // Load all drivers loadDrivers(); // Load add driver profiles loadProfiles(); // INDI Control Panel and Ekos Options optionsB->setIcon(QIcon::fromTheme("configure", QIcon(":/icons/ekos_setup.png"))); optionsB->setAttribute(Qt::WA_LayoutUsesWidgetRect); // Setup Tab toolsWidget->tabBar()->setTabIcon(0, QIcon(":/icons/ekos_setup.png")); toolsWidget->tabBar()->setTabToolTip(0, i18n("Setup")); // Initialize Ekos Scheduler Module schedulerProcess.reset(new Ekos::Scheduler()); toolsWidget->addTab(schedulerProcess.get(), QIcon(":/icons/ekos_scheduler.png"), ""); toolsWidget->tabBar()->setTabToolTip(1, i18n("Scheduler")); connect(schedulerProcess.get(), &Scheduler::newLog, this, &Ekos::Manager::updateLog); //connect(schedulerProcess.get(), SIGNAL(newTarget(QString)), mountTarget, SLOT(setText(QString))); connect(schedulerProcess.get(), &Ekos::Scheduler::newTarget, [&](const QString & target) { mountTarget->setText(target); ekosLiveClient.get()->message()->updateMountStatus(QJsonObject({{"target", target}})); }); // Temporary fix. Not sure how to resize Ekos Dialog to fit contents of the various tabs in the QScrollArea which are added // dynamically. I used setMinimumSize() but it doesn't appear to make any difference. // Also set Layout policy to SetMinAndMaxSize as well. Any idea how to fix this? // FIXME //resize(1000,750); summaryPreview.reset(new FITSView(previewWidget, FITS_NORMAL)); previewWidget->setContentsMargins(0, 0, 0, 0); summaryPreview->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); summaryPreview->setBaseSize(previewWidget->size()); summaryPreview->createFloatingToolBar(); summaryPreview->setCursorMode(FITSView::dragCursor); QVBoxLayout * vlayout = new QVBoxLayout(); vlayout->addWidget(summaryPreview.get()); previewWidget->setLayout(vlayout); // JM 2019-01-19: Why cloud images depend on summary preview? // connect(summaryPreview.get(), &FITSView::loaded, [&]() // { // // UUID binds the cloud & preview frames by a common key // QString uuid = QUuid::createUuid().toString(); // uuid = uuid.remove(QRegularExpression("[-{}]")); // //ekosLiveClient.get()->media()->sendPreviewImage(summaryPreview.get(), uuid); // ekosLiveClient.get()->cloud()->sendPreviewImage(summaryPreview.get(), uuid); // }); if (Options::ekosLeftIcons()) { toolsWidget->setTabPosition(QTabWidget::West); QTransform trans; trans.rotate(90); QIcon icon = toolsWidget->tabIcon(0); QPixmap pix = icon.pixmap(QSize(48, 48)); icon = QIcon(pix.transformed(trans)); toolsWidget->setTabIcon(0, icon); icon = toolsWidget->tabIcon(1); pix = icon.pixmap(QSize(48, 48)); icon = QIcon(pix.transformed(trans)); toolsWidget->setTabIcon(1, icon); } //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); resize(Options::ekosWindowWidth(), Options::ekosWindowHeight()); } void Manager::changeAlwaysOnTop(Qt::ApplicationState state) { if (isVisible()) { if (state == Qt::ApplicationActive) setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint); else setWindowFlags(windowFlags() & ~Qt::WindowStaysOnTopHint); show(); } } Manager::~Manager() { toolsWidget->disconnect(this); //delete previewPixmap; } void Manager::closeEvent(QCloseEvent * event) { // QAction * a = KStars::Instance()->actionCollection()->action("show_ekos"); // a->setChecked(false); // 2019-02-14 JM: Close event, for some reason, make all the children disappear // when the widget is shown again. Applying a workaround here event->ignore(); hide(); } void Manager::hideEvent(QHideEvent * /*event*/) { Options::setEkosWindowWidth(width()); Options::setEkosWindowHeight(height()); QAction * a = KStars::Instance()->actionCollection()->action("show_ekos"); a->setChecked(false); } void Manager::showEvent(QShowEvent * /*event*/) { QAction * a = KStars::Instance()->actionCollection()->action("show_ekos"); a->setChecked(true); // Just show the profile wizard ONCE per session if (profileWizardLaunched == false && profiles.count() == 1) { profileWizardLaunched = true; wizardProfile(); } } void Manager::resizeEvent(QResizeEvent *) { //previewImage->setPixmap(previewPixmap->scaled(previewImage->width(), previewImage->height(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); if (focusStarPixmap.get() != nullptr) focusStarImage->setPixmap(focusStarPixmap->scaled(focusStarImage->width(), focusStarImage->height(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); //if (focusProfilePixmap) //focusProfileImage->setPixmap(focusProfilePixmap->scaled(focusProfileImage->width(), focusProfileImage->height(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); if (guideStarPixmap.get() != nullptr) guideStarImage->setPixmap(guideStarPixmap->scaled(guideStarImage->width(), guideStarImage->height(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); //if (guideProfilePixmap) //guideProfileImage->setPixmap(guideProfilePixmap->scaled(guideProfileImage->width(), guideProfileImage->height(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); } void Manager::loadProfiles() { profiles.clear(); KStarsData::Instance()->userdb()->GetAllProfiles(profiles); profileModel->clear(); for (auto &pi : profiles) { QList info; info << new QStandardItem(pi->id) << new QStandardItem(pi->name) << new QStandardItem(pi->host) << new QStandardItem(pi->port); profileModel->appendRow(info); } profileModel->sort(0); profileCombo->blockSignals(true); profileCombo->setModel(profileModel.get()); profileCombo->setModelColumn(1); profileCombo->blockSignals(false); // Load last used profile from options int index = profileCombo->findText(Options::profile()); // If not found, set it to first item if (index == -1) index = 0; profileCombo->setCurrentIndex(index); } void Manager::loadDrivers() { foreach (DriverInfo * dv, DriverManager::Instance()->getDrivers()) { if (dv->getDriverSource() != HOST_SOURCE) driversList[dv->getLabel()] = dv; } } void Manager::reset() { qCDebug(KSTARS_EKOS) << "Resetting Ekos Manager..."; // Filter Manager filterManager.reset(new Ekos::FilterManager()); nDevices = 0; useGuideHead = false; useST4 = false; removeTabs(); genericDevices.clear(); managedDevices.clear(); captureProcess.reset(); focusProcess.reset(); guideProcess.reset(); domeProcess.reset(); alignProcess.reset(); mountProcess.reset(); weatherProcess.reset(); observatoryProcess.reset(); dustCapProcess.reset(); DarkLibrary::Instance()->reset(); Ekos::CommunicationStatus previousStatus = m_ekosStatus; m_ekosStatus = Ekos::Idle; if (previousStatus != m_ekosStatus) emit ekosStatusChanged(m_ekosStatus); previousStatus = m_indiStatus; m_indiStatus = Ekos::Idle; if (previousStatus != m_indiStatus) emit indiStatusChanged(m_indiStatus); connectB->setEnabled(false); disconnectB->setEnabled(false); //controlPanelB->setEnabled(false); processINDIB->setEnabled(true); mountGroup->setEnabled(false); focusGroup->setEnabled(false); captureGroup->setEnabled(false); guideGroup->setEnabled(false); sequenceLabel->setText(i18n("Sequence")); sequenceProgress->setValue(0); captureProgress->setValue(0); overallRemainingTime->setText("--:--:--"); sequenceRemainingTime->setText("--:--:--"); imageRemainingTime->setText("--:--:--"); mountStatus->setText(i18n("Idle")); mountStatus->setStyleSheet(QString()); captureStatus->setText(i18n("Idle")); focusStatus->setText(i18n("Idle")); guideStatus->setText(i18n("Idle")); if (capturePI) capturePI->stopAnimation(); if (mountPI) mountPI->stopAnimation(); if (focusPI) focusPI->stopAnimation(); if (guidePI) guidePI->stopAnimation(); m_isStarted = false; //processINDIB->setText(i18n("Start INDI")); processINDIB->setIcon(QIcon::fromTheme("media-playback-start")); processINDIB->setToolTip(i18n("Start")); } void Manager::processINDI() { if (m_isStarted == false) start(); else stop(); } bool Manager::stop() { cleanDevices(); serialPortAssistant.reset(); serialPortAssistantB->setEnabled(false); profileGroup->setEnabled(true); setWindowTitle(i18n("Ekos")); return true; } bool Manager::start() { // Don't start if it is already started before if (m_ekosStatus == Ekos::Pending || m_ekosStatus == Ekos::Success) { qCDebug(KSTARS_EKOS) << "Ekos Manager start called but current Ekos Status is" << m_ekosStatus << "Ignoring request."; return true; } if (m_LocalMode) qDeleteAll(managedDrivers); managedDrivers.clear(); // If clock was paused, unpaused it and sync time if (KStarsData::Instance()->clock()->isActive() == false) { KStarsData::Instance()->changeDateTime(KStarsDateTime::currentDateTimeUtc()); KStarsData::Instance()->clock()->start(); } reset(); // connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]() // { // QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); // qDebug() << "Dialog was accepted!"; // }); // KSMessageBox::Instance()->questionYesNo("Do you want to proceed?", "Confirm", 21); currentProfile = getCurrentProfile(); m_LocalMode = currentProfile->isLocal(); // Load profile location if one exists updateProfileLocation(currentProfile); bool haveCCD = false, haveGuider = false; if (currentProfile->guidertype == Ekos::Guide::GUIDE_PHD2) { Options::setPHD2Host(currentProfile->guiderhost); Options::setPHD2Port(currentProfile->guiderport); } else if (currentProfile->guidertype == Ekos::Guide::GUIDE_LINGUIDER) { Options::setLinGuiderHost(currentProfile->guiderhost); Options::setLinGuiderPort(currentProfile->guiderport); } if (m_LocalMode) { DriverInfo * drv = driversList.value(currentProfile->mount()); if (drv != nullptr) managedDrivers.append(drv->clone()); drv = driversList.value(currentProfile->ccd()); if (drv != nullptr) { managedDrivers.append(drv->clone()); haveCCD = true; } Options::setGuiderType(currentProfile->guidertype); drv = driversList.value(currentProfile->guider()); if (drv != nullptr) { haveGuider = true; // If the guider and ccd are the same driver, we have two cases: // #1 Drivers that only support ONE device per driver (such as sbig) // #2 Drivers that supports multiples devices per driver (such as sx) // For #1, we modify guider_di to make a unique label for the other device with postfix "Guide" // For #2, we set guider_di to nullptr and we prompt the user to select which device is primary ccd and which is guider // since this is the only way to find out in real time. if (haveCCD && currentProfile->guider() == currentProfile->ccd()) { if (checkUniqueBinaryDriver( driversList.value(currentProfile->ccd()), drv)) { drv = nullptr; } else { drv->setUniqueLabel(drv->getLabel() + " Guide"); } } if (drv) managedDrivers.append(drv->clone()); } drv = driversList.value(currentProfile->ao()); if (drv != nullptr) managedDrivers.append(drv->clone()); drv = driversList.value(currentProfile->filter()); if (drv != nullptr) managedDrivers.append(drv->clone()); drv = driversList.value(currentProfile->focuser()); if (drv != nullptr) managedDrivers.append(drv->clone()); drv = driversList.value(currentProfile->dome()); if (drv != nullptr) managedDrivers.append(drv->clone()); drv = driversList.value(currentProfile->weather()); if (drv != nullptr) managedDrivers.append(drv->clone()); drv = driversList.value(currentProfile->aux1()); if (drv != nullptr) { if (!checkUniqueBinaryDriver(driversList.value(currentProfile->ccd()), drv) && !checkUniqueBinaryDriver(driversList.value(currentProfile->guider()), drv)) managedDrivers.append(drv->clone()); } drv = driversList.value(currentProfile->aux2()); if (drv != nullptr) { if (!checkUniqueBinaryDriver(driversList.value(currentProfile->ccd()), drv) && !checkUniqueBinaryDriver(driversList.value(currentProfile->guider()), drv)) managedDrivers.append(drv->clone()); } drv = driversList.value(currentProfile->aux3()); if (drv != nullptr) { if (!checkUniqueBinaryDriver(driversList.value(currentProfile->ccd()), drv) && !checkUniqueBinaryDriver(driversList.value(currentProfile->guider()), drv)) managedDrivers.append(drv->clone()); } drv = driversList.value(currentProfile->aux4()); if (drv != nullptr) { if (!checkUniqueBinaryDriver(driversList.value(currentProfile->ccd()), drv) && !checkUniqueBinaryDriver(driversList.value(currentProfile->guider()), drv)) managedDrivers.append(drv->clone()); } // Add remote drivers if we have any if (currentProfile->remotedrivers.isEmpty() == false && currentProfile->remotedrivers.contains("@")) { for (auto remoteDriver : currentProfile->remotedrivers.split(",")) { QString name, label, host("localhost"), port("7624"); QStringList properties = remoteDriver.split(QRegExp("[@:]")); if (properties.length() > 1) { name = properties[0]; host = properties[1]; if (properties.length() > 2) port = properties[2]; } DriverInfo * dv = new DriverInfo(name); dv->setRemoteHost(host); dv->setRemotePort(port); label = name; // Remove extra quotes label.remove("\""); dv->setLabel(label); dv->setUniqueLabel(label); managedDrivers.append(dv); } } if (haveCCD == false && haveGuider == false) { KSNotification::error(i18n("Ekos requires at least one CCD or Guider to operate.")); managedDrivers.clear(); return false; } nDevices = managedDrivers.count(); } else { DriverInfo * remote_indi = new DriverInfo(QString("Ekos Remote Host")); remote_indi->setHostParameters(currentProfile->host, QString::number(currentProfile->port)); remote_indi->setDriverSource(GENERATED_SOURCE); managedDrivers.append(remote_indi); haveCCD = currentProfile->drivers.contains("CCD"); haveGuider = currentProfile->drivers.contains("Guider"); Options::setGuiderType(currentProfile->guidertype); if (haveCCD == false && haveGuider == false) { KSNotification::error(i18n("Ekos requires at least one CCD or Guider to operate.")); delete (remote_indi); nDevices = 0; return false; } nDevices = currentProfile->drivers.count(); } connect(INDIListener::Instance(), &INDIListener::newDevice, this, &Ekos::Manager::processNewDevice); connect(INDIListener::Instance(), &INDIListener::newTelescope, this, &Ekos::Manager::setTelescope); connect(INDIListener::Instance(), &INDIListener::newCCD, this, &Ekos::Manager::setCCD); connect(INDIListener::Instance(), &INDIListener::newFilter, this, &Ekos::Manager::setFilter); connect(INDIListener::Instance(), &INDIListener::newFocuser, this, &Ekos::Manager::setFocuser); connect(INDIListener::Instance(), &INDIListener::newDome, this, &Ekos::Manager::setDome); connect(INDIListener::Instance(), &INDIListener::newWeather, this, &Ekos::Manager::setWeather); connect(INDIListener::Instance(), &INDIListener::newDustCap, this, &Ekos::Manager::setDustCap); connect(INDIListener::Instance(), &INDIListener::newLightBox, this, &Ekos::Manager::setLightBox); connect(INDIListener::Instance(), &INDIListener::newST4, this, &Ekos::Manager::setST4); connect(INDIListener::Instance(), &INDIListener::deviceRemoved, this, &Ekos::Manager::removeDevice, Qt::DirectConnection); #ifdef Q_OS_OSX if (m_LocalMode || currentProfile->host == "localhost") { if (isRunning("PTPCamera")) { if (KMessageBox::Yes == (KMessageBox::questionYesNo(nullptr, i18n("Ekos detected that PTP Camera is running and may prevent a Canon or Nikon camera from connecting to Ekos. Do you want to quit PTP Camera now?"), i18n("PTP Camera"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "ekos_shutdown_PTPCamera"))) { //TODO is there a better way to do this. QProcess p; p.start("killall PTPCamera"); p.waitForFinished(); } } } #endif if (m_LocalMode) { if (isRunning("indiserver")) { if (KMessageBox::Yes == (KMessageBox::questionYesNo(nullptr, i18n("Ekos detected an instance of INDI server running. Do you wish to " "shut down the existing instance before starting a new one?"), i18n("INDI Server"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "ekos_shutdown_existing_indiserver"))) { DriverManager::Instance()->stopAllDevices(); //TODO is there a better way to do this. QProcess p; p.start("pkill indiserver"); p.waitForFinished(); } } appendLogText(i18n("Starting INDI services...")); if (DriverManager::Instance()->startDevices(managedDrivers) == false) { INDIListener::Instance()->disconnect(this); qDeleteAll(managedDrivers); managedDrivers.clear(); m_ekosStatus = Ekos::Error; emit ekosStatusChanged(m_ekosStatus); return false; } connect(DriverManager::Instance(), SIGNAL(serverTerminated(QString, QString)), this, SLOT(processServerTermination(QString, QString))); m_ekosStatus = Ekos::Pending; emit ekosStatusChanged(m_ekosStatus); if (currentProfile->autoConnect) appendLogText(i18n("INDI services started on port %1.", managedDrivers.first()->getPort())); else appendLogText( i18n("INDI services started on port %1. Please connect devices.", managedDrivers.first()->getPort())); QTimer::singleShot(MAX_LOCAL_INDI_TIMEOUT, this, &Ekos::Manager::checkINDITimeout); } else { // If we need to use INDI Web Manager if (currentProfile->INDIWebManagerPort > 0) { appendLogText(i18n("Establishing communication with remote INDI Web Manager...")); m_RemoteManagerStart = false; if (INDI::WebManager::isOnline(currentProfile)) { INDI::WebManager::syncCustomDrivers(currentProfile); currentProfile->isStellarMate = INDI::WebManager::isStellarMate(currentProfile); if (INDI::WebManager::areDriversRunning(currentProfile) == false) { INDI::WebManager::stopProfile(currentProfile); if (INDI::WebManager::startProfile(currentProfile) == false) { appendLogText(i18n("Failed to start profile on remote INDI Web Manager.")); return false; } appendLogText(i18n("Starting profile on remote INDI Web Manager...")); m_RemoteManagerStart = true; } } else appendLogText(i18n("Warning: INDI Web Manager is not online.")); } appendLogText( i18n("Connecting to remote INDI server at %1 on port %2 ...", currentProfile->host, currentProfile->port)); qApp->processEvents(); QApplication::setOverrideCursor(Qt::WaitCursor); if (DriverManager::Instance()->connectRemoteHost(managedDrivers.first()) == false) { appendLogText(i18n("Failed to connect to remote INDI server.")); INDIListener::Instance()->disconnect(this); qDeleteAll(managedDrivers); managedDrivers.clear(); m_ekosStatus = Ekos::Error; emit ekosStatusChanged(m_ekosStatus); QApplication::restoreOverrideCursor(); return false; } connect(DriverManager::Instance(), SIGNAL(serverTerminated(QString, QString)), this, SLOT(processServerTermination(QString, QString))); QApplication::restoreOverrideCursor(); m_ekosStatus = Ekos::Pending; emit ekosStatusChanged(m_ekosStatus); appendLogText( i18n("INDI services started. Connection to remote INDI server is successful. Waiting for devices...")); QTimer::singleShot(MAX_REMOTE_INDI_TIMEOUT, this, &Ekos::Manager::checkINDITimeout); } connectB->setEnabled(false); disconnectB->setEnabled(false); //controlPanelB->setEnabled(false); profileGroup->setEnabled(false); m_isStarted = true; //processINDIB->setText(i18n("Stop INDI")); processINDIB->setIcon(QIcon::fromTheme("media-playback-stop")); processINDIB->setToolTip(i18n("Stop")); setWindowTitle(i18n("Ekos - %1 Profile", currentProfile->name)); return true; } void Manager::checkINDITimeout() { // Don't check anything unless we're still pending if (m_ekosStatus != Ekos::Pending) return; if (nDevices <= 0) { m_ekosStatus = Ekos::Success; emit ekosStatusChanged(m_ekosStatus); return; } if (m_LocalMode) { QStringList remainingDevices; foreach (DriverInfo * drv, managedDrivers) { if (drv->getDevices().count() == 0) remainingDevices << QString("+ %1").arg( drv->getUniqueLabel().isEmpty() == false ? drv->getUniqueLabel() : drv->getName()); } if (remainingDevices.count() == 1) { appendLogText(i18n("Unable to establish:\n%1\nPlease ensure the device is connected and powered on.", remainingDevices.at(0))); KNotification::beep(i18n("Ekos startup error")); } else { appendLogText(i18n("Unable to establish the following devices:\n%1\nPlease ensure each device is connected " "and powered on.", remainingDevices.join("\n"))); KNotification::beep(i18n("Ekos startup error")); } } else { QStringList remainingDevices; for (auto &driver : currentProfile->drivers.values()) { bool driverFound = false; for (auto &device : genericDevices) { if (device->getBaseDevice()->getDriverName() == driver) { driverFound = true; break; } } if (driverFound == false) remainingDevices << QString("+ %1").arg(driver); } if (remainingDevices.count() == 1) { appendLogText(i18n("Unable to establish remote device:\n%1\nPlease ensure remote device name corresponds " "to actual device name.", remainingDevices.at(0))); KNotification::beep(i18n("Ekos startup error")); } else { appendLogText(i18n("Unable to establish remote devices:\n%1\nPlease ensure remote device name corresponds " "to actual device name.", remainingDevices.join("\n"))); KNotification::beep(i18n("Ekos startup error")); } } m_ekosStatus = Ekos::Error; } void Manager::connectDevices() { // Check if already connected int nConnected = 0; Ekos::CommunicationStatus previousStatus = m_indiStatus; for (auto &device : genericDevices) { if (device->isConnected()) nConnected++; } if (genericDevices.count() == nConnected) { m_indiStatus = Ekos::Success; emit indiStatusChanged(m_indiStatus); return; } m_indiStatus = Ekos::Pending; if (previousStatus != m_indiStatus) emit indiStatusChanged(m_indiStatus); for (auto &device : genericDevices) { qCDebug(KSTARS_EKOS) << "Connecting " << device->getDeviceName(); device->Connect(); } connectB->setEnabled(false); disconnectB->setEnabled(true); appendLogText(i18n("Connecting INDI devices...")); } void Manager::disconnectDevices() { for (auto &device : genericDevices) { qCDebug(KSTARS_EKOS) << "Disconnecting " << device->getDeviceName(); device->Disconnect(); } appendLogText(i18n("Disconnecting INDI devices...")); } void Manager::processServerTermination(const QString &host, const QString &port) { if ((m_LocalMode && managedDrivers.first()->getPort() == port) || (currentProfile->host == host && currentProfile->port == port.toInt())) { cleanDevices(false); } } void Manager::cleanDevices(bool stopDrivers) { if (m_ekosStatus == Ekos::Idle) return; INDIListener::Instance()->disconnect(this); DriverManager::Instance()->disconnect(this); if (managedDrivers.isEmpty() == false) { if (m_LocalMode) { if (stopDrivers) DriverManager::Instance()->stopDevices(managedDrivers); } else { if (stopDrivers) DriverManager::Instance()->disconnectRemoteHost(managedDrivers.first()); if (m_RemoteManagerStart && currentProfile->INDIWebManagerPort != -1) { INDI::WebManager::stopProfile(currentProfile); m_RemoteManagerStart = false; } } } reset(); profileGroup->setEnabled(true); appendLogText(i18n("INDI services stopped.")); } void Manager::processNewDevice(ISD::GDInterface * devInterface) { qCInfo(KSTARS_EKOS) << "Ekos received a new device: " << devInterface->getDeviceName(); Ekos::CommunicationStatus previousStatus = m_indiStatus; for(auto &device : genericDevices) { if (!strcmp(device->getDeviceName(), devInterface->getDeviceName())) { qCWarning(KSTARS_EKOS) << "Found duplicate device, ignoring..."; return; } } // Always reset INDI Connection status if we receive a new device m_indiStatus = Ekos::Idle; if (previousStatus != m_indiStatus) emit indiStatusChanged(m_indiStatus); genericDevices.append(devInterface); nDevices--; connect(devInterface, &ISD::GDInterface::Connected, this, &Ekos::Manager::deviceConnected); connect(devInterface, &ISD::GDInterface::Disconnected, this, &Ekos::Manager::deviceDisconnected); connect(devInterface, &ISD::GDInterface::propertyDefined, this, &Ekos::Manager::processNewProperty); connect(devInterface, &ISD::GDInterface::interfaceDefined, this, &Ekos::Manager::syncActiveDevices); if (currentProfile->isStellarMate) { connect(devInterface, &ISD::GDInterface::systemPortDetected, [this, devInterface]() { if (!serialPortAssistant) { serialPortAssistant.reset(new SerialPortAssistant(currentProfile, this)); serialPortAssistantB->setEnabled(true); } uint32_t driverInterface = devInterface->getDriverInterface(); // Ignore CCD interface if (driverInterface & INDI::BaseDevice::CCD_INTERFACE) return; if (driverInterface & INDI::BaseDevice::TELESCOPE_INTERFACE || driverInterface & INDI::BaseDevice::FOCUSER_INTERFACE || driverInterface & INDI::BaseDevice::FILTER_INTERFACE || driverInterface & INDI::BaseDevice::AUX_INTERFACE || driverInterface & INDI::BaseDevice::GPS_INTERFACE) serialPortAssistant->addDevice(devInterface); if (Options::autoLoadSerialAssistant()) serialPortAssistant->show(); }); } if (nDevices <= 0) { m_ekosStatus = Ekos::Success; emit ekosStatusChanged(m_ekosStatus); connectB->setEnabled(true); disconnectB->setEnabled(false); //controlPanelB->setEnabled(true); if (m_LocalMode == false && nDevices == 0) { if (currentProfile->autoConnect) appendLogText(i18n("Remote devices established.")); else appendLogText(i18n("Remote devices established. Please connect devices.")); } } } void Manager::deviceConnected() { connectB->setEnabled(false); disconnectB->setEnabled(true); processINDIB->setEnabled(false); Ekos::CommunicationStatus previousStatus = m_indiStatus; if (Options::verboseLogging()) { ISD::GDInterface * device = qobject_cast(sender()); qCInfo(KSTARS_EKOS) << device->getDeviceName() << "Version:" << device->getDriverVersion() << "Interface:" << device->getDriverInterface() << "is connected."; } int nConnectedDevices = 0; foreach (ISD::GDInterface * device, genericDevices) { if (device->isConnected()) nConnectedDevices++; } qCDebug(KSTARS_EKOS) << nConnectedDevices << " devices connected out of " << genericDevices.count(); if (nConnectedDevices >= currentProfile->drivers.count()) //if (nConnectedDevices >= genericDevices.count()) { m_indiStatus = Ekos::Success; qCInfo(KSTARS_EKOS) << "All INDI devices are now connected."; } else m_indiStatus = Ekos::Pending; if (previousStatus != m_indiStatus) emit indiStatusChanged(m_indiStatus); ISD::GDInterface * dev = static_cast(sender()); if (dev->getBaseDevice()->getDriverInterface() & INDI::BaseDevice::TELESCOPE_INTERFACE) { if (mountProcess.get() != nullptr) { mountProcess->setEnabled(true); if (alignProcess.get() != nullptr) alignProcess->setEnabled(true); } } else if (dev->getBaseDevice()->getDriverInterface() & INDI::BaseDevice::CCD_INTERFACE) { if (captureProcess.get() != nullptr) captureProcess->setEnabled(true); if (focusProcess.get() != nullptr) focusProcess->setEnabled(true); if (alignProcess.get() != nullptr) { if (mountProcess.get() && mountProcess->isEnabled()) alignProcess->setEnabled(true); else alignProcess->setEnabled(false); } if (guideProcess.get() != nullptr) guideProcess->setEnabled(true); } else if (dev->getBaseDevice()->getDriverInterface() & INDI::BaseDevice::FOCUSER_INTERFACE) { if (focusProcess.get() != nullptr) focusProcess->setEnabled(true); } if (Options::neverLoadConfig()) return; INDIConfig tConfig = Options::loadConfigOnConnection() ? LOAD_LAST_CONFIG : LOAD_DEFAULT_CONFIG; foreach (ISD::GDInterface * device, genericDevices) { if (device == dev) { connect(dev, &ISD::GDInterface::switchUpdated, this, &Ekos::Manager::watchDebugProperty); ISwitchVectorProperty * configProp = device->getBaseDevice()->getSwitch("CONFIG_PROCESS"); if (configProp && configProp->s == IPS_IDLE) device->setConfig(tConfig); break; } } } void Manager::deviceDisconnected() { ISD::GDInterface * dev = static_cast(sender()); Ekos::CommunicationStatus previousStatus = m_indiStatus; if (dev != nullptr) { if (dev->getState("CONNECTION") == IPS_ALERT) m_indiStatus = Ekos::Error; else if (dev->getState("CONNECTION") == IPS_BUSY) m_indiStatus = Ekos::Pending; else m_indiStatus = Ekos::Idle; if (Options::verboseLogging()) qCDebug(KSTARS_EKOS) << dev->getDeviceName() << " is disconnected."; appendLogText(i18n("%1 is disconnected.", dev->getDeviceName())); } else m_indiStatus = Ekos::Idle; if (previousStatus != m_indiStatus) emit indiStatusChanged(m_indiStatus); connectB->setEnabled(true); disconnectB->setEnabled(false); processINDIB->setEnabled(true); if (dev != nullptr && dev->getBaseDevice() && (dev->getBaseDevice()->getDriverInterface() & INDI::BaseDevice::TELESCOPE_INTERFACE)) { if (mountProcess.get() != nullptr) mountProcess->setEnabled(false); } // Do not disable modules on device connection loss, let them handle it /* else if (dev->getBaseDevice()->getDriverInterface() & INDI::BaseDevice::CCD_INTERFACE) { if (captureProcess.get() != nullptr) captureProcess->setEnabled(false); if (focusProcess.get() != nullptr) focusProcess->setEnabled(false); if (alignProcess.get() != nullptr) alignProcess->setEnabled(false); if (guideProcess.get() != nullptr) guideProcess->setEnabled(false); } else if (dev->getBaseDevice()->getDriverInterface() & INDI::BaseDevice::FOCUSER_INTERFACE) { if (focusProcess.get() != nullptr) focusProcess->setEnabled(false); }*/ } void Manager::setTelescope(ISD::GDInterface * scopeDevice) { //mount = scopeDevice; managedDevices[KSTARS_TELESCOPE] = scopeDevice; appendLogText(i18n("%1 is online.", scopeDevice->getDeviceName())); connect(scopeDevice, SIGNAL(numberUpdated(INumberVectorProperty *)), this, SLOT(processNewNumber(INumberVectorProperty *)), Qt::UniqueConnection); initMount(); mountProcess->setTelescope(scopeDevice); double primaryScopeFL = 0, primaryScopeAperture = 0, guideScopeFL = 0, guideScopeAperture = 0; getCurrentProfileTelescopeInfo(primaryScopeFL, primaryScopeAperture, guideScopeFL, guideScopeAperture); // Save telescope info in mount driver mountProcess->setTelescopeInfo(QList() << primaryScopeFL << primaryScopeAperture << guideScopeFL << guideScopeAperture); if (guideProcess.get() != nullptr) { guideProcess->setTelescope(scopeDevice); guideProcess->setTelescopeInfo(primaryScopeFL, primaryScopeAperture, guideScopeFL, guideScopeAperture); } if (alignProcess.get() != nullptr) { alignProcess->setTelescope(scopeDevice); alignProcess->setTelescopeInfo(primaryScopeFL, primaryScopeAperture, guideScopeFL, guideScopeAperture); } // if (domeProcess.get() != nullptr) // domeProcess->setTelescope(scopeDevice); ekosLiveClient->message()->sendMounts(); ekosLiveClient->message()->sendScopes(); } void Manager::setCCD(ISD::GDInterface * ccdDevice) { // No duplicates for (auto oneCCD : findDevices(KSTARS_CCD)) if (oneCCD == ccdDevice) return; managedDevices.insertMulti(KSTARS_CCD, ccdDevice); initCapture(); captureProcess->setEnabled(true); captureProcess->addCCD(ccdDevice); QString primaryCCD, guiderCCD; // Only look for primary & guider CCDs if we can tell a difference between them // otherwise rely on saved options if (currentProfile->ccd() != currentProfile->guider()) { foreach (ISD::GDInterface * device, findDevices(KSTARS_CCD)) { if (QString(device->getDeviceName()).startsWith(currentProfile->ccd(), Qt::CaseInsensitive)) primaryCCD = QString(device->getDeviceName()); else if (QString(device->getDeviceName()).startsWith(currentProfile->guider(), Qt::CaseInsensitive)) guiderCCD = QString(device->getDeviceName()); } } bool rc = false; if (Options::defaultCaptureCCD().isEmpty() == false) rc = captureProcess->setCamera(Options::defaultCaptureCCD()); if (rc == false && primaryCCD.isEmpty() == false) captureProcess->setCamera(primaryCCD); initFocus(); focusProcess->addCCD(ccdDevice); rc = false; if (Options::defaultFocusCCD().isEmpty() == false) rc = focusProcess->setCamera(Options::defaultFocusCCD()); if (rc == false && primaryCCD.isEmpty() == false) focusProcess->setCamera(primaryCCD); initAlign(); alignProcess->addCCD(ccdDevice); rc = false; if (Options::defaultAlignCCD().isEmpty() == false) rc = alignProcess->setCamera(Options::defaultAlignCCD()); if (rc == false && primaryCCD.isEmpty() == false) alignProcess->setCamera(primaryCCD); initGuide(); guideProcess->addCamera(ccdDevice); rc = false; if (Options::defaultGuideCCD().isEmpty() == false) rc = guideProcess->setCamera(Options::defaultGuideCCD()); if (rc == false && guiderCCD.isEmpty() == false) guideProcess->setCamera(guiderCCD); appendLogText(i18n("%1 is online.", ccdDevice->getDeviceName())); connect(ccdDevice, SIGNAL(numberUpdated(INumberVectorProperty *)), this, SLOT(processNewNumber(INumberVectorProperty *)), Qt::UniqueConnection); if (managedDevices.contains(KSTARS_TELESCOPE)) { alignProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); captureProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); guideProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); } } void Manager::setFilter(ISD::GDInterface * filterDevice) { // No duplicates if (findDevices(KSTARS_FILTER).contains(filterDevice) == false) managedDevices.insertMulti(KSTARS_FILTER, filterDevice); appendLogText(i18n("%1 filter is online.", filterDevice->getDeviceName())); initCapture(); connect(filterDevice, SIGNAL(numberUpdated(INumberVectorProperty *)), this, SLOT(processNewNumber(INumberVectorProperty *)), Qt::UniqueConnection); connect(filterDevice, SIGNAL(textUpdated(ITextVectorProperty *)), this, SLOT(processNewText(ITextVectorProperty *)), Qt::UniqueConnection); captureProcess->addFilter(filterDevice); initFocus(); focusProcess->addFilter(filterDevice); initAlign(); alignProcess->addFilter(filterDevice); } void Manager::setFocuser(ISD::GDInterface * focuserDevice) { // No duplicates if (findDevices(KSTARS_FOCUSER).contains(focuserDevice) == false) managedDevices.insertMulti(KSTARS_FOCUSER, focuserDevice); initCapture(); initFocus(); focusProcess->addFocuser(focuserDevice); if (Options::defaultFocusFocuser().isEmpty() == false) focusProcess->setFocuser(Options::defaultFocusFocuser()); appendLogText(i18n("%1 focuser is online.", focuserDevice->getDeviceName())); } void Manager::setDome(ISD::GDInterface * domeDevice) { managedDevices[KSTARS_DOME] = domeDevice; initDome(); domeProcess->setDome(domeDevice); if (captureProcess.get() != nullptr) captureProcess->setDome(domeDevice); if (alignProcess.get() != nullptr) alignProcess->setDome(domeDevice); // if (managedDevices.contains(KSTARS_TELESCOPE)) // domeProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); appendLogText(i18n("%1 is online.", domeDevice->getDeviceName())); } void Manager::setWeather(ISD::GDInterface * weatherDevice) { managedDevices[KSTARS_WEATHER] = weatherDevice; initWeather(); weatherProcess->setWeather(weatherDevice); appendLogText(i18n("%1 is online.", weatherDevice->getDeviceName())); } void Manager::setDustCap(ISD::GDInterface * dustCapDevice) { // No duplicates if (findDevices(KSTARS_AUXILIARY).contains(dustCapDevice) == false) managedDevices.insertMulti(KSTARS_AUXILIARY, dustCapDevice); initDustCap(); dustCapProcess->setDustCap(dustCapDevice); appendLogText(i18n("%1 is online.", dustCapDevice->getDeviceName())); if (captureProcess.get() != nullptr) captureProcess->setDustCap(dustCapDevice); DarkLibrary::Instance()->setRemoteCap(dustCapDevice); } void Manager::setLightBox(ISD::GDInterface * lightBoxDevice) { // No duplicates if (findDevices(KSTARS_AUXILIARY).contains(lightBoxDevice) == false) managedDevices.insertMulti(KSTARS_AUXILIARY, lightBoxDevice); if (captureProcess.get() != nullptr) captureProcess->setLightBox(lightBoxDevice); } void Manager::removeDevice(ISD::GDInterface * devInterface) { // switch (devInterface->getType()) // { // case KSTARS_CCD: // removeTabs(); // break; // default: // break; // } if (alignProcess) alignProcess->removeDevice(devInterface); if (captureProcess) captureProcess->removeDevice(devInterface); if (focusProcess) focusProcess->removeDevice(devInterface); if (mountProcess) mountProcess->removeDevice(devInterface); if (guideProcess) guideProcess->removeDevice(devInterface); if (domeProcess) domeProcess->removeDevice(devInterface); if (weatherProcess) weatherProcess->removeDevice(devInterface); if (dustCapProcess) { dustCapProcess->removeDevice(devInterface); DarkLibrary::Instance()->removeDevice(devInterface); } appendLogText(i18n("%1 is offline.", devInterface->getDeviceName())); // #1 Remove from Generic Devices // Generic devices are ALL the devices we receive from INDI server // Whether Ekos cares about them (i.e. selected equipment) or extra devices we // do not care about for (auto &device : genericDevices) { if (!strcmp(device->getDeviceName(), devInterface->getDeviceName())) { genericDevices.removeOne(device); } } // #2 Remove from Ekos Managed Device // Managed devices are devices selected by the user in the device profile for (auto &device : managedDevices.values()) { if (!strcmp(device->getDeviceName(), devInterface->getDeviceName())) { managedDevices.remove(managedDevices.key(device)); if (managedDevices.count() == 0) cleanDevices(); //break; } } if (managedDevices.isEmpty()) removeTabs(); } void Manager::processNewText(ITextVectorProperty * tvp) { if (!strcmp(tvp->name, "FILTER_NAME")) { ekosLiveClient.get()->message()->sendFilterWheels(); } if (!strcmp(tvp->name, "ACTIVE_DEVICES")) { syncActiveDevices(); } } void Manager::processNewNumber(INumberVectorProperty * nvp) { if (!strcmp(nvp->name, "TELESCOPE_INFO") && managedDevices.contains(KSTARS_TELESCOPE)) { if (guideProcess.get() != nullptr) { guideProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); //guideProcess->syncTelescopeInfo(); } if (alignProcess.get() != nullptr) { alignProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); //alignProcess->syncTelescopeInfo(); } if (mountProcess.get() != nullptr) { mountProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); //mountProcess->syncTelescopeInfo(); } return; } if (!strcmp(nvp->name, "CCD_INFO") || !strcmp(nvp->name, "GUIDER_INFO") || !strcmp(nvp->name, "CCD_FRAME") || !strcmp(nvp->name, "GUIDER_FRAME")) { if (focusProcess.get() != nullptr) focusProcess->syncCCDInfo(); if (guideProcess.get() != nullptr) guideProcess->syncCCDInfo(); if (alignProcess.get() != nullptr) alignProcess->syncCCDInfo(); return; } /* if (!strcmp(nvp->name, "FILTER_SLOT")) { if (captureProcess.get() != nullptr) captureProcess->checkFilter(); if (focusProcess.get() != nullptr) focusProcess->checkFilter(); if (alignProcess.get() != nullptr) alignProcess->checkFilter(); } */ } void Manager::processNewProperty(INDI::Property * prop) { ISD::GenericDevice * deviceInterface = qobject_cast(sender()); if (!strcmp(prop->getName(), "CONNECTION") && currentProfile->autoConnect) { // Check if we need to do any mappings const QString port = m_ProfileMapping.value(QString(deviceInterface->getDeviceName())).toString(); // If we don't have port mapping, then we connect immediately. if (port.isEmpty()) deviceInterface->Connect(); return; } if (!strcmp(prop->getName(), "DEVICE_PORT")) { // Check if we need to do any mappings const QString port = m_ProfileMapping.value(QString(deviceInterface->getDeviceName())).toString(); if (!port.isEmpty()) { ITextVectorProperty *tvp = prop->getText(); IUSaveText(&(tvp->tp[0]), port.toLatin1().data()); deviceInterface->getDriverInfo()->getClientManager()->sendNewText(tvp); // Now connect if we need to. if (currentProfile->autoConnect) deviceInterface->Connect(); return; } } // Check if we need to turn on DEBUG for logging purposes if (!strcmp(prop->getName(), "DEBUG")) { uint16_t interface = deviceInterface->getBaseDevice()->getDriverInterface(); if ( opsLogs->getINDIDebugInterface() & interface ) { // Check if we need to enable debug logging for the INDI drivers. ISwitchVectorProperty * debugSP = prop->getSwitch(); debugSP->sp[0].s = ISS_ON; debugSP->sp[1].s = ISS_OFF; deviceInterface->getDriverInfo()->getClientManager()->sendNewSwitch(debugSP); } } // Handle debug levels for logging purposes if (!strcmp(prop->getName(), "DEBUG_LEVEL")) { uint16_t interface = deviceInterface->getBaseDevice()->getDriverInterface(); // Check if the logging option for the specific device class is on and if the device interface matches it. if ( opsLogs->getINDIDebugInterface() & interface ) { // Turn on everything ISwitchVectorProperty * debugLevel = prop->getSwitch(); for (int i = 0; i < debugLevel->nsp; i++) debugLevel->sp[i].s = ISS_ON; deviceInterface->getDriverInfo()->getClientManager()->sendNewSwitch(debugLevel); } } if (!strcmp(prop->getName(), "ACTIVE_DEVICES")) { // Reset ACTIVE_FILTER aux0 and aux1 since we use them for overrides. ITextVectorProperty *tvp = prop->getText(); IText *tp = IUFindText(tvp, "ACTIVE_FILTER"); if (tp) { tp->aux0 = tp->aux1 = nullptr; } if (deviceInterface->getDriverInterface() > 0) syncActiveDevices(); } if (!strcmp(prop->getName(), "TELESCOPE_INFO") || !strcmp(prop->getName(), "TELESCOPE_SLEW_RATE") || !strcmp(prop->getName(), "TELESCOPE_PARK")) { ekosLiveClient.get()->message()->sendMounts(); ekosLiveClient.get()->message()->sendScopes(); } if (!strcmp(prop->getName(), "CCD_INFO") || !strcmp(prop->getName(), "CCD_TEMPERATURE") || !strcmp(prop->getName(), "CCD_ISO") || !strcmp(prop->getName(), "CCD_GAIN") || !strcmp(prop->getName(), "CCD_CONTROLS")) { ekosLiveClient.get()->message()->sendCameras(); ekosLiveClient.get()->media()->registerCameras(); } if (!strcmp(prop->getName(), "ABS_DOME_POSITION") || !strcmp(prop->getName(), "DOME_ABORT_MOTION") || !strcmp(prop->getName(), "DOME_PARK")) { ekosLiveClient.get()->message()->sendDomes(); } if (!strcmp(prop->getName(), "CAP_PARK") || !strcmp(prop->getName(), "FLAT_LIGHT_CONTROL")) { ekosLiveClient.get()->message()->sendCaps(); } if (!strcmp(prop->getName(), "FILTER_NAME")) ekosLiveClient.get()->message()->sendFilterWheels(); if (!strcmp(prop->getName(), "FILTER_NAME")) filterManager.data()->initFilterProperties(); if (!strcmp(prop->getName(), "CONFIRM_FILTER_SET")) filterManager.data()->initFilterProperties(); if (!strcmp(prop->getName(), "CCD_INFO") || !strcmp(prop->getName(), "GUIDER_INFO")) { if (focusProcess.get() != nullptr) focusProcess->syncCCDInfo(); if (guideProcess.get() != nullptr) guideProcess->syncCCDInfo(); if (alignProcess.get() != nullptr) alignProcess->syncCCDInfo(); return; } if (!strcmp(prop->getName(), "TELESCOPE_INFO") && managedDevices.contains(KSTARS_TELESCOPE)) { if (guideProcess.get() != nullptr) { guideProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); //guideProcess->syncTelescopeInfo(); } if (alignProcess.get() != nullptr) { alignProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); //alignProcess->syncTelescopeInfo(); } if (mountProcess.get() != nullptr) { mountProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); //mountProcess->syncTelescopeInfo(); } return; } if (!strcmp(prop->getName(), "GUIDER_EXPOSURE")) { foreach (ISD::GDInterface * device, findDevices(KSTARS_CCD)) { if (!strcmp(device->getDeviceName(), prop->getDeviceName())) { initCapture(); initGuide(); useGuideHead = true; captureProcess->addGuideHead(device); guideProcess->addGuideHead(device); bool rc = false; if (Options::defaultGuideCCD().isEmpty() == false) rc = guideProcess->setCamera(Options::defaultGuideCCD()); if (rc == false) guideProcess->setCamera(QString(device->getDeviceName()) + QString(" Guider")); return; } } return; } if (!strcmp(prop->getName(), "CCD_FRAME_TYPE")) { if (captureProcess.get() != nullptr) { foreach (ISD::GDInterface * device, findDevices(KSTARS_CCD)) { if (!strcmp(device->getDeviceName(), prop->getDeviceName())) { captureProcess->syncFrameType(device); return; } } } return; } if (!strcmp(prop->getName(), "CCD_ISO")) { if (captureProcess.get() != nullptr) captureProcess->checkCCD(); return; } if (!strcmp(prop->getName(), "TELESCOPE_PARK") && managedDevices.contains(KSTARS_TELESCOPE)) { if (captureProcess.get() != nullptr) captureProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); if (mountProcess.get() != nullptr) mountProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); return; } /* if (!strcmp(prop->getName(), "FILTER_NAME")) { if (captureProcess.get() != nullptr) captureProcess->checkFilter(); if (focusProcess.get() != nullptr) focusProcess->checkFilter(); if (alignProcess.get() != nullptr) alignProcess->checkFilter(); return; } */ if (!strcmp(prop->getName(), "ASTROMETRY_SOLVER")) { foreach (ISD::GDInterface * device, genericDevices) { if (!strcmp(device->getDeviceName(), prop->getDeviceName())) { initAlign(); alignProcess->setAstrometryDevice(device); break; } } } if (!strcmp(prop->getName(), "ABS_ROTATOR_ANGLE")) { managedDevices[KSTARS_ROTATOR] = deviceInterface; if (captureProcess.get() != nullptr) captureProcess->setRotator(deviceInterface); if (alignProcess.get() != nullptr) alignProcess->setRotator(deviceInterface); } if (!strcmp(prop->getName(), "GPS_REFRESH")) { managedDevices.insertMulti(KSTARS_AUXILIARY, deviceInterface); if (mountProcess.get() != nullptr) mountProcess->setGPS(deviceInterface); } if (focusProcess.get() != nullptr && strstr(prop->getName(), "FOCUS_")) { focusProcess->checkFocuser(); } } QList Manager::getAllDevices() { QList deviceList; QMapIterator i(managedDevices); while (i.hasNext()) { i.next(); deviceList.append(i.value()); } return deviceList; } QList Manager::findDevices(DeviceFamily type) { QList deviceList; QMapIterator i(managedDevices); while (i.hasNext()) { i.next(); if (i.key() == type) deviceList.append(i.value()); } return deviceList; } QList Manager::findDevicesByInterface(uint32_t interface) { QList deviceList; for (const auto dev : genericDevices) { uint32_t devInterface = dev->getDriverInterface(); if (devInterface & interface) deviceList.append(dev); } return deviceList; } void Manager::processTabChange() { QWidget * currentWidget = toolsWidget->currentWidget(); //if (focusProcess.get() != nullptr && currentWidget != focusProcess) //focusProcess->resetFrame(); if (alignProcess.get() && alignProcess.get() == currentWidget) { if (alignProcess->isEnabled() == false && captureProcess->isEnabled()) { if (managedDevices[KSTARS_CCD]->isConnected() && managedDevices.contains(KSTARS_TELESCOPE)) { if (alignProcess->isParserOK()) alignProcess->setEnabled(true); //#ifdef Q_OS_WIN else if (Options::solverBackend() == Ekos::Align::SOLVER_ASTROMETRYNET) { // If current setting is remote astrometry and profile doesn't contain // remote astrometry, then we switch to online solver. Otherwise, the whole align // module remains disabled and the user cannot change re-enable it since he cannot select online solver ProfileInfo * pi = getCurrentProfile(); if (Options::astrometrySolverType() == Ekos::Align::SOLVER_REMOTE && pi->aux1() != "Astrometry" && pi->aux2() != "Astrometry" && pi->aux3() != "Astrometry" && pi->aux4() != "Astrometry") { Options::setAstrometrySolverType(Ekos::Align::SOLVER_ONLINE); alignModule()->setAstrometrySolverType(Ekos::Align::SOLVER_ONLINE); alignProcess->setEnabled(true); } } //#endif } } alignProcess->checkCCD(); } else if (captureProcess.get() != nullptr && currentWidget == captureProcess.get()) { captureProcess->checkCCD(); } else if (focusProcess.get() != nullptr && currentWidget == focusProcess.get()) { focusProcess->checkCCD(); } else if (guideProcess.get() != nullptr && currentWidget == guideProcess.get()) { guideProcess->checkCCD(); } updateLog(); } void Manager::updateLog() { //if (enableLoggingCheck->isChecked() == false) //return; QWidget * currentWidget = toolsWidget->currentWidget(); if (currentWidget == setupTab) ekosLogOut->setPlainText(m_LogText.join("\n")); else if (currentWidget == alignProcess.get()) ekosLogOut->setPlainText(alignProcess->getLogText()); else if (currentWidget == captureProcess.get()) ekosLogOut->setPlainText(captureProcess->getLogText()); else if (currentWidget == focusProcess.get()) ekosLogOut->setPlainText(focusProcess->getLogText()); else if (currentWidget == guideProcess.get()) ekosLogOut->setPlainText(guideProcess->getLogText()); else if (currentWidget == mountProcess.get()) ekosLogOut->setPlainText(mountProcess->getLogText()); else if (currentWidget == schedulerProcess.get()) ekosLogOut->setPlainText(schedulerProcess->getLogText()); else if (currentWidget == observatoryProcess.get()) ekosLogOut->setPlainText(observatoryProcess->getLogText()); #ifdef Q_OS_OSX repaint(); //This is a band-aid for a bug in QT 5.10.0 #endif } void Manager::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) << text; emit newLog(text); updateLog(); } void Manager::clearLog() { QWidget * currentWidget = toolsWidget->currentWidget(); if (currentWidget == setupTab) { m_LogText.clear(); updateLog(); } else if (currentWidget == alignProcess.get()) alignProcess->clearLog(); else if (currentWidget == captureProcess.get()) captureProcess->clearLog(); else if (currentWidget == focusProcess.get()) focusProcess->clearLog(); else if (currentWidget == guideProcess.get()) guideProcess->clearLog(); else if (currentWidget == mountProcess.get()) mountProcess->clearLog(); else if (currentWidget == schedulerProcess.get()) schedulerProcess->clearLog(); else if (currentWidget == observatoryProcess.get()) observatoryProcess->clearLog(); } void Manager::initCapture() { if (captureProcess.get() != nullptr) return; captureProcess.reset(new Ekos::Capture()); captureProcess->setEnabled(false); int index = toolsWidget->addTab(captureProcess.get(), QIcon(":/icons/ekos_ccd.png"), ""); toolsWidget->tabBar()->setTabToolTip(index, i18nc("Charge-Coupled Device", "CCD")); if (Options::ekosLeftIcons()) { QTransform trans; trans.rotate(90); QIcon icon = toolsWidget->tabIcon(index); QPixmap pix = icon.pixmap(QSize(48, 48)); icon = QIcon(pix.transformed(trans)); toolsWidget->setTabIcon(index, icon); } connect(captureProcess.get(), &Ekos::Capture::newLog, this, &Ekos::Manager::updateLog); connect(captureProcess.get(), &Ekos::Capture::newStatus, this, &Ekos::Manager::updateCaptureStatus); connect(captureProcess.get(), &Ekos::Capture::newImage, this, &Ekos::Manager::updateCaptureProgress); connect(captureProcess.get(), &Ekos::Capture::newSequenceImage, [&](const QString & filename, const QString & previewFITS) { if (Options::useSummaryPreview() && QFile::exists(filename)) { if (Options::autoImageToFITS()) { if (previewFITS.isEmpty() == false) summaryPreview->loadFITS(previewFITS); } else summaryPreview->loadFITS(filename); } }); connect(captureProcess.get(), &Ekos::Capture::newDownloadProgress, this, &Ekos::Manager::updateDownloadProgress); connect(captureProcess.get(), &Ekos::Capture::newExposureProgress, this, &Ekos::Manager::updateExposureProgress); captureGroup->setEnabled(true); sequenceProgress->setEnabled(true); captureProgress->setEnabled(true); imageProgress->setEnabled(true); captureProcess->setFilterManager(filterManager); if (!capturePI) { capturePI = new QProgressIndicator(captureProcess.get()); captureStatusLayout->insertWidget(0, capturePI); } foreach (ISD::GDInterface * device, findDevices(KSTARS_AUXILIARY)) { if (device->getBaseDevice()->getDriverInterface() & INDI::BaseDevice::DUSTCAP_INTERFACE) captureProcess->setDustCap(device); if (device->getBaseDevice()->getDriverInterface() & INDI::BaseDevice::LIGHTBOX_INTERFACE) captureProcess->setLightBox(device); } if (managedDevices.contains(KSTARS_DOME)) { captureProcess->setDome(managedDevices[KSTARS_DOME]); } if (managedDevices.contains(KSTARS_ROTATOR)) { captureProcess->setRotator(managedDevices[KSTARS_ROTATOR]); } connectModules(); emit newModule("Capture"); } void Manager::initAlign() { if (alignProcess.get() != nullptr) return; alignProcess.reset(new Ekos::Align(currentProfile)); double primaryScopeFL = 0, primaryScopeAperture = 0, guideScopeFL = 0, guideScopeAperture = 0; getCurrentProfileTelescopeInfo(primaryScopeFL, primaryScopeAperture, guideScopeFL, guideScopeAperture); alignProcess->setTelescopeInfo(primaryScopeFL, primaryScopeAperture, guideScopeFL, guideScopeAperture); alignProcess->setEnabled(false); int index = toolsWidget->addTab(alignProcess.get(), QIcon(":/icons/ekos_align.png"), ""); toolsWidget->tabBar()->setTabToolTip(index, i18n("Align")); connect(alignProcess.get(), &Ekos::Align::newLog, this, &Ekos::Manager::updateLog); if (Options::ekosLeftIcons()) { QTransform trans; trans.rotate(90); QIcon icon = toolsWidget->tabIcon(index); QPixmap pix = icon.pixmap(QSize(48, 48)); icon = QIcon(pix.transformed(trans)); toolsWidget->setTabIcon(index, icon); } alignProcess->setFilterManager(filterManager); if (managedDevices.contains(KSTARS_DOME)) { alignProcess->setDome(managedDevices[KSTARS_DOME]); } if (managedDevices.contains(KSTARS_ROTATOR)) { alignProcess->setRotator(managedDevices[KSTARS_ROTATOR]); } connectModules(); emit newModule("Align"); } void Manager::initFocus() { if (focusProcess.get() != nullptr) return; focusProcess.reset(new Ekos::Focus()); int index = toolsWidget->addTab(focusProcess.get(), QIcon(":/icons/ekos_focus.png"), ""); toolsWidget->tabBar()->setTabToolTip(index, i18n("Focus")); // Focus <---> Manager connections connect(focusProcess.get(), &Ekos::Focus::newLog, this, &Ekos::Manager::updateLog); connect(focusProcess.get(), &Ekos::Focus::newStatus, this, &Ekos::Manager::setFocusStatus); connect(focusProcess.get(), &Ekos::Focus::newStarPixmap, this, &Ekos::Manager::updateFocusStarPixmap); connect(focusProcess.get(), &Ekos::Focus::newProfilePixmap, this, &Ekos::Manager::updateFocusProfilePixmap); connect(focusProcess.get(), &Ekos::Focus::newHFR, this, &Ekos::Manager::updateCurrentHFR); // Focus <---> Filter Manager connections focusProcess->setFilterManager(filterManager); connect(filterManager.data(), &Ekos::FilterManager::checkFocus, focusProcess.get(), &Ekos::Focus::checkFocus, Qt::UniqueConnection); connect(focusProcess.get(), &Ekos::Focus::newStatus, filterManager.data(), &Ekos::FilterManager::setFocusStatus, Qt::UniqueConnection); connect(filterManager.data(), &Ekos::FilterManager::newFocusOffset, focusProcess.get(), &Ekos::Focus::adjustFocusOffset, Qt::UniqueConnection); connect(focusProcess.get(), &Ekos::Focus::focusPositionAdjusted, filterManager.data(), &Ekos::FilterManager::setFocusOffsetComplete, Qt::UniqueConnection); connect(focusProcess.get(), &Ekos::Focus::absolutePositionChanged, filterManager.data(), &Ekos::FilterManager::setFocusAbsolutePosition, Qt::UniqueConnection); if (Options::ekosLeftIcons()) { QTransform trans; trans.rotate(90); QIcon icon = toolsWidget->tabIcon(index); QPixmap pix = icon.pixmap(QSize(48, 48)); icon = QIcon(pix.transformed(trans)); toolsWidget->setTabIcon(index, icon); } focusGroup->setEnabled(true); if (!focusPI) { focusPI = new QProgressIndicator(focusProcess.get()); focusStatusLayout->insertWidget(0, focusPI); } connectModules(); emit newModule("Focus"); } void Manager::updateCurrentHFR(double newHFR, int position) { currentHFR->setText(QString("%1").arg(newHFR, 0, 'f', 2) + " px"); QJsonObject cStatus = { {"hfr", newHFR}, {"pos", position} }; ekosLiveClient.get()->message()->updateFocusStatus(cStatus); } void Manager::updateSigmas(double ra, double de) { errRA->setText(QString::number(ra, 'f', 2) + "\""); errDEC->setText(QString::number(de, 'f', 2) + "\""); QJsonObject cStatus = { {"rarms", ra}, {"derms", de} }; ekosLiveClient.get()->message()->updateGuideStatus(cStatus); } void Manager::initMount() { if (mountProcess.get() != nullptr) return; mountProcess.reset(new Ekos::Mount()); int index = toolsWidget->addTab(mountProcess.get(), QIcon(":/icons/ekos_mount.png"), ""); toolsWidget->tabBar()->setTabToolTip(index, i18n("Mount")); connect(mountProcess.get(), &Ekos::Mount::newLog, this, &Ekos::Manager::updateLog); connect(mountProcess.get(), &Ekos::Mount::newCoords, this, &Ekos::Manager::updateMountCoords); connect(mountProcess.get(), &Ekos::Mount::newStatus, this, &Ekos::Manager::updateMountStatus); connect(mountProcess.get(), &Ekos::Mount::newTarget, [&](const QString & target) { mountTarget->setText(target); ekosLiveClient.get()->message()->updateMountStatus(QJsonObject({{"target", target}})); }); connect(mountProcess.get(), &Ekos::Mount::slewRateChanged, [&](int slewRate) { QJsonObject status = { { "slewRate", slewRate} }; ekosLiveClient.get()->message()->updateMountStatus(status); } ); foreach (ISD::GDInterface * device, findDevices(KSTARS_AUXILIARY)) { if (device->getBaseDevice()->getDriverInterface() & INDI::BaseDevice::GPS_INTERFACE) mountProcess->setGPS(device); } if (Options::ekosLeftIcons()) { QTransform trans; trans.rotate(90); QIcon icon = toolsWidget->tabIcon(index); QPixmap pix = icon.pixmap(QSize(48, 48)); icon = QIcon(pix.transformed(trans)); toolsWidget->setTabIcon(index, icon); } if (!mountPI) { mountPI = new QProgressIndicator(mountProcess.get()); mountStatusLayout->insertWidget(0, mountPI); } mountGroup->setEnabled(true); connectModules(); emit newModule("Mount"); } void Manager::initGuide() { if (guideProcess.get() == nullptr) { guideProcess.reset(new Ekos::Guide()); double primaryScopeFL = 0, primaryScopeAperture = 0, guideScopeFL = 0, guideScopeAperture = 0; getCurrentProfileTelescopeInfo(primaryScopeFL, primaryScopeAperture, guideScopeFL, guideScopeAperture); // Save telescope info in mount driver guideProcess->setTelescopeInfo(primaryScopeFL, primaryScopeAperture, guideScopeFL, guideScopeAperture); } //if ( (haveGuider || ccdCount > 1 || useGuideHead) && useST4 && toolsWidget->indexOf(guideProcess) == -1) if ((findDevices(KSTARS_CCD).isEmpty() == false || useGuideHead) && useST4 && toolsWidget->indexOf(guideProcess.get()) == -1) { //if (mount && mount->isConnected()) if (managedDevices.contains(KSTARS_TELESCOPE) && managedDevices[KSTARS_TELESCOPE]->isConnected()) guideProcess->setTelescope(managedDevices[KSTARS_TELESCOPE]); int index = toolsWidget->addTab(guideProcess.get(), QIcon(":/icons/ekos_guide.png"), ""); toolsWidget->tabBar()->setTabToolTip(index, i18n("Guide")); connect(guideProcess.get(), &Ekos::Guide::newLog, this, &Ekos::Manager::updateLog); guideGroup->setEnabled(true); if (!guidePI) { guidePI = new QProgressIndicator(guideProcess.get()); guideStatusLayout->insertWidget(0, guidePI); } connect(guideProcess.get(), &Ekos::Guide::newStatus, this, &Ekos::Manager::updateGuideStatus); connect(guideProcess.get(), &Ekos::Guide::newStarPixmap, this, &Ekos::Manager::updateGuideStarPixmap); connect(guideProcess.get(), &Ekos::Guide::newProfilePixmap, this, &Ekos::Manager::updateGuideProfilePixmap); connect(guideProcess.get(), &Ekos::Guide::newAxisSigma, this, &Ekos::Manager::updateSigmas); if (Options::ekosLeftIcons()) { QTransform trans; trans.rotate(90); QIcon icon = toolsWidget->tabIcon(index); QPixmap pix = icon.pixmap(QSize(48, 48)); icon = QIcon(pix.transformed(trans)); toolsWidget->setTabIcon(index, icon); } } connectModules(); emit newModule("Guide"); } void Manager::initDome() { if (domeProcess.get() != nullptr) return; domeProcess.reset(new Ekos::Dome()); connect(domeProcess.get(), &Ekos::Dome::newStatus, [&](ISD::Dome::Status newStatus) { // For roll-off domes // cw ---> unparking // ccw --> parking if (domeProcess->isRolloffRoof() && (newStatus == ISD::Dome::DOME_MOVING_CW || newStatus == ISD::Dome::DOME_MOVING_CCW)) { newStatus = (newStatus == ISD::Dome::DOME_MOVING_CW) ? ISD::Dome::DOME_UNPARKING : ISD::Dome::DOME_PARKING; } QJsonObject status = { { "status", ISD::Dome::getStatusString(newStatus)} }; ekosLiveClient.get()->message()->updateDomeStatus(status); }); connect(domeProcess.get(), &Ekos::Dome::azimuthPositionChanged, [&](double pos) { QJsonObject status = { { "az", pos} }; ekosLiveClient.get()->message()->updateDomeStatus(status); }); initObservatory(nullptr, domeProcess.get()); emit newModule("Dome"); ekosLiveClient->message()->sendDomes(); } void Manager::initWeather() { if (weatherProcess.get() != nullptr) return; weatherProcess.reset(new Ekos::Weather()); initObservatory(weatherProcess.get(), nullptr); emit newModule("Weather"); } void Manager::initObservatory(Weather *weather, Dome *dome) { if (observatoryProcess.get() == nullptr) { // Initialize the Observatory Module observatoryProcess.reset(new Ekos::Observatory()); int index = toolsWidget->addTab(observatoryProcess.get(), QIcon(":/icons/ekos_observatory.png"), ""); toolsWidget->tabBar()->setTabToolTip(index, i18n("Observatory")); connect(observatoryProcess.get(), &Ekos::Observatory::newLog, this, &Ekos::Manager::updateLog); } Observatory *obs = observatoryProcess.get(); if (weather != nullptr) obs->getWeatherModel()->initModel(weather); if (dome != nullptr) obs->getDomeModel()->initModel(dome); emit newModule("Observatory"); } void Manager::initDustCap() { if (dustCapProcess.get() != nullptr) return; dustCapProcess.reset(new Ekos::DustCap()); connect(dustCapProcess.get(), &Ekos::DustCap::newStatus, [&](ISD::DustCap::Status newStatus) { QJsonObject status = { { "status", ISD::DustCap::getStatusString(newStatus)} }; ekosLiveClient.get()->message()->updateCapStatus(status); }); connect(dustCapProcess.get(), &Ekos::DustCap::lightToggled, [&](bool enabled) { QJsonObject status = { { "lightS", enabled} }; ekosLiveClient.get()->message()->updateCapStatus(status); }); connect(dustCapProcess.get(), &Ekos::DustCap::lightIntensityChanged, [&](uint16_t value) { QJsonObject status = { { "lightB", value} }; ekosLiveClient.get()->message()->updateCapStatus(status); }); emit newModule("DustCap"); ekosLiveClient->message()->sendCaps(); } void Manager::setST4(ISD::ST4 * st4Driver) { appendLogText(i18n("Guider port from %1 is ready.", st4Driver->getDeviceName())); useST4 = true; initGuide(); guideProcess->addST4(st4Driver); if (Options::defaultST4Driver().isEmpty() == false) guideProcess->setST4(Options::defaultST4Driver()); } void Manager::removeTabs() { disconnect(toolsWidget, &QTabWidget::currentChanged, this, &Ekos::Manager::processTabChange); for (int i = 2; i < toolsWidget->count(); i++) toolsWidget->removeTab(i); alignProcess.reset(); captureProcess.reset(); focusProcess.reset(); guideProcess.reset(); mountProcess.reset(); domeProcess.reset(); weatherProcess.reset(); observatoryProcess.reset(); dustCapProcess.reset(); managedDevices.clear(); connect(toolsWidget, &QTabWidget::currentChanged, this, &Ekos::Manager::processTabChange, Qt::UniqueConnection); } bool Manager::isRunning(const QString &process) { QProcess ps; #ifdef Q_OS_OSX ps.start("pgrep", QStringList() << process); ps.waitForFinished(); QString output = ps.readAllStandardOutput(); return output.length() > 0; #else ps.start("ps", QStringList() << "-o" << "comm" << "--no-headers" << "-C" << process); ps.waitForFinished(); QString output = ps.readAllStandardOutput(); return output.contains(process); #endif } void Manager::addObjectToScheduler(SkyObject * object) { if (schedulerProcess.get() != nullptr) schedulerProcess->addObject(object); } QString Manager::getCurrentJobName() { return schedulerProcess->getCurrentJobName(); } bool Manager::setProfile(const QString &profileName) { int index = profileCombo->findText(profileName); if (index < 0) return false; profileCombo->setCurrentIndex(index); return true; } void Manager::editNamedProfile(const QJsonObject &profileInfo) { ProfileEditor editor(this); setProfile(profileInfo["name"].toString()); currentProfile = getCurrentProfile(); editor.setPi(currentProfile); editor.setSettings(profileInfo); editor.saveProfile(); } void Manager::addNamedProfile(const QJsonObject &profileInfo) { ProfileEditor editor(this); editor.setSettings(profileInfo); editor.saveProfile(); profiles.clear(); loadProfiles(); profileCombo->setCurrentIndex(profileCombo->count() - 1); currentProfile = getCurrentProfile(); } void Manager::deleteNamedProfile(const QString &name) { currentProfile = getCurrentProfile(); for (auto &pi : profiles) { // Do not delete an actively running profile // Do not delete simulator profile if (pi->name == "Simulators" || pi->name != name || (pi.get() == currentProfile && ekosStatus() != Idle)) continue; KStarsData::Instance()->userdb()->DeleteProfile(pi.get()); profiles.clear(); loadProfiles(); currentProfile = getCurrentProfile(); return; } } QJsonObject Manager::getNamedProfile(const QString &name) { QJsonObject profileInfo; // Get current profile for (auto &pi : profiles) { if (name == pi->name) return pi->toJson(); } return QJsonObject(); } QStringList Manager::getProfiles() { QStringList profiles; for (int i = 0; i < profileCombo->count(); i++) profiles << profileCombo->itemText(i); return profiles; } void Manager::addProfile() { ProfileEditor editor(this); if (editor.exec() == QDialog::Accepted) { profiles.clear(); loadProfiles(); profileCombo->setCurrentIndex(profileCombo->count() - 1); } currentProfile = getCurrentProfile(); } void Manager::editProfile() { ProfileEditor editor(this); currentProfile = getCurrentProfile(); editor.setPi(currentProfile); if (editor.exec() == QDialog::Accepted) { int currentIndex = profileCombo->currentIndex(); profiles.clear(); loadProfiles(); profileCombo->setCurrentIndex(currentIndex); } currentProfile = getCurrentProfile(); } void Manager::deleteProfile() { currentProfile = getCurrentProfile(); if (currentProfile->name == "Simulators") return; auto executeDeleteProfile = [&]() { KStarsData::Instance()->userdb()->DeleteProfile(currentProfile); profiles.clear(); loadProfiles(); currentProfile = getCurrentProfile(); }; connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, executeDeleteProfile]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); executeDeleteProfile(); }); KSMessageBox::Instance()->questionYesNo(i18n("Are you sure you want to delete the profile?"), i18n("Confirm Delete")); } void Manager::wizardProfile() { ProfileWizard wz; if (wz.exec() != QDialog::Accepted) return; ProfileEditor editor(this); editor.setProfileName(wz.profileName); editor.setAuxDrivers(wz.selectedAuxDrivers()); if (wz.useInternalServer == false) editor.setHostPort(wz.host, wz.port); editor.setWebManager(wz.useWebManager); editor.setGuiderType(wz.selectedExternalGuider()); // Disable connection options editor.setConnectionOptionsEnabled(false); if (editor.exec() == QDialog::Accepted) { profiles.clear(); loadProfiles(); profileCombo->setCurrentIndex(profileCombo->count() - 1); } currentProfile = getCurrentProfile(); } ProfileInfo * Manager::getCurrentProfile() { ProfileInfo * currProfile = nullptr; // Get current profile for (auto &pi : profiles) { if (profileCombo->currentText() == pi->name) { currProfile = pi.get(); break; } } return currProfile; } void Manager::updateProfileLocation(ProfileInfo * pi) { if (pi->city.isEmpty() == false) { bool cityFound = KStars::Instance()->setGeoLocation(pi->city, pi->province, pi->country); if (cityFound) appendLogText(i18n("Site location updated to %1.", KStarsData::Instance()->geo()->fullName())); else appendLogText(i18n("Failed to update site location to %1. City not found.", KStarsData::Instance()->geo()->fullName())); } } void Manager::updateMountStatus(ISD::Telescope::Status status) { static ISD::Telescope::Status lastStatus = ISD::Telescope::MOUNT_IDLE; if (status == lastStatus) return; lastStatus = status; mountStatus->setText(dynamic_cast(managedDevices[KSTARS_TELESCOPE])->getStatusString(status)); mountStatus->setStyleSheet(QString()); switch (status) { case ISD::Telescope::MOUNT_PARKING: case ISD::Telescope::MOUNT_SLEWING: case ISD::Telescope::MOUNT_MOVING: mountPI->setColor(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor"))); if (mountPI->isAnimated() == false) mountPI->startAnimation(); break; case ISD::Telescope::MOUNT_TRACKING: mountPI->setColor(Qt::darkGreen); if (mountPI->isAnimated() == false) mountPI->startAnimation(); break; case ISD::Telescope::MOUNT_PARKED: mountStatus->setStyleSheet("font-weight:bold;background-color:red;border:2px solid black;"); if (mountPI->isAnimated()) mountPI->stopAnimation(); break; default: if (mountPI->isAnimated()) mountPI->stopAnimation(); } QJsonObject cStatus = { {"status", mountStatus->text()} }; ekosLiveClient.get()->message()->updateMountStatus(cStatus); } void Manager::updateMountCoords(const QString &ra, const QString &dec, const QString &az, const QString &alt) { raOUT->setText(ra); decOUT->setText(dec); azOUT->setText(az); altOUT->setText(alt); QJsonObject cStatus = { {"ra", dms::fromString(ra, false).Degrees()}, {"de", dms::fromString(dec, true).Degrees()}, {"az", dms::fromString(az, true).Degrees()}, {"at", dms::fromString(alt, true).Degrees()}, }; ekosLiveClient.get()->message()->updateMountStatus(cStatus); } void Manager::updateCaptureStatus(Ekos::CaptureState status) { captureStatus->setText(Ekos::getCaptureStatusString(status)); captureProgress->setValue(captureProcess->getProgressPercentage()); overallCountDown.setHMS(0, 0, 0); overallCountDown = overallCountDown.addSecs(captureProcess->getOverallRemainingTime()); sequenceCountDown.setHMS(0, 0, 0); sequenceCountDown = sequenceCountDown.addSecs(captureProcess->getActiveJobRemainingTime()); if (status != Ekos::CAPTURE_ABORTED && status != Ekos::CAPTURE_COMPLETE && status != Ekos::CAPTURE_IDLE) { if (status == Ekos::CAPTURE_CAPTURING) capturePI->setColor(Qt::darkGreen); else capturePI->setColor(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor"))); if (capturePI->isAnimated() == false) { capturePI->startAnimation(); countdownTimer.start(); } } else { if (capturePI->isAnimated()) { capturePI->stopAnimation(); countdownTimer.stop(); if (focusStatus->text() == "Complete") { if (focusPI->isAnimated()) focusPI->stopAnimation(); } imageProgress->setValue(0); sequenceLabel->setText(i18n("Sequence")); imageRemainingTime->setText("--:--:--"); overallRemainingTime->setText("--:--:--"); sequenceRemainingTime->setText("--:--:--"); } } QJsonObject cStatus = { {"status", captureStatus->text()}, {"seqt", sequenceRemainingTime->text()}, {"ovt", overallRemainingTime->text()} }; ekosLiveClient.get()->message()->updateCaptureStatus(cStatus); } void Manager::updateCaptureProgress(Ekos::SequenceJob * job) { // Image is set to nullptr only on initial capture start up int completed = job->getCompleted(); // if (job->getUploadMode() == ISD::CCD::UPLOAD_LOCAL) // completed = job->getCompleted() + 1; // else // completed = job->isPreview() ? job->getCompleted() : job->getCompleted() + 1; if (job->isPreview() == false) { sequenceLabel->setText(QString("Job # %1/%2 %3 (%4/%5)") .arg(captureProcess->getActiveJobID() + 1) .arg(captureProcess->getJobCount()) .arg(job->getFullPrefix()) .arg(completed) .arg(job->getCount())); } else sequenceLabel->setText(i18n("Preview")); sequenceProgress->setRange(0, job->getCount()); sequenceProgress->setValue(completed); QJsonObject status = { {"seqv", completed}, {"seqr", job->getCount()}, {"seql", sequenceLabel->text()} }; ekosLiveClient.get()->message()->updateCaptureStatus(status); if (job->getStatus() == SequenceJob::JOB_BUSY) { QString uuid = QUuid::createUuid().toString(); uuid = uuid.remove(QRegularExpression("[-{}]")); // FITSView *image = job->getActiveChip()->getImageView(FITS_NORMAL); // ekosLiveClient.get()->media()->sendPreviewImage(image, uuid); // ekosLiveClient.get()->cloud()->sendPreviewImage(image, uuid); QString filename = job->property("filename").toString(); ekosLiveClient.get()->media()->sendPreviewImage(filename, uuid); if (job->isPreview() == false) ekosLiveClient.get()->cloud()->sendPreviewImage(filename, uuid); } } void Manager::updateDownloadProgress(double timeLeft) { imageCountDown.setHMS(0, 0, 0); imageCountDown = imageCountDown.addSecs(timeLeft); imageRemainingTime->setText(imageCountDown.toString("hh:mm:ss")); } void Manager::updateExposureProgress(Ekos::SequenceJob * job) { imageCountDown.setHMS(0, 0, 0); imageCountDown = imageCountDown.addSecs(job->getExposeLeft() + captureProcess->getEstimatedDownloadTime()); if (imageCountDown.hour() == 23) imageCountDown.setHMS(0, 0, 0); imageProgress->setRange(0, job->getExposure()); imageProgress->setValue(job->getExposeLeft()); imageRemainingTime->setText(imageCountDown.toString("hh:mm:ss")); QJsonObject status { {"expv", job->getExposeLeft()}, {"expr", job->getExposure()} }; ekosLiveClient.get()->message()->updateCaptureStatus(status); } void Manager::updateCaptureCountDown() { overallCountDown = overallCountDown.addSecs(-1); if (overallCountDown.hour() == 23) overallCountDown.setHMS(0, 0, 0); sequenceCountDown = sequenceCountDown.addSecs(-1); if (sequenceCountDown.hour() == 23) sequenceCountDown.setHMS(0, 0, 0); overallRemainingTime->setText(overallCountDown.toString("hh:mm:ss")); sequenceRemainingTime->setText(sequenceCountDown.toString("hh:mm:ss")); QJsonObject status = { {"seqt", sequenceRemainingTime->text()}, {"ovt", overallRemainingTime->text()} }; ekosLiveClient.get()->message()->updateCaptureStatus(status); } void Manager::updateFocusStarPixmap(QPixmap &starPixmap) { if (starPixmap.isNull()) return; focusStarPixmap.reset(new QPixmap(starPixmap)); focusStarImage->setPixmap(focusStarPixmap->scaled(focusStarImage->width(), focusStarImage->height(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); } void Manager::updateFocusProfilePixmap(QPixmap &profilePixmap) { if (profilePixmap.isNull()) return; focusProfileImage->setPixmap(profilePixmap); } void Manager::setFocusStatus(Ekos::FocusState status) { focusStatus->setText(Ekos::getFocusStatusString(status)); if (status >= Ekos::FOCUS_PROGRESS) { focusPI->setColor(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor"))); if (focusPI->isAnimated() == false) focusPI->startAnimation(); } else if (status == Ekos::FOCUS_COMPLETE && Options::enforceAutofocus() && captureProcess->getActiveJobID() != -1) { focusPI->setColor(Qt::darkGreen); if (focusPI->isAnimated() == false) focusPI->startAnimation(); } else { if (focusPI->isAnimated()) focusPI->stopAnimation(); } QJsonObject cStatus = { {"status", focusStatus->text()} }; ekosLiveClient.get()->message()->updateFocusStatus(cStatus); } void Manager::updateGuideStatus(Ekos::GuideState status) { guideStatus->setText(Ekos::getGuideStatusString(status)); switch (status) { case Ekos::GUIDE_IDLE: case Ekos::GUIDE_CALIBRATION_ERROR: case Ekos::GUIDE_ABORTED: case Ekos::GUIDE_SUSPENDED: case Ekos::GUIDE_DITHERING_ERROR: case Ekos::GUIDE_CALIBRATION_SUCESS: if (guidePI->isAnimated()) guidePI->stopAnimation(); break; case Ekos::GUIDE_CALIBRATING: guidePI->setColor(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor"))); if (guidePI->isAnimated() == false) guidePI->startAnimation(); break; case Ekos::GUIDE_GUIDING: guidePI->setColor(Qt::darkGreen); if (guidePI->isAnimated() == false) guidePI->startAnimation(); break; case Ekos::GUIDE_DITHERING: guidePI->setColor(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor"))); if (guidePI->isAnimated() == false) guidePI->startAnimation(); break; case Ekos::GUIDE_DITHERING_SUCCESS: guidePI->setColor(Qt::darkGreen); if (guidePI->isAnimated() == false) guidePI->startAnimation(); break; default: if (guidePI->isAnimated()) guidePI->stopAnimation(); break; } QJsonObject cStatus = { {"status", guideStatus->text()} }; ekosLiveClient.get()->message()->updateGuideStatus(cStatus); } void Manager::updateGuideStarPixmap(QPixmap &starPix) { if (starPix.isNull()) return; guideStarPixmap.reset(new QPixmap(starPix)); guideStarImage->setPixmap(guideStarPixmap->scaled(guideStarImage->width(), guideStarImage->height(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); } void Manager::updateGuideProfilePixmap(QPixmap &profilePix) { if (profilePix.isNull()) return; guideProfileImage->setPixmap(profilePix); } void Manager::setTarget(SkyObject * o) { mountTarget->setText(o->name()); ekosLiveClient.get()->message()->updateMountStatus(QJsonObject({{"target", o->name()}})); } void Manager::showEkosOptions() { QWidget * currentWidget = toolsWidget->currentWidget(); if (alignProcess.get() && alignProcess.get() == currentWidget) { KConfigDialog * alignSettings = KConfigDialog::exists("alignsettings"); if (alignSettings) { alignSettings->setEnabled(true); alignSettings->show(); } return; } if (guideProcess.get() && guideProcess.get() == currentWidget) { KConfigDialog::showDialog("guidesettings"); return; } if (ekosOptionsWidget == nullptr) { optionsB->click(); } else if (KConfigDialog::showDialog("settings")) { KConfigDialog * cDialog = KConfigDialog::exists("settings"); cDialog->setCurrentPage(ekosOptionsWidget); } } void Manager::getCurrentProfileTelescopeInfo(double &primaryFocalLength, double &primaryAperture, double &guideFocalLength, double &guideAperture) { ProfileInfo * pi = getCurrentProfile(); if (pi) { int primaryScopeID = 0, guideScopeID = 0; primaryScopeID = pi->primaryscope; guideScopeID = pi->guidescope; if (primaryScopeID > 0 || guideScopeID > 0) { // Get all OAL equipment filter list QList m_scopeList; KStarsData::Instance()->userdb()->GetAllScopes(m_scopeList); - foreach(OAL::Scope * oneScope, m_scopeList) + for(auto oneScope : m_scopeList) { if (oneScope->id().toInt() == primaryScopeID) { primaryFocalLength = oneScope->focalLength(); primaryAperture = oneScope->aperture(); } if (oneScope->id().toInt() == guideScopeID) { guideFocalLength = oneScope->focalLength(); guideAperture = oneScope->aperture(); } } + + qDeleteAll(m_scopeList); } } } void Manager::updateDebugInterfaces() { KSUtils::Logging::SyncFilterRules(); for (ISD::GDInterface * device : genericDevices) { INDI::Property * debugProp = device->getProperty("DEBUG"); ISwitchVectorProperty * debugSP = nullptr; if (debugProp) debugSP = debugProp->getSwitch(); else continue; // Check if the debug interface matches the driver device class if ( ( opsLogs->getINDIDebugInterface() & device->getBaseDevice()->getDriverInterface() ) && debugSP->sp[0].s != ISS_ON) { debugSP->sp[0].s = ISS_ON; debugSP->sp[1].s = ISS_OFF; device->getDriverInfo()->getClientManager()->sendNewSwitch(debugSP); appendLogText(i18n("Enabling debug logging for %1...", device->getDeviceName())); } else if ( !( opsLogs->getINDIDebugInterface() & device->getBaseDevice()->getDriverInterface() ) && debugSP->sp[0].s != ISS_OFF) { debugSP->sp[0].s = ISS_OFF; debugSP->sp[1].s = ISS_ON; device->getDriverInfo()->getClientManager()->sendNewSwitch(debugSP); appendLogText(i18n("Disabling debug logging for %1...", device->getDeviceName())); } if (opsLogs->isINDISettingsChanged()) device->setConfig(SAVE_CONFIG); } } void Manager::watchDebugProperty(ISwitchVectorProperty * svp) { if (!strcmp(svp->name, "DEBUG")) { ISD::GenericDevice * deviceInterface = qobject_cast(sender()); // We don't process pure general interfaces if (deviceInterface->getBaseDevice()->getDriverInterface() == INDI::BaseDevice::GENERAL_INTERFACE) return; // If debug was turned off, but our logging policy requires it then turn it back on. // We turn on debug logging if AT LEAST one driver interface is selected by the logging settings if (svp->s == IPS_OK && svp->sp[0].s == ISS_OFF && (opsLogs->getINDIDebugInterface() & deviceInterface->getBaseDevice()->getDriverInterface())) { svp->sp[0].s = ISS_ON; svp->sp[1].s = ISS_OFF; deviceInterface->getDriverInfo()->getClientManager()->sendNewSwitch(svp); appendLogText(i18n("Re-enabling debug logging for %1...", deviceInterface->getDeviceName())); } // To turn off debug logging, NONE of the driver interfaces should be enabled in logging settings. // For example, if we have CCD+FilterWheel device and CCD + Filter Wheel logging was turned on in // the log settings, then if the user turns off only CCD logging, the debug logging is NOT // turned off until he turns off Filter Wheel logging as well. else if (svp->s == IPS_OK && svp->sp[0].s == ISS_ON && !(opsLogs->getINDIDebugInterface() & deviceInterface->getBaseDevice()->getDriverInterface())) { svp->sp[0].s = ISS_OFF; svp->sp[1].s = ISS_ON; deviceInterface->getDriverInfo()->getClientManager()->sendNewSwitch(svp); appendLogText(i18n("Re-disabling debug logging for %1...", deviceInterface->getDeviceName())); } } } void Manager::announceEvent(const QString &message, KSNotification::EventType event) { ekosLiveClient.get()->message()->sendEvent(message, event); } void Manager::connectModules() { // Guide <---> Capture connections if (captureProcess.get() && guideProcess.get()) { captureProcess.get()->disconnect(guideProcess.get()); guideProcess.get()->disconnect(captureProcess.get()); // Guide Limits connect(guideProcess.get(), &Ekos::Guide::newStatus, captureProcess.get(), &Ekos::Capture::setGuideStatus, Qt::UniqueConnection); connect(guideProcess.get(), &Ekos::Guide::newAxisDelta, captureProcess.get(), &Ekos::Capture::setGuideDeviation); // Dithering connect(captureProcess.get(), &Ekos::Capture::newStatus, guideProcess.get(), &Ekos::Guide::setCaptureStatus, Qt::UniqueConnection); // Guide Head connect(captureProcess.get(), &Ekos::Capture::suspendGuiding, guideProcess.get(), &Ekos::Guide::suspend, Qt::UniqueConnection); connect(captureProcess.get(), &Ekos::Capture::resumeGuiding, guideProcess.get(), &Ekos::Guide::resume, Qt::UniqueConnection); connect(guideProcess.get(), &Ekos::Guide::guideChipUpdated, captureProcess.get(), &Ekos::Capture::setGuideChip, Qt::UniqueConnection); // Meridian Flip connect(captureProcess.get(), &Ekos::Capture::meridianFlipStarted, guideProcess.get(), &Ekos::Guide::abort, Qt::UniqueConnection); connect(captureProcess.get(), &Ekos::Capture::meridianFlipCompleted, guideProcess.get(), &Ekos::Guide::guideAfterMeridianFlip, Qt::UniqueConnection); } // Guide <---> Mount connections if (guideProcess.get() && mountProcess.get()) { // Parking connect(mountProcess.get(), &Ekos::Mount::newStatus, guideProcess.get(), &Ekos::Guide::setMountStatus, Qt::UniqueConnection); } // Focus <---> Guide connections if (guideProcess.get() && focusProcess.get()) { // Suspend connect(focusProcess.get(), &Ekos::Focus::suspendGuiding, guideProcess.get(), &Ekos::Guide::suspend, Qt::UniqueConnection); connect(focusProcess.get(), &Ekos::Focus::resumeGuiding, guideProcess.get(), &Ekos::Guide::resume, Qt::UniqueConnection); } // Capture <---> Focus connections if (captureProcess.get() && focusProcess.get()) { // Check focus HFR value connect(captureProcess.get(), &Ekos::Capture::checkFocus, focusProcess.get(), &Ekos::Focus::checkFocus, Qt::UniqueConnection); // Reset Focus connect(captureProcess.get(), &Ekos::Capture::resetFocus, focusProcess.get(), &Ekos::Focus::resetFrame, Qt::UniqueConnection); // New Focus Status connect(focusProcess.get(), &Ekos::Focus::newStatus, captureProcess.get(), &Ekos::Capture::setFocusStatus, Qt::UniqueConnection); // New Focus HFR connect(focusProcess.get(), &Ekos::Focus::newHFR, captureProcess.get(), &Ekos::Capture::setHFR, Qt::UniqueConnection); } // Capture <---> Align connections if (captureProcess.get() && alignProcess.get()) { // Alignment flag connect(alignProcess.get(), &Ekos::Align::newStatus, captureProcess.get(), &Ekos::Capture::setAlignStatus, Qt::UniqueConnection); // Solver data connect(alignProcess.get(), &Ekos::Align::newSolverResults, captureProcess.get(), &Ekos::Capture::setAlignResults, Qt::UniqueConnection); // Capture Status connect(captureProcess.get(), &Ekos::Capture::newStatus, alignProcess.get(), &Ekos::Align::setCaptureStatus, Qt::UniqueConnection); } // Capture <---> Mount connections if (captureProcess.get() && mountProcess.get()) { // Meridian Flip states connect(captureProcess.get(), &Ekos::Capture::meridianFlipStarted, mountProcess.get(), &Ekos::Mount::disableAltLimits, Qt::UniqueConnection); connect(captureProcess.get(), &Ekos::Capture::meridianFlipCompleted, mountProcess.get(), &Ekos::Mount::enableAltLimits, Qt::UniqueConnection); connect(captureProcess.get(), &Ekos::Capture::newMeridianFlipStatus, mountProcess.get(), &Ekos::Mount::meridianFlipStatusChanged, Qt::UniqueConnection); connect(mountProcess.get(), &Ekos::Mount::newMeridianFlipStatus, captureProcess.get(), &Ekos::Capture::meridianFlipStatusChanged, Qt::UniqueConnection); // Mount Status connect(mountProcess.get(), &Ekos::Mount::newStatus, captureProcess.get(), &Ekos::Capture::setMountStatus, Qt::UniqueConnection); } // Capture <---> EkosLive connections if (captureProcess.get() && ekosLiveClient.get()) { captureProcess.get()->disconnect(ekosLiveClient.get()->message()); connect(captureProcess.get(), &Ekos::Capture::dslrInfoRequested, ekosLiveClient.get()->message(), &EkosLive::Message::requestDSLRInfo); connect(captureProcess.get(), &Ekos::Capture::sequenceChanged, ekosLiveClient.get()->message(), &EkosLive::Message::sendCaptureSequence); connect(captureProcess.get(), &Ekos::Capture::settingsUpdated, ekosLiveClient.get()->message(), &EkosLive::Message::sendCaptureSettings); } // Focus <---> Align connections if (focusProcess.get() && alignProcess.get()) { connect(focusProcess.get(), &Ekos::Focus::newStatus, alignProcess.get(), &Ekos::Align::setFocusStatus, Qt::UniqueConnection); } // Focus <---> Mount connections if (focusProcess.get() && mountProcess.get()) { connect(mountProcess.get(), &Ekos::Mount::newStatus, focusProcess.get(), &Ekos::Focus::setMountStatus, Qt::UniqueConnection); } // Mount <---> Align connections if (mountProcess.get() && alignProcess.get()) { connect(mountProcess.get(), &Ekos::Mount::newStatus, alignProcess.get(), &Ekos::Align::setMountStatus, Qt::UniqueConnection); } // Mount <---> Guide connections if (mountProcess.get() && guideProcess.get()) { connect(mountProcess.get(), &Ekos::Mount::pierSideChanged, guideProcess.get(), &Ekos::Guide::setPierSide, Qt::UniqueConnection); } // Focus <---> Align connections if (focusProcess.get() && alignProcess.get()) { connect(focusProcess.get(), &Ekos::Focus::newStatus, alignProcess.get(), &Ekos::Align::setFocusStatus, Qt::UniqueConnection); } // Align <--> EkosLive connections if (alignProcess.get() && ekosLiveClient.get()) { alignProcess.get()->disconnect(ekosLiveClient.get()->message()); alignProcess.get()->disconnect(ekosLiveClient.get()->media()); connect(alignProcess.get(), &Ekos::Align::newStatus, ekosLiveClient.get()->message(), &EkosLive::Message::setAlignStatus); connect(alignProcess.get(), &Ekos::Align::newSolution, ekosLiveClient.get()->message(), &EkosLive::Message::setAlignSolution); connect(alignProcess.get(), &Ekos::Align::newPAHStage, ekosLiveClient.get()->message(), &EkosLive::Message::setPAHStage); connect(alignProcess.get(), &Ekos::Align::newPAHMessage, ekosLiveClient.get()->message(), &EkosLive::Message::setPAHMessage); connect(alignProcess.get(), &Ekos::Align::PAHEnabled, ekosLiveClient.get()->message(), &EkosLive::Message::setPAHEnabled); connect(alignProcess.get(), &Ekos::Align::newImage, [&](FITSView * view) { ekosLiveClient.get()->media()->sendPreviewImage(view, QString()); }); connect(alignProcess.get(), &Ekos::Align::newFrame, ekosLiveClient.get()->media(), &EkosLive::Media::sendUpdatedFrame); connect(alignProcess.get(), &Ekos::Align::polarResultUpdated, ekosLiveClient.get()->message(), &EkosLive::Message::setPolarResults); connect(alignProcess.get(), &Ekos::Align::settingsUpdated, ekosLiveClient.get()->message(), &EkosLive::Message::sendAlignSettings); connect(alignProcess.get(), &Ekos::Align::newCorrectionVector, ekosLiveClient.get()->media(), &EkosLive::Media::setCorrectionVector); } } void Manager::setEkosLiveConnected(bool enabled) { ekosLiveClient.get()->setConnected(enabled); } void Manager::setEkosLiveConfig(bool onlineService, bool rememberCredentials, bool autoConnect) { ekosLiveClient.get()->setConfig(onlineService, rememberCredentials, autoConnect); } void Manager::setEkosLiveUser(const QString &username, const QString &password) { ekosLiveClient.get()->setUser(username, password); } bool Manager::ekosLiveStatus() { return ekosLiveClient.get()->isConnected(); } void Manager::syncActiveDevices() { for (auto oneDevice : genericDevices) { // Find out what ACTIVE_DEVICES properties this driver needs // and update it from the the existing drivers. ITextVectorProperty *tvp = oneDevice->getBaseDevice()->getText("ACTIVE_DEVICES"); if (tvp) { bool propertyUpdated = false; for (int i = 0; i < tvp->ntp; i++) { QList devs; if (!strcmp(tvp->tp[i].name, "ACTIVE_TELESCOPE")) { devs = findDevicesByInterface(INDI::BaseDevice::TELESCOPE_INTERFACE); } else if (!strcmp(tvp->tp[i].name, "ACTIVE_DOME")) { devs = findDevicesByInterface(INDI::BaseDevice::DOME_INTERFACE); } else if (!strcmp(tvp->tp[i].name, "ACTIVE_GPS")) { devs = findDevicesByInterface(INDI::BaseDevice::GPS_INTERFACE); } else if (!strcmp(tvp->tp[i].name, "ACTIVE_FILTER")) { if (tvp->tp[i].aux0 != nullptr) { bool *override = static_cast(tvp->tp[i].aux0); if (override && *override) continue; } devs = findDevicesByInterface(INDI::BaseDevice::FILTER_INTERFACE); } else if (!strcmp(tvp->tp[i].name, "ACTIVE_WEATHER")) { devs = findDevicesByInterface(INDI::BaseDevice::WEATHER_INTERFACE); } if (!devs.empty()) { if (strcmp(tvp->tp[i].text, devs.first()->getDeviceName())) { propertyUpdated = true; IUSaveText(&tvp->tp[i], devs.first()->getDeviceName()); oneDevice->getDriverInfo()->getClientManager()->sendNewText(tvp); } } } // Save configuration if (propertyUpdated) oneDevice->setConfig(SAVE_CONFIG); } } } bool Manager::checkUniqueBinaryDriver(DriverInfo *primaryDriver, DriverInfo *secondaryDriver) { if (!primaryDriver || !secondaryDriver) return false; return (primaryDriver->getExecutable() == secondaryDriver->getExecutable() && primaryDriver->getAuxInfo().value("mdpd", false).toBool() == true); } } diff --git a/kstars/ekos/mount/mount.cpp b/kstars/ekos/mount/mount.cpp index 72775446d..a15b5bd11 100644 --- a/kstars/ekos/mount/mount.cpp +++ b/kstars/ekos/mount/mount.cpp @@ -1,1644 +1,1645 @@ /* Ekos Mount Module Copyright (C) 2015 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 "mount.h" #include #include #include #include #include #include #include "Options.h" #include "ksmessagebox.h" #include "indi/driverinfo.h" #include "indi/indicommon.h" #include "indi/clientmanager.h" #include "indi/indifilter.h" #include "mountadaptor.h" #include "ekos/manager.h" #include "kstars.h" #include "skymapcomposite.h" #include "kspaths.h" #include "dialogs/finddialog.h" #include "kstarsdata.h" #include "ksutils.h" #include #include extern const char *libindi_strings_context; #define UPDATE_DELAY 1000 #define ABORT_DISPATCH_LIMIT 3 namespace Ekos { Mount::Mount() { setupUi(this); new MountAdaptor(this); QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Mount", this); // Set up DBus interfaces QPointer ekosInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", QDBusConnection::sessionBus(), this); qDBusRegisterMetaType(); // Connecting DBus signals QDBusConnection::sessionBus().connect("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", "newModule", this, SLOT(registerNewModule(QString))); //connect(ekosInterface, SIGNAL(newModule(QString)), this, SLOT(registerNewModule(QString))); currentTelescope = nullptr; abortDispatch = -1; minAltLimit->setValue(Options::minimumAltLimit()); maxAltLimit->setValue(Options::maximumAltLimit()); connect(minAltLimit, SIGNAL(editingFinished()), this, SLOT(saveLimits())); connect(maxAltLimit, SIGNAL(editingFinished()), this, SLOT(saveLimits())); connect(mountToolBoxB, SIGNAL(clicked()), this, SLOT(toggleMountToolBox())); connect(saveB, SIGNAL(clicked()), this, SLOT(save())); connect(clearAlignmentModelB, &QPushButton::clicked, this, [this]() { resetModel(); }); connect(clearParkingB, &QPushButton::clicked, this, [this]() { if (currentTelescope) currentTelescope->clearParking(); }); connect(purgeConfigB, &QPushButton::clicked, this, [this]() { if (currentTelescope) { if (KMessageBox::questionYesNo(KStars::Instance(), i18n("Are you sure you want to clear all mount configurations?"), i18n("Mount Configuration"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "purge_mount_settings_dialog") == KMessageBox::Yes) { resetModel(); currentTelescope->clearParking(); currentTelescope->setConfig(PURGE_CONFIG); } } }); connect(enableLimitsCheck, SIGNAL(toggled(bool)), this, SLOT(enableAltitudeLimits(bool))); enableLimitsCheck->setChecked(Options::enableAltitudeLimits()); altLimitEnabled = enableLimitsCheck->isChecked(); // meridian flip meridianFlipCheckBox->setChecked(Options::executeMeridianFlip()); // Meridian Flip Unit meridianFlipDegreesR->setChecked(Options::meridianFlipUnitDegrees()); meridianFlipHoursR->setChecked(!Options::meridianFlipUnitDegrees()); // This is always in hours double offset = Options::meridianFlipOffset(); // Hours --> Degrees if (meridianFlipDegreesR->isChecked()) offset *= 15.0; meridianFlipTimeBox->setValue(offset); connect(meridianFlipCheckBox, &QCheckBox::toggled, this, &Ekos::Mount::meridianFlipSetupChanged); connect(meridianFlipTimeBox, static_cast(&QDoubleSpinBox::valueChanged), this, &Ekos::Mount::meridianFlipSetupChanged); connect(meridianFlipDegreesR, &QRadioButton::toggled, this, [this]() { Options::setMeridianFlipUnitDegrees(meridianFlipDegreesR->isChecked()); // Hours ---> Degrees if (meridianFlipDegreesR->isChecked()) meridianFlipTimeBox->setValue(meridianFlipTimeBox->value() * 15.0); // Degrees --> Hours else meridianFlipTimeBox->setValue(rangeHA(meridianFlipTimeBox->value() / 15.0)); }); updateTimer.setInterval(UPDATE_DELAY); connect(&updateTimer, SIGNAL(timeout()), this, SLOT(updateTelescopeCoords())); everyDayCheck->setChecked(Options::parkEveryDay()); connect(everyDayCheck, &QCheckBox::toggled, this, [](bool toggled) { Options::setParkEveryDay(toggled); }); startupTimeEdit->setTime(QTime::fromString(Options::parkTime())); connect(startupTimeEdit, &QTimeEdit::editingFinished, this, [this]() { Options::setParkTime(startupTimeEdit->time().toString()); }); connect(&autoParkTimer, &QTimer::timeout, this, &Mount::startAutoPark); connect(startTimerB, &QPushButton::clicked, this, &Mount::startParkTimer); connect(stopTimerB, &QPushButton::clicked, this, &Mount::stopParkTimer); stopTimerB->setEnabled(false); if (everyDayCheck->isChecked()) startTimerB->animateClick(); // QML Stuff m_BaseView = new QQuickView(); #if 0 QString MountBox_Location; #if defined(Q_OS_OSX) MountBox_Location = QCoreApplication::applicationDirPath() + "/../Resources/data/ekos/mount/qml/mountbox.qml"; if (!QFileInfo(MountBox_Location).exists()) MountBox_Location = KSPaths::locate(QStandardPaths::AppDataLocation, "ekos/mount/qml/mountbox.qml"); #elif defined(Q_OS_WIN) MountBox_Location = KSPaths::locate(QStandardPaths::GenericDataLocation, "ekos/mount/qml/mountbox.qml"); #else MountBox_Location = KSPaths::locate(QStandardPaths::AppDataLocation, "ekos/mount/qml/mountbox.qml"); #endif m_BaseView->setSource(QUrl::fromLocalFile(MountBox_Location)); #endif m_BaseView->setSource(QUrl("qrc:/qml/mount/mountbox.qml")); m_BaseView->setTitle(i18n("Mount Control")); #ifdef Q_OS_OSX m_BaseView->setFlags(Qt::Tool | Qt::WindowStaysOnTopHint); #else m_BaseView->setFlags(Qt::WindowStaysOnTopHint | Qt::WindowCloseButtonHint); #endif // Theming? m_BaseView->setColor(Qt::black); m_BaseObj = m_BaseView->rootObject(); m_Ctxt = m_BaseView->rootContext(); ///Use instead of KDeclarative m_Ctxt->setContextObject(new KLocalizedContext(m_BaseView)); m_Ctxt->setContextProperty("mount", this); m_BaseView->setMinimumSize(QSize(210, 540)); m_BaseView->setResizeMode(QQuickView::SizeRootObjectToView); m_SpeedSlider = m_BaseObj->findChild("speedSliderObject"); m_SpeedLabel = m_BaseObj->findChild("speedLabelObject"); m_raValue = m_BaseObj->findChild("raValueObject"); m_deValue = m_BaseObj->findChild("deValueObject"); m_azValue = m_BaseObj->findChild("azValueObject"); m_altValue = m_BaseObj->findChild("altValueObject"); m_haValue = m_BaseObj->findChild("haValueObject"); m_zaValue = m_BaseObj->findChild("zaValueObject"); m_targetText = m_BaseObj->findChild("targetTextObject"); m_targetRAText = m_BaseObj->findChild("targetRATextObject"); m_targetDEText = m_BaseObj->findChild("targetDETextObject"); m_J2000Check = m_BaseObj->findChild("j2000CheckObject"); m_JNowCheck = m_BaseObj->findChild("jnowCheckObject"); m_Park = m_BaseObj->findChild("parkButtonObject"); m_Unpark = m_BaseObj->findChild("unparkButtonObject"); m_statusText = m_BaseObj->findChild("statusTextObject"); m_equatorialCheck = m_BaseObj->findChild("equatorialCheckObject"); m_horizontalCheck = m_BaseObj->findChild("horizontalCheckObject"); m_leftRightCheck = m_BaseObj->findChild("leftRightCheckObject"); m_upDownCheck = m_BaseObj->findChild("upDownCheckObject"); m_leftRightCheck->setProperty("checked", Options::leftRightReversed()); m_upDownCheck->setProperty("checked", Options::upDownReversed()); //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); } Mount::~Mount() { delete(m_Ctxt); delete(m_BaseObj); + delete(currentTargetPosition); } void Mount::setTelescope(ISD::GDInterface *newTelescope) { if (newTelescope == currentTelescope) { updateTimer.start(); if (enableLimitsCheck->isChecked()) currentTelescope->setAltLimits(minAltLimit->value(), maxAltLimit->value()); syncTelescopeInfo(); return; } if (currentGPS != nullptr) syncGPS(); currentTelescope = static_cast(newTelescope); currentTelescope->disconnect(this); connect(currentTelescope, &ISD::GDInterface::numberUpdated, this, &Mount::updateNumber); connect(currentTelescope, &ISD::GDInterface::switchUpdated, this, &Mount::updateSwitch); connect(currentTelescope, &ISD::GDInterface::textUpdated, this, &Mount::updateText); connect(currentTelescope, &ISD::Telescope::newTarget, this, &Mount::newTarget); connect(currentTelescope, &ISD::Telescope::slewRateChanged, this, &Mount::slewRateChanged); connect(currentTelescope, &ISD::Telescope::pierSideChanged, this, &Mount::pierSideChanged); connect(currentTelescope, &ISD::Telescope::Disconnected, [this]() { updateTimer.stop(); m_BaseView->hide(); }); connect(currentTelescope, &ISD::Telescope::newParkStatus, [&](ISD::ParkStatus status) { m_ParkStatus = status; emit newParkStatus(status); // If mount is unparked AND every day auto-paro check is ON // AND auto park timer is not yet started, we try to initiate it. if (status == ISD::PARK_UNPARKED && everyDayCheck->isChecked() && autoParkTimer.isActive() == false) startTimerB->animateClick(); }); connect(currentTelescope, &ISD::Telescope::ready, this, &Mount::ready); //Disable this for now since ALL INDI drivers now log their messages to verbose output //connect(currentTelescope, SIGNAL(messageUpdated(int)), this, SLOT(updateLog(int)), Qt::UniqueConnection); if (enableLimitsCheck->isChecked()) currentTelescope->setAltLimits(minAltLimit->value(), maxAltLimit->value()); updateTimer.start(); syncTelescopeInfo(); // Send initial status m_Status = currentTelescope->status(); emit newStatus(m_Status); m_ParkStatus = currentTelescope->parkStatus(); emit newParkStatus(m_ParkStatus); } void Mount::removeDevice(ISD::GDInterface *device) { if (currentTelescope && !strcmp(currentTelescope->getDeviceName(), device->getDeviceName())) { currentTelescope->disconnect(this); updateTimer.stop(); m_BaseView->hide(); qCDebug(KSTARS_EKOS_MOUNT) << "Removing mount driver" << device->getDeviceName(); currentTelescope = nullptr; } else if (currentGPS && !strcmp(currentGPS->getDeviceName(), device->getDeviceName())) { currentGPS->disconnect(this); currentGPS = nullptr; } } void Mount::syncTelescopeInfo() { if (currentTelescope == nullptr || currentTelescope->isConnected() == false) return; INumberVectorProperty *nvp = currentTelescope->getBaseDevice()->getNumber("TELESCOPE_INFO"); if (nvp) { primaryScopeGroup->setTitle(currentTelescope->getDeviceName()); guideScopeGroup->setTitle(i18n("%1 guide scope", currentTelescope->getDeviceName())); INumber *np = IUFindNumber(nvp, "TELESCOPE_APERTURE"); if (np && np->value > 0) primaryScopeApertureIN->setValue(np->value); np = IUFindNumber(nvp, "TELESCOPE_FOCAL_LENGTH"); if (np && np->value > 0) primaryScopeFocalIN->setValue(np->value); np = IUFindNumber(nvp, "GUIDER_APERTURE"); if (np && np->value > 0) guideScopeApertureIN->setValue(np->value); np = IUFindNumber(nvp, "GUIDER_FOCAL_LENGTH"); if (np && np->value > 0) guideScopeFocalIN->setValue(np->value); } ISwitchVectorProperty *svp = currentTelescope->getBaseDevice()->getSwitch("TELESCOPE_SLEW_RATE"); if (svp) { int index = IUFindOnSwitchIndex(svp); // QtQuick m_SpeedSlider->setEnabled(true); m_SpeedSlider->setProperty("maximumValue", svp->nsp - 1); m_SpeedSlider->setProperty("value", index); m_SpeedLabel->setProperty("text", i18nc(libindi_strings_context, svp->sp[index].label)); m_SpeedLabel->setEnabled(true); } else { // QtQuick m_SpeedSlider->setEnabled(false); m_SpeedLabel->setEnabled(false); } if (currentTelescope->canPark()) { parkB->setEnabled(!currentTelescope->isParked()); unparkB->setEnabled(currentTelescope->isParked()); connect(parkB, SIGNAL(clicked()), currentTelescope, SLOT(Park()), Qt::UniqueConnection); connect(unparkB, SIGNAL(clicked()), currentTelescope, SLOT(UnPark()), Qt::UniqueConnection); // QtQuick m_Park->setEnabled(!currentTelescope->isParked()); m_Unpark->setEnabled(currentTelescope->isParked()); } else { parkB->setEnabled(false); unparkB->setEnabled(false); disconnect(parkB, SIGNAL(clicked()), currentTelescope, SLOT(Park())); disconnect(unparkB, SIGNAL(clicked()), currentTelescope, SLOT(UnPark())); // QtQuick m_Park->setEnabled(false); m_Unpark->setEnabled(false); } // Configs svp = currentTelescope->getBaseDevice()->getSwitch("APPLY_SCOPE_CONFIG"); if (svp) { scopeConfigCombo->disconnect(); scopeConfigCombo->clear(); for (int i = 0; i < svp->nsp; i++) scopeConfigCombo->addItem(svp->sp[i].label); scopeConfigCombo->setCurrentIndex(IUFindOnSwitchIndex(svp)); connect(scopeConfigCombo, SIGNAL(activated(int)), this, SLOT(setScopeConfig(int))); } // Tracking State svp = currentTelescope->getBaseDevice()->getSwitch("TELESCOPE_TRACK_STATE"); if (svp) { trackingGroup->setEnabled(true); trackOnB->disconnect(); trackOffB->disconnect(); connect(trackOnB, &QPushButton::clicked, [&]() { currentTelescope->setTrackEnabled(true); }); connect(trackOffB, &QPushButton::clicked, [&]() { if (KMessageBox::questionYesNo(KStars::Instance(), i18n("Are you sure you want to turn off mount tracking?"), i18n("Mount Tracking"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "turn_off_mount_tracking_dialog") == KMessageBox::Yes) currentTelescope->setTrackEnabled(false); }); } else { trackOnB->setChecked(false); trackOffB->setChecked(false); trackingGroup->setEnabled(false); } ITextVectorProperty *tvp = currentTelescope->getBaseDevice()->getText("SCOPE_CONFIG_NAME"); if (tvp) scopeConfigNameEdit->setText(tvp->tp[0].text); } void Mount::registerNewModule(const QString &name) { if (name == "Capture") { captureInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Capture", "org.kde.kstars.Ekos.Capture", QDBusConnection::sessionBus(), this); } } void Mount::updateText(ITextVectorProperty *tvp) { if (!strcmp(tvp->name, "SCOPE_CONFIG_NAME")) { scopeConfigNameEdit->setText(tvp->tp[0].text); } } bool Mount::setScopeConfig(int index) { ISwitchVectorProperty *svp = currentTelescope->getBaseDevice()->getSwitch("APPLY_SCOPE_CONFIG"); if (svp == nullptr) return false; IUResetSwitch(svp); svp->sp[index].s = ISS_ON; // Clear scope config name so that it gets filled by INDI scopeConfigNameEdit->clear(); currentTelescope->getDriverInfo()->getClientManager()->sendNewSwitch(svp); return true; } void Mount::updateTelescopeCoords() { // No need to update coords if we are still parked. if (m_Status == ISD::Telescope::MOUNT_PARKED && m_Status == currentTelescope->status()) return; double ra = 0, dec = 0; if (currentTelescope && currentTelescope->getEqCoords(&ra, &dec)) { if (currentTelescope->isJ2000()) { telescopeCoord.setRA0(ra); telescopeCoord.setDec0(dec); // Get JNow as well telescopeCoord.apparentCoord(static_cast(J2000), KStars::Instance()->data()->ut().djd()); } else { telescopeCoord.setRA(ra); telescopeCoord.setDec(dec); } // Ekos Mount Tab coords are always in JNow raOUT->setText(telescopeCoord.ra().toHMSString()); decOUT->setText(telescopeCoord.dec().toDMSString()); // Mount Control Panel coords depend on the switch if (m_JNowCheck->property("checked").toBool()) { m_raValue->setProperty("text", telescopeCoord.ra().toHMSString()); m_deValue->setProperty("text", telescopeCoord.dec().toDMSString()); } else { // If epoch is already J2000, then we don't need to convert to JNow if (currentTelescope->isJ2000() == false) { SkyPoint J2000Coord(telescopeCoord.ra(), telescopeCoord.dec()); J2000Coord.apparentCoord(KStars::Instance()->data()->ut().djd(), static_cast(J2000)); //J2000Coord.precessFromAnyEpoch(KStars::Instance()->data()->ut().djd(), static_cast(J2000)); telescopeCoord.setRA0(J2000Coord.ra()); telescopeCoord.setDec0(J2000Coord.dec()); } m_raValue->setProperty("text", telescopeCoord.ra0().toHMSString()); m_deValue->setProperty("text", telescopeCoord.dec0().toDMSString()); } // Get horizontal coords telescopeCoord.EquatorialToHorizontal(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat()); azOUT->setText(telescopeCoord.az().toDMSString()); m_azValue->setProperty("text", telescopeCoord.az().toDMSString()); altOUT->setText(telescopeCoord.alt().toDMSString()); m_altValue->setProperty("text", telescopeCoord.alt().toDMSString()); dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst()); dms ha(lst - telescopeCoord.ra()); QChar sgn('+'); if (ha.Hours() > 12.0) { ha.setH(24.0 - ha.Hours()); sgn = '-'; } haOUT->setText(QString("%1%2").arg(sgn).arg(ha.toHMSString())); m_haValue->setProperty("text", haOUT->text()); lstOUT->setText(lst.toHMSString()); double currentAlt = telescopeCoord.altRefracted().Degrees(); m_zaValue->setProperty("text", dms(90 - currentAlt).toDMSString()); if (minAltLimit->isEnabled() && (currentAlt < minAltLimit->value() || currentAlt > maxAltLimit->value())) { if (currentAlt < minAltLimit->value()) { // Only stop if current altitude is less than last altitude indicate worse situation if (currentAlt < lastAlt && (abortDispatch == -1 || (currentTelescope->isInMotion() /* && ++abortDispatch > ABORT_DISPATCH_LIMIT*/))) { appendLogText(i18n("Telescope altitude is below minimum altitude limit of %1. Aborting motion...", QString::number(minAltLimit->value(), 'g', 3))); currentTelescope->Abort(); currentTelescope->setTrackEnabled(false); //KNotification::event( QLatin1String( "OperationFailed" )); KNotification::beep(); abortDispatch++; } } else { // Only stop if current altitude is higher than last altitude indicate worse situation if (currentAlt > lastAlt && (abortDispatch == -1 || (currentTelescope->isInMotion() /* && ++abortDispatch > ABORT_DISPATCH_LIMIT*/))) { appendLogText(i18n("Telescope altitude is above maximum altitude limit of %1. Aborting motion...", QString::number(maxAltLimit->value(), 'g', 3))); currentTelescope->Abort(); currentTelescope->setTrackEnabled(false); //KNotification::event( QLatin1String( "OperationFailed" )); KNotification::beep(); abortDispatch++; } } } else abortDispatch = -1; lastAlt = currentAlt; emit newCoords(raOUT->text(), decOUT->text(), azOUT->text(), altOUT->text()); ISD::Telescope::Status currentStatus = currentTelescope->status(); bool isTracking = (currentStatus == ISD::Telescope::MOUNT_TRACKING); if (m_Status != currentStatus) { qCDebug(KSTARS_EKOS_MOUNT) << "Mount status changed from " << currentTelescope->getStatusString(m_Status) << " to " << currentTelescope->getStatusString(currentStatus); // If we just finished a slew, let's update initialHA and the current target's position if (currentStatus == ISD::Telescope::MOUNT_TRACKING && m_Status == ISD::Telescope::MOUNT_SLEWING) { setInitialHA((sgn == '-' ? -1 : 1) * ha.Hours()); delete currentTargetPosition; currentTargetPosition = new SkyPoint(telescopeCoord.ra(), telescopeCoord.dec()); } m_Status = currentStatus; parkB->setEnabled(!currentTelescope->isParked()); unparkB->setEnabled(currentTelescope->isParked()); m_Park->setEnabled(!currentTelescope->isParked()); m_Unpark->setEnabled(currentTelescope->isParked()); m_statusText->setProperty("text", currentTelescope->getStatusString(currentStatus)); QAction *a = KStars::Instance()->actionCollection()->action("telescope_track"); if (a != nullptr) a->setChecked(currentStatus == ISD::Telescope::MOUNT_TRACKING); emit newStatus(m_Status); } if (trackingGroup->isEnabled()) { trackOnB->setChecked(isTracking); trackOffB->setChecked(!isTracking); } if (currentTelescope->isConnected() == false) updateTimer.stop(); else if (updateTimer.isActive() == false) updateTimer.start(); // Auto Park Timer if (autoParkTimer.isActive()) { QTime remainingTime(0, 0, 0); remainingTime = remainingTime.addMSecs(autoParkTimer.remainingTime()); countdownLabel->setText(remainingTime.toString("hh:mm:ss")); } if (isTracking && checkMeridianFlip(lst)) executeMeridianFlip(); } else updateTimer.stop(); } void Mount::updateNumber(INumberVectorProperty *nvp) { if (!strcmp(nvp->name, "TELESCOPE_INFO")) { if (nvp->s == IPS_ALERT) { QString newMessage; if (primaryScopeApertureIN->value() == 1 || primaryScopeFocalIN->value() == 1) newMessage = i18n("Error syncing telescope info. Please fill telescope aperture and focal length."); else newMessage = i18n("Error syncing telescope info. Check INDI control panel for more details."); if (newMessage != lastNotificationMessage) { appendLogText(newMessage); lastNotificationMessage = newMessage; } } else { syncTelescopeInfo(); QString newMessage = i18n("Telescope info updated successfully."); if (newMessage != lastNotificationMessage) { appendLogText(newMessage); lastNotificationMessage = newMessage; } } } if (currentGPS != nullptr && !strcmp(nvp->device, currentGPS->getDeviceName()) && !strcmp(nvp->name, "GEOGRAPHIC_COORD") && nvp->s == IPS_OK) syncGPS(); } bool Mount::setSlewRate(int index) { if (currentTelescope) return currentTelescope->setSlewRate(index); return false; } void Mount::setUpDownReversed(bool enabled) { Options::setUpDownReversed(enabled); } void Mount::setLeftRightReversed(bool enabled) { Options::setLeftRightReversed(enabled); } void Mount::setMeridianFlipValues(bool activate, double hours) { meridianFlipCheckBox->setChecked(activate); // Hours --> Degrees if (meridianFlipDegreesR->isChecked()) meridianFlipTimeBox->setValue(hours * 15.0); else meridianFlipTimeBox->setValue(hours); Options::setExecuteMeridianFlip(meridianFlipCheckBox->isChecked()); // It is always saved in hours Options::setMeridianFlipOffset(hours); } void Mount::meridianFlipSetupChanged() { if (meridianFlipCheckBox->isChecked() == false) // reset meridian flip setMeridianFlipStatus(FLIP_NONE); Options::setExecuteMeridianFlip(meridianFlipCheckBox->isChecked()); double offset = meridianFlipTimeBox->value(); // Degrees --> Hours if (meridianFlipDegreesR->isChecked()) offset /= 15.0; // It is always saved in hours Options::setMeridianFlipOffset(offset); } void Mount::setMeridianFlipStatus(MeridianFlipStatus status) { if (m_MFStatus != status) { m_MFStatus = status; qCDebug (KSTARS_EKOS_MOUNT) << "Setting meridian flip status to " << status; meridianFlipStatusChanged(status); emit newMeridianFlipStatus(status); } } void Mount::updateSwitch(ISwitchVectorProperty *svp) { if (!strcmp(svp->name, "TELESCOPE_SLEW_RATE")) { int index = IUFindOnSwitchIndex(svp); m_SpeedSlider->setProperty("value", index); m_SpeedLabel->setProperty("text", i18nc(libindi_strings_context, svp->sp[index].label)); } /*else if (!strcmp(svp->name, "TELESCOPE_PARK")) { ISwitch *sp = IUFindSwitch(svp, "PARK"); if (sp) { parkB->setEnabled((sp->s == ISS_OFF)); unparkB->setEnabled((sp->s == ISS_ON)); } }*/ } void Mount::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_MOUNT) << text; emit newLog(text); } void Mount::updateLog(int messageID) { INDI::BaseDevice *dv = currentTelescope->getBaseDevice(); QString message = QString::fromStdString(dv->messageQueue(messageID)); m_LogText.insert(0, i18nc("Message shown in Ekos Mount module", "%1", message)); emit newLog(message); } void Mount::clearLog() { m_LogText.clear(); emit newLog(QString()); } void Mount::motionCommand(int command, int NS, int WE) { if (NS != -1) { if (Options::upDownReversed()) NS = !NS; currentTelescope->MoveNS(static_cast(NS), static_cast(command)); } if (WE != -1) { if (Options::leftRightReversed()) WE = !WE; currentTelescope->MoveWE(static_cast(WE), static_cast(command)); } } void Mount::save() { if (currentTelescope == nullptr) return; if (scopeConfigNameEdit->text().isEmpty() == false) { ITextVectorProperty *tvp = currentTelescope->getBaseDevice()->getText("SCOPE_CONFIG_NAME"); if (tvp) { IUSaveText(&(tvp->tp[0]), scopeConfigNameEdit->text().toLatin1().constData()); currentTelescope->getDriverInfo()->getClientManager()->sendNewText(tvp); } } INumberVectorProperty *nvp = currentTelescope->getBaseDevice()->getNumber("TELESCOPE_INFO"); if (nvp) { primaryScopeGroup->setTitle(currentTelescope->getDeviceName()); guideScopeGroup->setTitle(i18n("%1 guide scope", currentTelescope->getDeviceName())); INumber *np = IUFindNumber(nvp, "TELESCOPE_APERTURE"); if (np) np->value = primaryScopeApertureIN->value(); np = IUFindNumber(nvp, "TELESCOPE_FOCAL_LENGTH"); if (np) np->value = primaryScopeFocalIN->value(); np = IUFindNumber(nvp, "GUIDER_APERTURE"); if (np) np->value = guideScopeApertureIN->value() == 1 ? primaryScopeApertureIN->value() : guideScopeApertureIN->value(); np = IUFindNumber(nvp, "GUIDER_FOCAL_LENGTH"); if (np) np->value = guideScopeFocalIN->value() == 1 ? primaryScopeFocalIN->value() : guideScopeFocalIN->value(); ClientManager *clientManager = currentTelescope->getDriverInfo()->getClientManager(); clientManager->sendNewNumber(nvp); currentTelescope->setConfig(SAVE_CONFIG); //appendLogText(i18n("Saving telescope information...")); } else appendLogText(i18n("Failed to save telescope information.")); } void Mount::saveLimits() { Options::setMinimumAltLimit(minAltLimit->value()); Options::setMaximumAltLimit(maxAltLimit->value()); currentTelescope->setAltLimits(minAltLimit->value(), maxAltLimit->value()); } void Mount::enableAltitudeLimits(bool enable) { Options::setEnableAltitudeLimits(enable); if (enable) { minAltLabel->setEnabled(true); maxAltLabel->setEnabled(true); minAltLimit->setEnabled(true); maxAltLimit->setEnabled(true); if (currentTelescope) currentTelescope->setAltLimits(minAltLimit->value(), maxAltLimit->value()); } else { minAltLabel->setEnabled(false); maxAltLabel->setEnabled(false); minAltLimit->setEnabled(false); maxAltLimit->setEnabled(false); if (currentTelescope) currentTelescope->setAltLimits(-1, -1); } } void Mount::enableAltLimits() { //Only enable if it was already enabled before and the minAltLimit is currently disabled. if (altLimitEnabled && minAltLimit->isEnabled() == false) enableAltitudeLimits(true); } void Mount::disableAltLimits() { altLimitEnabled = enableLimitsCheck->isChecked(); enableAltitudeLimits(false); } QList Mount::altitudeLimits() { QList limits; limits.append(minAltLimit->value()); limits.append(maxAltLimit->value()); return limits; } void Mount::setAltitudeLimits(QList limits) { minAltLimit->setValue(limits[0]); maxAltLimit->setValue(limits[1]); } void Mount::setAltitudeLimitsEnabled(bool enable) { enableLimitsCheck->setChecked(enable); } bool Mount::altitudeLimitsEnabled() { return enableLimitsCheck->isChecked(); } void Mount::setJ2000Enabled(bool enabled) { m_J2000Check->setProperty("checked", enabled); } bool Mount::gotoTarget(const QString &target) { SkyObject *object = KStarsData::Instance()->skyComposite()->findByName(target); if (object != nullptr) return slew(object->ra().Hours(), object->dec().Degrees()); return false; } bool Mount::syncTarget(const QString &target) { SkyObject *object = KStarsData::Instance()->skyComposite()->findByName(target); if (object != nullptr) return sync(object->ra().Hours(), object->dec().Degrees()); return false; } bool Mount::slew(const QString &RA, const QString &DEC) { dms ra, de; if (m_equatorialCheck->property("checked").toBool()) { ra = dms::fromString(RA, false); de = dms::fromString(DEC, true); } else { dms az = dms::fromString(RA, true); dms at = dms::fromString(DEC, true); SkyPoint horizontalTarget; horizontalTarget.setAz(az); horizontalTarget.setAlt(at); horizontalTarget.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat()); ra = horizontalTarget.ra(); de = horizontalTarget.dec(); } // If J2000 was checked and the Mount is _not_ already using native J2000 coordinates // then we need to convert J2000 to JNow. Otherwise, we send J2000 as is. if (m_J2000Check->property("checked").toBool() && currentTelescope && currentTelescope->isJ2000() == false) { // J2000 ---> JNow SkyPoint J2000Coord(ra, de); J2000Coord.setRA0(ra); J2000Coord.setDec0(de); J2000Coord.apparentCoord(static_cast(J2000), KStars::Instance()->data()->ut().djd()); ra = J2000Coord.ra(); de = J2000Coord.dec(); } return slew(ra.Hours(), de.Degrees()); } bool Mount::slew(double RA, double DEC) { if (currentTelescope == nullptr || currentTelescope->isConnected() == false) return false; dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst()); double HA = lst.Hours() - RA; HA = rangeHA(HA); // if (HA > 12.0) // HA -= 24.0; setInitialHA(HA); // reset the meridian flip status if the slew is not the meridian flip itself if (m_MFStatus != FLIP_RUNNING) setMeridianFlipStatus(FLIP_NONE); delete currentTargetPosition; currentTargetPosition = new SkyPoint(RA, DEC); qCDebug(KSTARS_EKOS_MOUNT) << "Slewing to RA=" << currentTargetPosition->ra().toHMSString() << "DEC=" << currentTargetPosition->dec().toDMSString(); return currentTelescope->Slew(currentTargetPosition); } bool Mount::checkMeridianFlip(dms lst) { if (currentTelescope == nullptr || currentTelescope->isConnected() == false) { meridianFlipStatusText->setText("Status: inactive"); setMeridianFlipStatus(FLIP_NONE); return false; } if (currentTelescope->isTracking() == false) { meridianFlipStatusText->setText("Status: inactive (not tracking)"); return false; } double offset = meridianFlipTimeBox->value(); // Degrees --> Hours if (meridianFlipDegreesR->isChecked()) offset = rangeHA(offset / 15.0); double deltaHA = offset - lst.Hours() + telescopeCoord.ra().Hours(); deltaHA = rangeHA(deltaHA); int hh = static_cast (deltaHA); int mm = static_cast ((deltaHA - hh) * 60); int ss = static_cast ((deltaHA - hh - mm / 60.0) * 3600); switch (m_MFStatus) { case FLIP_NONE: if (initialHA() >= 0 || meridianFlipCheckBox->isChecked() == false) { meridianFlipStatusText->setText("Status: inactive"); return false; } if (deltaHA > 0) { QString message = QString("Meridian flip in %1h:%2m:%3s") .arg(hh, 2, 10, QChar('0')) .arg(mm, 2, 10, QChar('0')) .arg(ss, 2, 10, QChar('0')); meridianFlipStatusText->setText(message); return false; } else if (initialHA() < 0) { double offset = meridianFlipTimeBox->value(); // Degrees --> Hours if (meridianFlipDegreesR->isChecked()) offset = rangeHA(offset / 15.0); qCDebug(KSTARS_EKOS_MOUNT) << "Meridian flip planned with LST=" << lst.toHMSString() << " scope RA=" << telescopeCoord.ra().toHMSString() << " and meridian diff=" << offset; setMeridianFlipStatus(FLIP_PLANNED); return false; } break; case FLIP_PLANNED: return false; case FLIP_ACCEPTED: return true; case FLIP_RUNNING: if (currentTelescope->isTracking()) { // meridian flip completed setMeridianFlipStatus(FLIP_COMPLETED); } break; case FLIP_COMPLETED: setMeridianFlipStatus(FLIP_NONE); break; default: // do nothing break; } return false; } bool Mount::executeMeridianFlip() { if (initialHA() > 0) // no meridian flip necessary return false; if (currentTelescope->status() != ISD::Telescope::MOUNT_TRACKING) { // no meridian flip necessary return false; } dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst()); double HA = lst.Hours() - currentTargetPosition->ra().Hours(); if (HA > 12.0) // no meridian flip necessary return false; // execute meridian flip qCInfo(KSTARS_EKOS_MOUNT) << "Meridian flip: slewing to RA=" << currentTargetPosition->ra().toHMSString() << "DEC=" << currentTargetPosition->dec().toDMSString(); setMeridianFlipStatus(FLIP_RUNNING); bool result = slew(currentTargetPosition->ra().Hours(), currentTargetPosition->dec().Degrees()); if (result == false) qCWarning(KSTARS_EKOS_MOUNT) << "Meridian flip FAILED: slewing to RA=" << currentTargetPosition->ra().toHMSString() << "DEC=" << currentTargetPosition->dec().toDMSString(); return result; } void Mount::meridianFlipStatusChanged(Mount::MeridianFlipStatus status) { m_MFStatus = status; switch (status) { case FLIP_NONE: meridianFlipStatusText->setText("Status: inactive"); break; case FLIP_PLANNED: meridianFlipStatusText->setText("Meridian flip planned..."); break; case FLIP_WAITING: meridianFlipStatusText->setText("Meridian flip waiting..."); break; case FLIP_ACCEPTED: if (currentTelescope == nullptr || currentTelescope->isTracking() == false) // if the mount is not tracking, we go back one step setMeridianFlipStatus(FLIP_PLANNED); // otherwise do nothing, execution of meridian flip initianted in updateTelescopeCoords() break; case FLIP_RUNNING: meridianFlipStatusText->setText("Meridian flip running..."); break; case FLIP_COMPLETED: meridianFlipStatusText->setText("Meridian flip completed."); break; default: break; } } SkyPoint Mount::currentTarget() { return *currentTargetPosition; } bool Mount::sync(const QString &RA, const QString &DEC) { dms ra, de; ra = dms::fromString(RA, false); de = dms::fromString(DEC, true); if (m_J2000Check->property("checked").toBool()) { // J2000 ---> JNow SkyPoint J2000Coord(ra, de); J2000Coord.setRA0(ra); J2000Coord.setDec0(de); J2000Coord.updateCoordsNow(KStarsData::Instance()->updateNum()); ra = J2000Coord.ra(); de = J2000Coord.dec(); } return sync(ra.Hours(), de.Degrees()); } bool Mount::sync(double RA, double DEC) { if (currentTelescope == nullptr || currentTelescope->isConnected() == false) return false; return currentTelescope->Sync(RA, DEC); } bool Mount::abort() { return currentTelescope->Abort(); } IPState Mount::slewStatus() { if (currentTelescope == nullptr) return IPS_ALERT; return currentTelescope->getState("EQUATORIAL_EOD_COORD"); } QList Mount::equatorialCoords() { double ra, dec; QList coords; currentTelescope->getEqCoords(&ra, &dec); coords.append(ra); coords.append(dec); return coords; } QList Mount::horizontalCoords() { QList coords; coords.append(telescopeCoord.az().Degrees()); coords.append(telescopeCoord.alt().Degrees()); return coords; } double Mount::hourAngle() { dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst()); dms ha(lst.Degrees() - telescopeCoord.ra().Degrees()); double HA = rangeHA(ha.Hours()); return HA; // if (HA > 12.0) // return (HA - 24.0); // else // return HA; } QList Mount::telescopeInfo() { QList info; info.append(primaryScopeFocalIN->value()); info.append(primaryScopeApertureIN->value()); info.append(guideScopeFocalIN->value()); info.append(guideScopeApertureIN->value()); return info; } void Mount::setTelescopeInfo(const QList &info) { if (info[0] > 0) primaryScopeFocalIN->setValue(info[0]); if (info[1] > 0) primaryScopeApertureIN->setValue(info[1]); if (info[2] > 0) guideScopeFocalIN->setValue(info[2]); if (info[3] > 0) guideScopeApertureIN->setValue(info[3]); if (scopeConfigNameEdit->text().isEmpty() == false) appendLogText(i18n("Warning: Overriding %1 configuration.", scopeConfigNameEdit->text())); save(); } bool Mount::canPark() { if (currentTelescope == nullptr) return false; return currentTelescope->canPark(); } bool Mount::park() { if (currentTelescope == nullptr || currentTelescope->canPark() == false) return false; return currentTelescope->Park(); } bool Mount::unpark() { if (currentTelescope == nullptr || currentTelescope->canPark() == false) return false; return currentTelescope->UnPark(); } #if 0 Mount::ParkingStatus Mount::getParkingStatus() { if (currentTelescope == nullptr) return PARKING_ERROR; // In the case mount can't park, return mount is unparked if (currentTelescope->canPark() == false) return UNPARKING_OK; ISwitchVectorProperty *parkSP = currentTelescope->getBaseDevice()->getSwitch("TELESCOPE_PARK"); if (parkSP == nullptr) return PARKING_ERROR; switch (parkSP->s) { case IPS_IDLE: // If mount is unparked on startup, state is OK and switch is UNPARK if (parkSP->sp[1].s == ISS_ON) return UNPARKING_OK; else return PARKING_IDLE; //break; case IPS_OK: if (parkSP->sp[0].s == ISS_ON) return PARKING_OK; else return UNPARKING_OK; //break; case IPS_BUSY: if (parkSP->sp[0].s == ISS_ON) return PARKING_BUSY; else return UNPARKING_BUSY; case IPS_ALERT: // If mount replied with an error to the last un/park request, // assume state did not change in order to return a clear state if (parkSP->sp[0].s == ISS_ON) return PARKING_OK; else return UNPARKING_OK; } return PARKING_ERROR; } #endif void Mount::toggleMountToolBox() { if (m_BaseView->isVisible()) { m_BaseView->hide(); QAction *a = KStars::Instance()->actionCollection()->action("show_mount_box"); if (a) a->setChecked(false); } else { m_BaseView->show(); QAction *a = KStars::Instance()->actionCollection()->action("show_mount_box"); if (a) a->setChecked(true); } } void Mount::findTarget() { KStarsData *data = KStarsData::Instance(); if (FindDialog::Instance()->exec() == QDialog::Accepted) { SkyObject *object = FindDialog::Instance()->targetObject(); if (object != nullptr) { SkyObject *o = object->clone(); o->updateCoords(data->updateNum(), true, data->geo()->lat(), data->lst(), false); m_targetText->setProperty("text", o->name()); if (m_JNowCheck->property("checked").toBool()) { m_targetRAText->setProperty("text", o->ra().toHMSString()); m_targetDEText->setProperty("text", o->dec().toDMSString()); } else { m_targetRAText->setProperty("text", o->ra0().toHMSString()); m_targetDEText->setProperty("text", o->dec0().toDMSString()); } } } } void Mount::centerMount() { if (currentTelescope) currentTelescope->runCommand(INDI_FIND_TELESCOPE); } bool Mount::resetModel() { if (currentTelescope == nullptr) return false; if (currentTelescope->hasAlignmentModel() == false) return false; if (currentTelescope->clearAlignmentModel()) { appendLogText(i18n("Alignment Model cleared.")); return true; } appendLogText(i18n("Failed to clear Alignment Model.")); return false; } void Mount::setGPS(ISD::GDInterface *newGPS) { if (newGPS == currentGPS) return; auto executeSetGPS = [this, newGPS]() { currentGPS = newGPS; connect(newGPS, SIGNAL(numberUpdated(INumberVectorProperty*)), this, SLOT(updateNumber(INumberVectorProperty*)), Qt::UniqueConnection); appendLogText(i18n("GPS driver detected. KStars and mount time and location settings are now synced to the GPS driver.")); syncGPS(); }; if (Options::useGPSSource() == false) { connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, executeSetGPS]() { //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr); KSMessageBox::Instance()->disconnect(this); Options::setUseKStarsSource(false); Options::setUseMountSource(false); Options::setUseGPSSource(true); executeSetGPS(); }); KSMessageBox::Instance()->questionYesNo(i18n("GPS is detected. Do you want to switch time and location source to GPS?"), i18n("GPS Settings"), 10); } else executeSetGPS(); } void Mount::syncGPS() { // We only update when location is OK INumberVectorProperty *location = currentGPS->getBaseDevice()->getNumber("GEOGRAPHIC_COORD"); if (location == nullptr || location->s != IPS_OK) return; // Sync name if (currentTelescope) { ITextVectorProperty *activeDevices = currentTelescope->getBaseDevice()->getText("ACTIVE_DEVICES"); if (activeDevices) { IText *activeGPS = IUFindText(activeDevices, "ACTIVE_GPS"); if (activeGPS) { if (strcmp(activeGPS->text, currentGPS->getDeviceName())) { IUSaveText(activeGPS, currentGPS->getDeviceName()); currentTelescope->getDriverInfo()->getClientManager()->sendNewText(activeDevices); } } } } // GPS Refresh should only be called once automatically. if (GPSInitialized == false) { ISwitchVectorProperty *refreshGPS = currentGPS->getBaseDevice()->getSwitch("GPS_REFRESH"); if (refreshGPS) { refreshGPS->sp[0].s = ISS_ON; currentGPS->getDriverInfo()->getClientManager()->sendNewSwitch(refreshGPS); GPSInitialized = true; } } } void Mount::setTrackEnabled(bool enabled) { if (enabled) trackOnB->click(); else trackOffB->click(); } int Mount::slewRate() { if (currentTelescope == nullptr) return -1; return currentTelescope->getSlewRate(); } //QJsonArray Mount::getScopes() const //{ // QJsonArray scopes; // if (currentTelescope == nullptr) // return scopes; // QJsonObject primary = // { // {"name", "Primary"}, // {"mount", currentTelescope->getDeviceName()}, // {"aperture", primaryScopeApertureIN->value()}, // {"focalLength", primaryScopeFocalIN->value()}, // }; // scopes.append(primary); // QJsonObject guide = // { // {"name", "Guide"}, // {"mount", currentTelescope->getDeviceName()}, // {"aperture", primaryScopeApertureIN->value()}, // {"focalLength", primaryScopeFocalIN->value()}, // }; // scopes.append(guide); // return scopes; //} void Mount::startParkTimer() { if (currentTelescope == nullptr || m_ParkStatus == ISD::PARK_UNKNOWN) return; if (currentTelescope->isParked()) { appendLogText("Mount already parked."); return; } QTime parkTime = startupTimeEdit->time(); qCDebug(KSTARS_EKOS_MOUNT) << "Parking time is" << parkTime.toString(); QDateTime currentDateTime = KStarsData::Instance()->lt(); QDateTime parkDateTime(currentDateTime); parkDateTime.setTime(parkTime); qint64 parkMilliSeconds = parkDateTime.msecsTo(currentDateTime); qCDebug(KSTARS_EKOS_MOUNT) << "Until parking time:" << parkMilliSeconds << "ms or" << parkMilliSeconds / (60 * 60 * 1000) << "hours"; if (parkMilliSeconds > 0) { qCDebug(KSTARS_EKOS_MOUNT) << "Added a day to parking time..."; parkDateTime = parkDateTime.addDays(1); parkMilliSeconds = parkDateTime.msecsTo(currentDateTime); int hours = parkMilliSeconds / (1000 * 60 * 60); if (hours > 0) { // No need to display warning for every day check if (everyDayCheck->isChecked() == false) appendLogText(i18n("Parking time cannot be in the past.")); return; } else if (std::abs(hours) > 12) { qCDebug(KSTARS_EKOS_MOUNT) << "Parking time is" << hours << "which exceeds 12 hours, auto park is disabled."; return; } } parkMilliSeconds = std::abs(parkMilliSeconds); if (parkMilliSeconds > 24 * 60 * 60 * 1000) { appendLogText(i18n("Parking time must be within 24 hours of current time.")); return; } if (parkMilliSeconds > 12 * 60 * 60 * 1000) appendLogText(i18n("Warning! Parking time is more than 12 hours away.")); appendLogText(i18n("Caution: do not use Auto Park while scheduler is active.")); autoParkTimer.setInterval(parkMilliSeconds); autoParkTimer.start(); startTimerB->setEnabled(false); stopTimerB->setEnabled(true); } void Mount::stopParkTimer() { autoParkTimer.stop(); countdownLabel->setText("00:00:00"); stopTimerB->setEnabled(false); startTimerB->setEnabled(true); } void Mount::startAutoPark() { appendLogText(i18n("Parking timer is up.")); autoParkTimer.stop(); startTimerB->setEnabled(true); stopTimerB->setEnabled(false); countdownLabel->setText("00:00:00"); if (currentTelescope) { if (currentTelescope->isParked() == false) { appendLogText(i18n("Starting auto park...")); park(); } } } } diff --git a/kstars/fitsviewer/fitsdata.cpp b/kstars/fitsviewer/fitsdata.cpp index 96ec98fb6..d1111a48a 100644 --- a/kstars/fitsviewer/fitsdata.cpp +++ b/kstars/fitsviewer/fitsdata.cpp @@ -1,4319 +1,4324 @@ /*************************************************************************** FITSImage.cpp - FITS Image ------------------- begin : Thu Jan 22 2004 copyright : (C) 2004 by Jasem Mutlaq email : mutlaqja@ikarustech.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. * * * * Some code fragments were adapted from Peter Kirchgessner's FITS plugin* * See http://members.aol.com/pkirchg for more details. * ***************************************************************************/ #include "fitsdata.h" #include "sep/sep.h" #include "fpack.h" #include "kstarsdata.h" #include "ksutils.h" #include "kspaths.h" #include "Options.h" #include "skymapcomposite.h" #include "auxiliary/ksnotification.h" #include #include #include #include #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB) #include #include #endif #ifndef KSTARS_LITE #include "fitshistogram.h" #endif #include #include #include #define ZOOM_DEFAULT 100.0 #define ZOOM_MIN 10 #define ZOOM_MAX 400 #define ZOOM_LOW_INCR 10 #define ZOOM_HIGH_INCR 50 const int MINIMUM_ROWS_PER_CENTER = 3; const QString FITSData::m_TemporaryPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation); #define DIFFUSE_THRESHOLD 0.15 #define MAX_EDGE_LIMIT 10000 #define LOW_EDGE_CUTOFF_1 50 #define LOW_EDGE_CUTOFF_2 10 #define MINIMUM_EDGE_LIMIT 2 bool greaterThan(Edge * s1, Edge * s2) { //return s1->width > s2->width; return s1->sum > s2->sum; } FITSData::FITSData(FITSMode fitsMode): m_Mode(fitsMode) { debayerParams.method = DC1394_BAYER_METHOD_NEAREST; debayerParams.filter = DC1394_COLOR_FILTER_RGGB; debayerParams.offsetX = debayerParams.offsetY = 0; } FITSData::FITSData(const FITSData * other) { debayerParams.method = DC1394_BAYER_METHOD_NEAREST; debayerParams.filter = DC1394_COLOR_FILTER_RGGB; debayerParams.offsetX = debayerParams.offsetY = 0; this->m_Mode = other->m_Mode; this->m_DataType = other->m_DataType; this->m_Channels = other->m_Channels; memcpy(&stats, &(other->stats), sizeof(stats)); m_ImageBuffer = new uint8_t[stats.samples_per_channel * m_Channels * stats.bytesPerPixel]; memcpy(m_ImageBuffer, other->m_ImageBuffer, stats.samples_per_channel * m_Channels * stats.bytesPerPixel); } FITSData::~FITSData() { int status = 0; clearImageBuffers(); if (starCenters.count() > 0) qDeleteAll(starCenters); delete[] wcs_coord; if (objList.count() > 0) qDeleteAll(objList); if (fptr != nullptr) { fits_close_file(fptr, &status); fptr = nullptr; if (m_isTemporary && autoRemoveTemporaryFITS) QFile::remove(m_Filename); } qDeleteAll(records); } void FITSData::loadCommon(const QString &inFilename) { int status = 0; qDeleteAll(starCenters); starCenters.clear(); if (fptr != nullptr) { fits_close_file(fptr, &status); fptr = nullptr; // If current file is temporary AND // Auto Remove Temporary File is Set AND // New filename is different from existing filename // THen remove it. We have to check for name since we cannot delete // the same filename and try to open it below! if (m_isTemporary && autoRemoveTemporaryFITS && inFilename != m_Filename) QFile::remove(m_Filename); } m_Filename = inFilename; } bool FITSData::loadFITSFromMemory(const QString &inFilename, void *fits_buffer, size_t fits_buffer_size, bool silent) { loadCommon(inFilename); qCInfo(KSTARS_FITS) << "Reading FITS file buffer "; return privateLoad(fits_buffer, fits_buffer_size, silent); } QFuture FITSData::loadFITS(const QString &inFilename, bool silent) { loadCommon(inFilename); qCInfo(KSTARS_FITS) << "Loading FITS file " << m_Filename; QFuture result = QtConcurrent::run( this, &FITSData::privateLoad, nullptr, 0, silent); return result; } namespace { // Common code for reporting fits read errors. Always returns false. bool fitsOpenError(int status, const QString &message, bool silent) { char error_status[512]; fits_report_error(stderr, status); fits_get_errstatus(status, error_status); QString errMessage = message; errMessage.append(i18n(" Error: %1", QString::fromUtf8(error_status))); if (!silent) KSNotification::error(errMessage, i18n("FITS Open")); qCCritical(KSTARS_FITS) << errMessage; return false; } } bool FITSData::privateLoad(void *fits_buffer, size_t fits_buffer_size, bool silent) { int status = 0, anynull = 0; long naxes[3]; QString errMessage; m_isTemporary = m_Filename.startsWith(m_TemporaryPath); if (fits_buffer == nullptr && m_Filename.endsWith(".fz")) { // Store so we don't lose. m_compressedFilename = m_Filename; QString uncompressedFile = QDir::tempPath() + QString("/%1").arg(QUuid::createUuid().toString().remove(QRegularExpression("[-{}]"))); fpstate fpvar; fp_init (&fpvar); if (fp_unpack(m_Filename.toLatin1().data(), uncompressedFile.toLatin1().data(), fpvar) < 0) { errMessage = i18n("Failed to unpack compressed fits"); if (!silent) KSNotification::error(errMessage, i18n("FITS Open")); qCCritical(KSTARS_FITS) << errMessage; return false; } // Remove compressed .fz if it was temporary if (m_isTemporary && autoRemoveTemporaryFITS) QFile::remove(m_Filename); m_Filename = uncompressedFile; m_isTemporary = true; m_isCompressed = true; } if (fits_buffer == nullptr) { // Use open diskfile as it does not use extended file names which has problems opening // files with [ ] or ( ) in their names. if (fits_open_diskfile(&fptr, m_Filename.toLatin1(), READONLY, &status)) return fitsOpenError(status, i18n("Error opening fits file %1", m_Filename), silent); else stats.size = QFile(m_Filename).size(); } else { // Read the FITS file from a memory buffer. void *temp_buffer = fits_buffer; size_t temp_size = fits_buffer_size; if (fits_open_memfile(&fptr, m_Filename.toLatin1().data(), READONLY, &temp_buffer, &temp_size, 0, nullptr, &status)) return fitsOpenError(status, i18n("Error reading fits buffer."), silent); else stats.size = fits_buffer_size; } if (fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status)) return fitsOpenError(status, i18n("Could not locate image HDU."), silent); if (fits_get_img_param(fptr, 3, &(stats.bitpix), &(stats.ndim), naxes, &status)) return fitsOpenError(status, i18n("FITS file open error (fits_get_img_param)."), silent); if (stats.ndim < 2) { errMessage = i18n("1D FITS images are not supported in KStars."); if (!silent) KSNotification::error(errMessage, i18n("FITS Open")); qCCritical(KSTARS_FITS) << errMessage; return false; } switch (stats.bitpix) { case BYTE_IMG: m_DataType = TBYTE; stats.bytesPerPixel = sizeof(uint8_t); break; case SHORT_IMG: // Read SHORT image as USHORT m_DataType = TUSHORT; stats.bytesPerPixel = sizeof(int16_t); break; case USHORT_IMG: m_DataType = TUSHORT; stats.bytesPerPixel = sizeof(uint16_t); break; case LONG_IMG: // Read LONG image as ULONG m_DataType = TULONG; stats.bytesPerPixel = sizeof(int32_t); break; case ULONG_IMG: m_DataType = TULONG; stats.bytesPerPixel = sizeof(uint32_t); break; case FLOAT_IMG: m_DataType = TFLOAT; stats.bytesPerPixel = sizeof(float); break; case LONGLONG_IMG: m_DataType = TLONGLONG; stats.bytesPerPixel = sizeof(int64_t); break; case DOUBLE_IMG: m_DataType = TDOUBLE; stats.bytesPerPixel = sizeof(double); break; default: errMessage = i18n("Bit depth %1 is not supported.", stats.bitpix); if (!silent) KSNotification::error(errMessage, i18n("FITS Open")); qCCritical(KSTARS_FITS) << errMessage; return false; } if (stats.ndim < 3) naxes[2] = 1; if (naxes[0] == 0 || naxes[1] == 0) { errMessage = i18n("Image has invalid dimensions %1x%2", naxes[0], naxes[1]); if (!silent) KSNotification::error(errMessage, i18n("FITS Open")); qCCritical(KSTARS_FITS) << errMessage; return false; } stats.width = naxes[0]; stats.height = naxes[1]; stats.samples_per_channel = stats.width * stats.height; clearImageBuffers(); m_Channels = naxes[2]; // Channels always set to #1 if we are not required to process 3D Cubes // Or if mode is not FITS_NORMAL (guide, focus..etc) if (m_Mode != FITS_NORMAL || !Options::auto3DCube()) m_Channels = 1; m_ImageBuffer = new uint8_t[stats.samples_per_channel * m_Channels * stats.bytesPerPixel]; if (m_ImageBuffer == nullptr) { qCWarning(KSTARS_FITS) << "FITSData: Not enough memory for image_buffer channel. Requested: " << stats.samples_per_channel * m_Channels * stats.bytesPerPixel << " bytes."; clearImageBuffers(); return false; } rotCounter = 0; flipHCounter = 0; flipVCounter = 0; long nelements = stats.samples_per_channel * m_Channels; if (fits_read_img(fptr, m_DataType, 1, nelements, nullptr, m_ImageBuffer, &anynull, &status)) return fitsOpenError(status, i18n("Error reading image."), silent); parseHeader(); if (Options::autoDebayer() && checkDebayer()) { bayerBuffer = m_ImageBuffer; if (debayer()) calculateStats(); } else calculateStats(); WCSLoaded = false; if (m_Mode == FITS_NORMAL || m_Mode == FITS_ALIGN) checkForWCS(); starsSearched = false; return true; } int FITSData::saveFITS(const QString &newFilename) { if (newFilename == m_Filename) return 0; if (m_isCompressed) { KSNotification::error(i18n("Saving compressed files is not supported.")); return -1; } int status = 0, exttype = 0; long nelements; fitsfile * new_fptr; if (HasDebayer) { /* close current file */ if (fits_close_file(fptr, &status)) { fits_report_error(stderr, status); return status; } // Skip "!" in the beginning of the new file name QString finalFileName(newFilename); finalFileName.remove('!'); // Remove first otherwise copy will fail below if file exists QFile::remove(finalFileName); if (!QFile::copy(m_Filename, finalFileName)) { qCCritical(KSTARS_FITS()) << "FITS: Failed to copy " << m_Filename << " to " << finalFileName; fptr = nullptr; return -1; } if (m_isTemporary && autoRemoveTemporaryFITS) { QFile::remove(m_Filename); m_isTemporary = false; } m_Filename = finalFileName; // Use open diskfile as it does not use extended file names which has problems opening // files with [ ] or ( ) in their names. fits_open_diskfile(&fptr, m_Filename.toLatin1(), READONLY, &status); fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status); return 0; } nelements = stats.samples_per_channel * m_Channels; /* Create a new File, overwriting existing*/ if (fits_create_file(&new_fptr, newFilename.toLatin1(), &status)) { fits_report_error(stderr, status); return status; } // if (fits_movabs_hdu(fptr, 1, &exttype, &status)) // { // fits_report_error(stderr, status); // return status; // } if (fits_copy_header(fptr, new_fptr, &status)) { fits_report_error(stderr, status); return status; } /* close current file */ if (fits_close_file(fptr, &status)) { fits_report_error(stderr, status); return status; } status = 0; fptr = new_fptr; if (fits_movabs_hdu(fptr, 1, &exttype, &status)) { fits_report_error(stderr, status); return status; } /* Write Data */ if (fits_write_img(fptr, m_DataType, 1, nelements, m_ImageBuffer, &status)) { fits_report_error(stderr, status); return status; } /* Write keywords */ // Minimum if (fits_update_key(fptr, TDOUBLE, "DATAMIN", &(stats.min), "Minimum value", &status)) { fits_report_error(stderr, status); return status; } // Maximum if (fits_update_key(fptr, TDOUBLE, "DATAMAX", &(stats.max), "Maximum value", &status)) { fits_report_error(stderr, status); return status; } // NAXIS1 if (fits_update_key(fptr, TUSHORT, "NAXIS1", &(stats.width), "length of data axis 1", &status)) { fits_report_error(stderr, status); return status; } // NAXIS2 if (fits_update_key(fptr, TUSHORT, "NAXIS2", &(stats.height), "length of data axis 2", &status)) { fits_report_error(stderr, status); return status; } // ISO Date if (fits_write_date(fptr, &status)) { fits_report_error(stderr, status); return status; } QString history = QString("Modified by KStars on %1").arg(QDateTime::currentDateTime().toString("yyyy-MM-ddThh:mm:ss")); // History if (fits_write_history(fptr, history.toLatin1(), &status)) { fits_report_error(stderr, status); return status; } int rot = 0, mirror = 0; if (rotCounter > 0) { rot = (90 * rotCounter) % 360; if (rot < 0) rot += 360; } if (flipHCounter % 2 != 0 || flipVCounter % 2 != 0) mirror = 1; if ((rot != 0) || (mirror != 0)) rotWCSFITS(rot, mirror); rotCounter = flipHCounter = flipVCounter = 0; if (m_isTemporary && autoRemoveTemporaryFITS) { QFile::remove(m_Filename); m_isTemporary = false; } m_Filename = newFilename; fits_flush_file(fptr, &status); qCInfo(KSTARS_FITS) << "Saved FITS file:" << m_Filename; return status; } void FITSData::clearImageBuffers() { delete[] m_ImageBuffer; m_ImageBuffer = nullptr; bayerBuffer = nullptr; } void FITSData::calculateStats(bool refresh) { // Calculate min max calculateMinMax(refresh); // Get standard deviation and mean in one run switch (m_DataType) { case TBYTE: runningAverageStdDev(); break; case TSHORT: runningAverageStdDev(); break; case TUSHORT: runningAverageStdDev(); break; case TLONG: runningAverageStdDev(); break; case TULONG: runningAverageStdDev(); break; case TFLOAT: runningAverageStdDev(); break; case TLONGLONG: runningAverageStdDev(); break; case TDOUBLE: runningAverageStdDev(); break; default: return; } // FIXME That's not really SNR, must implement a proper solution for this value stats.SNR = stats.mean[0] / stats.stddev[0]; if (refresh && markStars) // Let's try to find star positions again after transformation starsSearched = false; } int FITSData::calculateMinMax(bool refresh) { int status, nfound = 0; status = 0; if ((fptr != nullptr) && !refresh) { if (fits_read_key_dbl(fptr, "DATAMIN", &(stats.min[0]), nullptr, &status) == 0) nfound++; if (fits_read_key_dbl(fptr, "DATAMAX", &(stats.max[0]), nullptr, &status) == 0) nfound++; // If we found both keywords, no need to calculate them, unless they are both zeros if (nfound == 2 && !(stats.min[0] == 0 && stats.max[0] == 0)) return 0; } stats.min[0] = 1.0E30; stats.max[0] = -1.0E30; stats.min[1] = 1.0E30; stats.max[1] = -1.0E30; stats.min[2] = 1.0E30; stats.max[2] = -1.0E30; switch (m_DataType) { case TBYTE: calculateMinMax(); break; case TSHORT: calculateMinMax(); break; case TUSHORT: calculateMinMax(); break; case TLONG: calculateMinMax(); break; case TULONG: calculateMinMax(); break; case TFLOAT: calculateMinMax(); break; case TLONGLONG: calculateMinMax(); break; case TDOUBLE: calculateMinMax(); break; default: break; } //qDebug() << "DATAMIN: " << stats.min << " - DATAMAX: " << stats.max; return 0; } template QPair FITSData::getParitionMinMax(uint32_t start, uint32_t stride) { auto * buffer = reinterpret_cast(m_ImageBuffer); T min = std::numeric_limits::max(); T max = std::numeric_limits::min(); uint32_t end = start + stride; for (uint32_t i = start; i < end; i++) { if (buffer[i] < min) min = buffer[i]; else if (buffer[i] > max) max = buffer[i]; } return qMakePair(min, max); } template void FITSData::calculateMinMax() { T min = std::numeric_limits::max(); T max = std::numeric_limits::min(); // Create N threads const uint8_t nThreads = 16; for (int n = 0; n < m_Channels; n++) { uint32_t cStart = n * stats.samples_per_channel; // Calculate how many elements we process per thread uint32_t tStride = stats.samples_per_channel / nThreads; // Calculate the final stride since we can have some left over due to division above uint32_t fStride = tStride + (stats.samples_per_channel - (tStride * nThreads)); // Start location for inspecting elements uint32_t tStart = cStart; // List of futures QList>> futures; for (int i = 0; i < nThreads; i++) { // Run threads futures.append(QtConcurrent::run(this, &FITSData::getParitionMinMax, tStart, (i == (nThreads - 1)) ? fStride : tStride)); tStart += tStride; } // Now wait for results for (int i = 0; i < nThreads; i++) { QPair result = futures[i].result(); if (result.first < min) min = result.first; if (result.second > max) max = result.second; } stats.min[n] = min; stats.max[n] = max; } } template QPair FITSData::getSquaredSumAndMean(uint32_t start, uint32_t stride) { uint32_t m_n = 2; double m_oldM = 0, m_newM = 0, m_oldS = 0, m_newS = 0; auto * buffer = reinterpret_cast(m_ImageBuffer); uint32_t end = start + stride; for (uint32_t i = start; i < end; i++) { m_newM = m_oldM + (buffer[i] - m_oldM) / m_n; m_newS = m_oldS + (buffer[i] - m_oldM) * (buffer[i] - m_newM); m_oldM = m_newM; m_oldS = m_newS; m_n++; } return qMakePair(m_newM, m_newS); } template void FITSData::runningAverageStdDev() { // Create N threads const uint8_t nThreads = 16; for (int n = 0; n < m_Channels; n++) { uint32_t cStart = n * stats.samples_per_channel; // Calculate how many elements we process per thread uint32_t tStride = stats.samples_per_channel / nThreads; // Calculate the final stride since we can have some left over due to division above uint32_t fStride = tStride + (stats.samples_per_channel - (tStride * nThreads)); // Start location for inspecting elements uint32_t tStart = cStart; // List of futures QList>> futures; for (int i = 0; i < nThreads; i++) { // Run threads futures.append(QtConcurrent::run(this, &FITSData::getSquaredSumAndMean, tStart, (i == (nThreads - 1)) ? fStride : tStride)); tStart += tStride; } double mean = 0, squared_sum = 0; // Now wait for results for (int i = 0; i < nThreads; i++) { QPair result = futures[i].result(); mean += result.first; squared_sum += result.second; } double variance = squared_sum / stats.samples_per_channel; stats.mean[n] = mean / nThreads; stats.stddev[n] = sqrt(variance); } } void FITSData::setMinMax(double newMin, double newMax, uint8_t channel) { stats.min[channel] = newMin; stats.max[channel] = newMax; } bool FITSData::parseHeader() { char * header = nullptr; int status = 0, nkeys = 0; if (fits_hdr2str(fptr, 0, nullptr, 0, &header, &nkeys, &status)) { fits_report_error(stderr, status); free(header); return false; } QString recordList = QString(header); for (int i = 0; i < nkeys; i++) { Record * oneRecord = new Record; // Quotes cause issues for simplified below so we're removing them. QString record = recordList.mid(i * 80, 80).remove("'"); QStringList properties = record.split(QRegExp("[=/]")); // If it is only a comment if (properties.size() == 1) { oneRecord->key = properties[0].mid(0, 7); oneRecord->comment = properties[0].mid(8).simplified(); } else { oneRecord->key = properties[0].simplified(); oneRecord->value = properties[1].simplified(); if (properties.size() > 2) oneRecord->comment = properties[2].simplified(); // Try to guess the value. // Test for integer & double. If neither, then leave it as "string". bool ok = false; // Is it Integer? oneRecord->value.toInt(&ok); if (ok) oneRecord->value.convert(QMetaType::Int); else { // Is it double? oneRecord->value.toDouble(&ok); if (ok) oneRecord->value.convert(QMetaType::Double); } } records.append(oneRecord); } free(header); return true; } bool FITSData::getRecordValue(const QString &key, QVariant &value) const { for (Record * oneRecord : records) { if (oneRecord->key == key) { value = oneRecord->value; return true; } } return false; } bool FITSData::checkCollision(Edge * s1, Edge * s2) { int dis; //distance int diff_x = s1->x - s2->x; int diff_y = s1->y - s2->y; dis = std::abs(sqrt(diff_x * diff_x + diff_y * diff_y)); dis -= s1->width / 2; dis -= s2->width / 2; if (dis <= 0) //collision return true; //no collision return false; } int FITSData::findCannyStar(FITSData * data, const QRect &boundary) { switch (data->property("dataType").toInt()) { case TBYTE: return FITSData::findCannyStar(data, boundary); case TSHORT: return FITSData::findCannyStar(data, boundary); case TUSHORT: return FITSData::findCannyStar(data, boundary); case TLONG: return FITSData::findCannyStar(data, boundary); case TULONG: return FITSData::findCannyStar(data, boundary); case TFLOAT: return FITSData::findCannyStar(data, boundary); case TLONGLONG: return FITSData::findCannyStar(data, boundary); case TDOUBLE: return FITSData::findCannyStar(data, boundary); default: break; } return 0; } int FITSData::findStars(StarAlgorithm algorithm, const QRect &trackingBox) { int count = 0; starAlgorithm = algorithm; qDeleteAll(starCenters); starCenters.clear(); switch (algorithm) { case ALGORITHM_SEP: count = findSEPStars(trackingBox); break; case ALGORITHM_GRADIENT: count = findCannyStar(this, trackingBox); break; case ALGORITHM_CENTROID: count = findCentroid(trackingBox); break; case ALGORITHM_THRESHOLD: count = findOneStar(trackingBox); break; } starsSearched = true; return count; } int FITSData::filterStars(const float innerRadius, const float outerRadius) { long const sqDiagonal = this->width() * this->width() / 4 + this->height() * this->height() / 4; long const sqInnerRadius = std::lround(sqDiagonal * innerRadius * innerRadius); long const sqOuterRadius = std::lround(sqDiagonal * outerRadius * outerRadius); starCenters.erase(std::remove_if(starCenters.begin(), starCenters.end(), [&](Edge * edge) { long const x = edge->x - this->width() / 2; long const y = edge->y - this->height() / 2; long const sqRadius = x * x + y * y; return sqRadius < sqInnerRadius || sqOuterRadius < sqRadius; }), starCenters.end()); return starCenters.count(); } template int FITSData::findCannyStar(FITSData * data, const QRect &boundary) { int subX = qMax(0, boundary.isNull() ? 0 : boundary.x()); int subY = qMax(0, boundary.isNull() ? 0 : boundary.y()); int subW = (boundary.isNull() ? data->width() : boundary.width()); int subH = (boundary.isNull() ? data->height() : boundary.height()); int BBP = data->getBytesPerPixel(); uint16_t dataWidth = data->width(); // #1 Find offsets uint32_t size = subW * subH; uint32_t offset = subX + subY * dataWidth; // #2 Create new buffer auto * buffer = new uint8_t[size * BBP]; // If there is no offset, copy whole buffer in one go if (offset == 0) memcpy(buffer, data->getImageBuffer(), size * BBP); else { uint8_t * dataPtr = buffer; uint8_t * origDataPtr = data->getImageBuffer(); uint32_t lineOffset = 0; // Copy data line by line for (int height = subY; height < (subY + subH); height++) { lineOffset = (subX + height * dataWidth) * BBP; memcpy(dataPtr, origDataPtr + lineOffset, subW * BBP); dataPtr += (subW * BBP); } } // #3 Create new FITSData to hold it auto * boundedImage = new FITSData(); boundedImage->stats.width = subW; boundedImage->stats.height = subH; boundedImage->stats.bitpix = data->stats.bitpix; boundedImage->stats.bytesPerPixel = data->stats.bytesPerPixel; boundedImage->stats.samples_per_channel = size; boundedImage->stats.ndim = 2; boundedImage->setProperty("dataType", data->property("dataType")); // #4 Set image buffer and calculate stats. boundedImage->setImageBuffer(buffer); boundedImage->calculateStats(true); // #5 Apply Median + High Contrast filter to remove noise and move data to non-linear domain boundedImage->applyFilter(FITS_MEDIAN); boundedImage->applyFilter(FITS_HIGH_CONTRAST); // #6 Perform Sobel to find gradients and their directions QVector gradients; QVector directions; // TODO Must trace neighbours and assign IDs to each shape so that they can be centered massed // and discarded whenever necessary. It won't work on noisy images unless this is done. boundedImage->sobel(gradients, directions); QVector ids(gradients.size()); int maxID = boundedImage->partition(subW, subH, gradients, ids); // Not needed anymore delete boundedImage; if (maxID == 0) return 0; typedef struct { float massX = 0; float massY = 0; float totalMass = 0; } massInfo; QMap masses; // #7 Calculate center of mass for all detected regions for (int y = 0; y < subH; y++) { for (int x = 0; x < subW; x++) { int index = x + y * subW; int regionID = ids[index]; if (regionID > 0) { float pixel = gradients[index]; masses[regionID].totalMass += pixel; masses[regionID].massX += x * pixel; masses[regionID].massY += y * pixel; } } } // Compare multiple masses, and only select the highest total mass one as the desired star int maxRegionID = 1; int maxTotalMass = masses[1].totalMass; double totalMassRatio = 1e6; for (auto key : masses.keys()) { massInfo oneMass = masses.value(key); if (oneMass.totalMass > maxTotalMass) { totalMassRatio = oneMass.totalMass / maxTotalMass; maxTotalMass = oneMass.totalMass; maxRegionID = key; } } // If image has many regions and there is no significant relative center of mass then it's just noise and no stars // are probably there above a useful threshold. if (maxID > 10 && totalMassRatio < 1.5) return 0; auto * center = new Edge; center->width = -1; center->x = masses[maxRegionID].massX / masses[maxRegionID].totalMass + 0.5; center->y = masses[maxRegionID].massY / masses[maxRegionID].totalMass + 0.5; center->HFR = 1; // Maximum Radius int maxR = qMin(subW - 1, subH - 1) / 2; for (int r = maxR; r > 1; r--) { int pass = 0; for (float theta = 0; theta < 2 * M_PI; theta += (2 * M_PI) / 36.0) { int testX = center->x + std::cos(theta) * r; int testY = center->y + std::sin(theta) * r; // if out of bound, break; if (testX < 0 || testX >= subW || testY < 0 || testY >= subH) break; if (gradients[testX + testY * subW] > 0) //if (thresholded[testX + testY * subW] > 0) { if (++pass >= 24) { center->width = r * 2; // Break of outer loop r = 0; break; } } } } qCDebug(KSTARS_FITS) << "FITS: Weighted Center is X: " << center->x << " Y: " << center->y << " Width: " << center->width; // If no stars were detected if (center->width == -1) { delete center; return 0; } // 30% fuzzy //center->width += center->width*0.3 * (running_threshold / threshold); double FSum = 0, HF = 0, TF = 0; const double resolution = 1.0 / 20.0; int cen_y = qRound(center->y); double rightEdge = center->x + center->width / 2.0; double leftEdge = center->x - center->width / 2.0; QVector subPixels; subPixels.reserve(center->width / resolution); const T * origBuffer = reinterpret_cast(data->getImageBuffer()) + offset; for (double x = leftEdge; x <= rightEdge; x += resolution) { double slice = resolution * (origBuffer[static_cast(floor(x)) + cen_y * dataWidth]); FSum += slice; subPixels.append(slice); } // Half flux HF = FSum / 2.0; int subPixelCenter = (center->width / resolution) / 2; // Start from center TF = subPixels[subPixelCenter]; double lastTF = TF; // Integrate flux along radius axis until we reach half flux //for (double k=resolution; k < (center->width/(2*resolution)); k += resolution) for (int k = 1; k < subPixelCenter; k++) { TF += subPixels[subPixelCenter + k]; TF += subPixels[subPixelCenter - k]; if (TF >= HF) { // We overpassed HF, let's calculate from last TF how much until we reach HF // #1 Accurate calculation, but very sensitive to small variations of flux center->HFR = (k - 1 + ((HF - lastTF) / (TF - lastTF)) * 2) * resolution; // #2 Less accurate calculation, but stable against small variations of flux //center->HFR = (k - 1) * resolution; break; } lastTF = TF; } // Correct center for subX and subY center->x += subX; center->y += subY; data->appendStar(center); qCDebug(KSTARS_FITS) << "Flux: " << FSum << " Half-Flux: " << HF << " HFR: " << center->HFR; return 1; } int FITSData::findOneStar(const QRect &boundary) { switch (m_DataType) { case TBYTE: return findOneStar(boundary); case TSHORT: return findOneStar(boundary); case TUSHORT: return findOneStar(boundary); case TLONG: return findOneStar(boundary); case TULONG: return findOneStar(boundary); case TFLOAT: return findOneStar(boundary); case TLONGLONG: return findOneStar(boundary); case TDOUBLE: return findOneStar(boundary); default: break; } return 0; } template int FITSData::findOneStar(const QRect &boundary) { if (boundary.isEmpty()) return -1; int subX = boundary.x(); int subY = boundary.y(); int subW = subX + boundary.width(); int subH = subY + boundary.height(); float massX = 0, massY = 0, totalMass = 0; auto * buffer = reinterpret_cast(m_ImageBuffer); // TODO replace magic number with something more useful to understand double threshold = stats.mean[0] * Options::focusThreshold() / 100.0; for (int y = subY; y < subH; y++) { for (int x = subX; x < subW; x++) { T pixel = buffer[x + y * stats.width]; if (pixel > threshold) { totalMass += pixel; massX += x * pixel; massY += y * pixel; } } } qCDebug(KSTARS_FITS) << "FITS: Weighted Center is X: " << massX / totalMass << " Y: " << massY / totalMass; auto * center = new Edge; center->width = -1; center->x = massX / totalMass + 0.5; center->y = massY / totalMass + 0.5; center->HFR = 1; // Maximum Radius int maxR = qMin(subW - 1, subH - 1) / 2; // Critical threshold double critical_threshold = threshold * 0.7; double running_threshold = threshold; while (running_threshold >= critical_threshold) { for (int r = maxR; r > 1; r--) { int pass = 0; for (float theta = 0; theta < 2 * M_PI; theta += (2 * M_PI) / 10.0) { int testX = center->x + std::cos(theta) * r; int testY = center->y + std::sin(theta) * r; // if out of bound, break; if (testX < subX || testX > subW || testY < subY || testY > subH) break; if (buffer[testX + testY * stats.width] > running_threshold) pass++; } //qDebug() << "Testing for radius " << r << " passes # " << pass << " @ threshold " << running_threshold; //if (pass >= 6) if (pass >= 5) { center->width = r * 2; break; } } if (center->width > 0) break; // Increase threshold fuzziness by 10% running_threshold -= running_threshold * 0.1; } // If no stars were detected if (center->width == -1) { delete center; return 0; } // 30% fuzzy //center->width += center->width*0.3 * (running_threshold / threshold); starCenters.append(center); double FSum = 0, HF = 0, TF = 0, min = stats.min[0]; const double resolution = 1.0 / 20.0; int cen_y = qRound(center->y); double rightEdge = center->x + center->width / 2.0; double leftEdge = center->x - center->width / 2.0; QVector subPixels; subPixels.reserve(center->width / resolution); for (double x = leftEdge; x <= rightEdge; x += resolution) { //subPixels[x] = resolution * (image_buffer[static_cast(floor(x)) + cen_y * stats.width] - min); double slice = resolution * (buffer[static_cast(floor(x)) + cen_y * stats.width] - min); FSum += slice; subPixels.append(slice); } // Half flux HF = FSum / 2.0; //double subPixelCenter = center->x - fmod(center->x,resolution); int subPixelCenter = (center->width / resolution) / 2; // Start from center TF = subPixels[subPixelCenter]; double lastTF = TF; // Integrate flux along radius axis until we reach half flux //for (double k=resolution; k < (center->width/(2*resolution)); k += resolution) for (int k = 1; k < subPixelCenter; k++) { TF += subPixels[subPixelCenter + k]; TF += subPixels[subPixelCenter - k]; if (TF >= HF) { // We have two ways to calculate HFR. The first is the correct method but it can get quite variable within 10% due to random fluctuations of the measured star. // The second method is not truly HFR but is much more resistant to noise. // #1 Approximate HFR, accurate and reliable but quite variable to small changes in star flux center->HFR = (k - 1 + ((HF - lastTF) / (TF - lastTF)) * 2) * resolution; // #2 Not exactly HFR, but much more stable //center->HFR = (k*resolution) * (HF/TF); break; } lastTF = TF; } return 1; } /*** Find center of stars and calculate Half Flux Radius */ int FITSData::findCentroid(const QRect &boundary, int initStdDev, int minEdgeWidth) { switch (m_DataType) { case TBYTE: return findCentroid(boundary, initStdDev, minEdgeWidth); case TSHORT: return findCentroid(boundary, initStdDev, minEdgeWidth); case TUSHORT: return findCentroid(boundary, initStdDev, minEdgeWidth); case TLONG: return findCentroid(boundary, initStdDev, minEdgeWidth); case TULONG: return findCentroid(boundary, initStdDev, minEdgeWidth); case TFLOAT: return findCentroid(boundary, initStdDev, minEdgeWidth); case TLONGLONG: return findCentroid(boundary, initStdDev, minEdgeWidth); case TDOUBLE: return findCentroid(boundary, initStdDev, minEdgeWidth); default: return -1; } } template int FITSData::findCentroid(const QRect &boundary, int initStdDev, int minEdgeWidth) { double threshold = 0, sum = 0, avg = 0, min = 0; int starDiameter = 0; int pixVal = 0; int minimumEdgeCount = MINIMUM_EDGE_LIMIT; auto * buffer = reinterpret_cast(m_ImageBuffer); double JMIndex = 100; #ifndef KSTARS_LITE if (histogram) { if (!histogram->isConstructed()) histogram->constructHistogram(); JMIndex = histogram->getJMIndex(); } #endif float dispersion_ratio = 1.5; QList edges; if (JMIndex < DIFFUSE_THRESHOLD) { minEdgeWidth = JMIndex * 35 + 1; minimumEdgeCount = minEdgeWidth - 1; } else { minEdgeWidth = 6; minimumEdgeCount = 4; } while (initStdDev >= 1) { minEdgeWidth--; minimumEdgeCount--; minEdgeWidth = qMax(3, minEdgeWidth); minimumEdgeCount = qMax(3, minimumEdgeCount); if (JMIndex < DIFFUSE_THRESHOLD) { // Taking the average out seems to have better result for noisy images threshold = stats.max[0] - stats.mean[0] * ((MINIMUM_STDVAR - initStdDev) * 0.5 + 1); min = stats.min[0]; if (threshold - min < 0) { threshold = stats.mean[0] * ((MINIMUM_STDVAR - initStdDev) * 0.5 + 1); min = 0; } dispersion_ratio = 1.4 - (MINIMUM_STDVAR - initStdDev) * 0.08; } else { threshold = stats.mean[0] + stats.stddev[0] * initStdDev * (0.3 - (MINIMUM_STDVAR - initStdDev) * 0.05); min = stats.min[0]; // Ratio between centeroid center and edge dispersion_ratio = 1.8 - (MINIMUM_STDVAR - initStdDev) * 0.2; } qCDebug(KSTARS_FITS) << "SNR: " << stats.SNR; qCDebug(KSTARS_FITS) << "The threshold level is " << threshold << "(actual " << threshold - min << ") minimum edge width" << minEdgeWidth << " minimum edge limit " << minimumEdgeCount; threshold -= min; int subX, subY, subW, subH; if (boundary.isNull()) { if (m_Mode == FITS_GUIDE || m_Mode == FITS_FOCUS) { // Only consider the central 70% subX = round(stats.width * 0.15); subY = round(stats.height * 0.15); subW = stats.width - subX; subH = stats.height - subY; } else { // Consider the complete area 100% subX = 0; subY = 0; subW = stats.width; subH = stats.height; } } else { subX = boundary.x(); subY = boundary.y(); subW = subX + boundary.width(); subH = subY + boundary.height(); } // Detect "edges" that are above threshold for (int i = subY; i < subH; i++) { starDiameter = 0; for (int j = subX; j < subW; j++) { pixVal = buffer[j + (i * stats.width)] - min; // If pixel value > threshold, let's get its weighted average if (pixVal >= threshold) { avg += j * pixVal; sum += pixVal; starDiameter++; } // Value < threshold but avg exists else if (sum > 0) { // We found a potential centroid edge if (starDiameter >= minEdgeWidth) { float center = avg / sum + 0.5; if (center > 0) { int i_center = std::floor(center); // Check if center is 10% or more brighter than edge, if not skip if (((buffer[i_center + (i * stats.width)] - min) / (buffer[i_center + (i * stats.width) - starDiameter / 2] - min) >= dispersion_ratio) && ((buffer[i_center + (i * stats.width)] - min) / (buffer[i_center + (i * stats.width) + starDiameter / 2] - min) >= dispersion_ratio)) { qCDebug(KSTARS_FITS) << "Edge center is " << buffer[i_center + (i * stats.width)] - min << " Edge is " << buffer[i_center + (i * stats.width) - starDiameter / 2] - min << " and ratio is " << ((buffer[i_center + (i * stats.width)] - min) / (buffer[i_center + (i * stats.width) - starDiameter / 2] - min)) << " located at X: " << center << " Y: " << i + 0.5; auto * newEdge = new Edge(); newEdge->x = center; newEdge->y = i + 0.5; newEdge->scanned = 0; newEdge->val = buffer[i_center + (i * stats.width)] - min; newEdge->width = starDiameter; newEdge->HFR = 0; newEdge->sum = sum; edges.append(newEdge); } } } // Reset avg = sum = starDiameter = 0; } } } qCDebug(KSTARS_FITS) << "Total number of edges found is: " << edges.count(); // In case of hot pixels if (edges.count() == 1 && initStdDev > 1) { initStdDev--; continue; } if (edges.count() >= MAX_EDGE_LIMIT) { qCWarning(KSTARS_FITS) << "Too many edges, aborting... " << edges.count(); qDeleteAll(edges); return -1; } if (edges.count() >= minimumEdgeCount) break; qDeleteAll(edges); edges.clear(); initStdDev--; } int cen_count = 0; int cen_x = 0; int cen_y = 0; int cen_v = 0; int cen_w = 0; int width_sum = 0; // Let's sort edges, starting with widest std::sort(edges.begin(), edges.end(), greaterThan); // Now, let's scan the edges and find the maximum centroid vertically for (int i = 0; i < edges.count(); i++) { qCDebug(KSTARS_FITS) << "# " << i << " Edge at (" << edges[i]->x << "," << edges[i]->y << ") With a value of " << edges[i]->val << " and width of " << edges[i]->width << " pixels. with sum " << edges[i]->sum; // If edge scanned already, skip if (edges[i]->scanned == 1) { qCDebug(KSTARS_FITS) << "Skipping check for center " << i << " because it was already counted"; continue; } qCDebug(KSTARS_FITS) << "Investigating edge # " << i << " now ..."; // Get X, Y, and Val of edge cen_x = edges[i]->x; cen_y = edges[i]->y; cen_v = edges[i]->sum; cen_w = edges[i]->width; float avg_x = 0; float avg_y = 0; sum = 0; cen_count = 0; // Now let's compare to other edges until we hit a maxima for (int j = 0; j < edges.count(); j++) { if (edges[j]->scanned) continue; if (checkCollision(edges[j], edges[i])) { if (edges[j]->sum >= cen_v) { cen_v = edges[j]->sum; cen_w = edges[j]->width; } edges[j]->scanned = 1; cen_count++; avg_x += edges[j]->x * edges[j]->val; avg_y += edges[j]->y * edges[j]->val; sum += edges[j]->val; continue; } } int cen_limit = (MINIMUM_ROWS_PER_CENTER - (MINIMUM_STDVAR - initStdDev)); if (edges.count() < LOW_EDGE_CUTOFF_1) { if (edges.count() < LOW_EDGE_CUTOFF_2) cen_limit = 1; else cen_limit = 2; } qCDebug(KSTARS_FITS) << "center_count: " << cen_count << " and initstdDev= " << initStdDev << " and limit is " << cen_limit; if (cen_limit < 1) continue; // If centroid count is within acceptable range //if (cen_limit >= 2 && cen_count >= cen_limit) if (cen_count >= cen_limit) { // We detected a centroid, let's init it auto * rCenter = new Edge(); rCenter->x = avg_x / sum; rCenter->y = avg_y / sum; width_sum += rCenter->width; rCenter->width = cen_w; qCDebug(KSTARS_FITS) << "Found a real center with number with (" << rCenter->x << "," << rCenter->y << ")"; // Calculate Total Flux From Center, Half Flux, Full Summation double TF = 0; double HF = 0; double FSum = 0; cen_x = (int)std::floor(rCenter->x); cen_y = (int)std::floor(rCenter->y); if (cen_x < 0 || cen_x > stats.width || cen_y < 0 || cen_y > stats.height) { delete rCenter; continue; } // Complete sum along the radius //for (int k=0; k < rCenter->width; k++) for (int k = rCenter->width / 2; k >= -(rCenter->width / 2); k--) { FSum += buffer[cen_x - k + (cen_y * stats.width)] - min; //qDebug() << image_buffer[cen_x-k+(cen_y*stats.width)] - min; } // Half flux HF = FSum / 2.0; // Total flux starting from center TF = buffer[cen_y * stats.width + cen_x] - min; int pixelCounter = 1; // Integrate flux along radius axis until we reach half flux for (int k = 1; k < rCenter->width / 2; k++) { if (TF >= HF) { qCDebug(KSTARS_FITS) << "Stopping at TF " << TF << " after #" << k << " pixels."; break; } TF += buffer[cen_y * stats.width + cen_x + k] - min; TF += buffer[cen_y * stats.width + cen_x - k] - min; pixelCounter++; } // Calculate weighted Half Flux Radius rCenter->HFR = pixelCounter * (HF / TF); // Store full flux rCenter->val = FSum; qCDebug(KSTARS_FITS) << "HFR for this center is " << rCenter->HFR << " pixels and the total flux is " << FSum; starCenters.append(rCenter); } } if (starCenters.count() > 1 && m_Mode != FITS_FOCUS) { float width_avg = (float)width_sum / starCenters.count(); float lsum = 0, sdev = 0; for (auto ¢er : starCenters) lsum += (center->width - width_avg) * (center->width - width_avg); sdev = (std::sqrt(lsum / (starCenters.count() - 1))) * 4; // Reject stars > 4 * stddev foreach (Edge * center, starCenters) if (center->width > sdev) starCenters.removeOne(center); //foreach(Edge *center, starCenters) //qDebug() << center->x << "," << center->y << "," << center->width << "," << center->val << endl; } // Release memory qDeleteAll(edges); return starCenters.count(); } double FITSData::getHFR(HFRType type) { // This method is less susceptible to noise // Get HFR for the brightest star only, instead of averaging all stars // It is more consistent. // TODO: Try to test this under using a real CCD. if (starCenters.empty()) return -1; if (type == HFR_MAX) { maxHFRStar = nullptr; int maxVal = 0; int maxIndex = 0; for (int i = 0; i < starCenters.count(); i++) { if (starCenters[i]->HFR > maxVal) { maxIndex = i; maxVal = starCenters[i]->HFR; } } maxHFRStar = starCenters[maxIndex]; return static_cast(starCenters[maxIndex]->HFR); } QVector HFRs; for (auto center : starCenters) HFRs << center->HFR; std::sort(HFRs.begin(), HFRs.end()); double sum = std::accumulate(HFRs.begin(), HFRs.end(), 0.0); double m = sum / HFRs.size(); if (HFRs.size() > 3) { double accum = 0.0; std::for_each (HFRs.begin(), HFRs.end(), [&](const double d) { accum += (d - m) * (d - m); }); double stddev = sqrt(accum / (HFRs.size() - 1)); // Remove stars over 2 standard deviations away. auto end1 = std::remove_if(HFRs.begin(), HFRs.end(), [m, stddev](const double hfr) { return hfr > (m + stddev * 2); }); auto end2 = std::remove_if(HFRs.begin(), end1, [m, stddev](const double hfr) { return hfr < (m - stddev * 2); }); // New mean sum = std::accumulate(HFRs.begin(), end2, 0.0); const int num_remaining = std::distance(HFRs.begin(), end2); if (num_remaining > 0) m = sum / num_remaining; } return m; } double FITSData::getHFR(int x, int y) { if (starCenters.empty()) return -1; for (int i = 0; i < starCenters.count(); i++) { if (std::fabs(starCenters[i]->x - x) <= starCenters[i]->width / 2 && std::fabs(starCenters[i]->y - y) <= starCenters[i]->width / 2) { return starCenters[i]->HFR; } } return -1; } void FITSData::applyFilter(FITSScale type, uint8_t * image, QVector * min, QVector * max) { if (type == FITS_NONE) return; QVector dataMin(3); QVector dataMax(3); if (min) dataMin = *min; if (max) dataMax = *max; switch (type) { case FITS_AUTO_STRETCH: { for (int i = 0; i < 3; i++) { dataMin[i] = stats.mean[i] - stats.stddev[i]; dataMax[i] = stats.mean[i] + stats.stddev[i] * 3; } } break; case FITS_HIGH_CONTRAST: { for (int i = 0; i < 3; i++) { dataMin[i] = stats.mean[i] + stats.stddev[i]; dataMax[i] = stats.mean[i] + stats.stddev[i] * 3; } } break; case FITS_HIGH_PASS: { for (int i = 0; i < 3; i++) { dataMin[i] = stats.mean[i]; } } break; default: break; } switch (m_DataType) { case TBYTE: { for (int i = 0; i < 3; i++) { dataMin[i] = dataMin[i] < 0 ? 0 : dataMin[i]; dataMax[i] = dataMax[i] > UINT8_MAX ? UINT8_MAX : dataMax[i]; } applyFilter(type, image, &dataMin, &dataMax); } break; case TSHORT: { for (int i = 0; i < 3; i++) { dataMin[i] = dataMin[i] < INT16_MIN ? INT16_MIN : dataMin[i]; dataMax[i] = dataMax[i] > INT16_MAX ? INT16_MAX : dataMax[i]; } applyFilter(type, image, &dataMin, &dataMax); } break; case TUSHORT: { for (int i = 0; i < 3; i++) { dataMin[i] = dataMin[i] < 0 ? 0 : dataMin[i]; dataMax[i] = dataMax[i] > UINT16_MAX ? UINT16_MAX : dataMax[i]; } applyFilter(type, image, &dataMin, &dataMax); } break; case TLONG: { for (int i = 0; i < 3; i++) { dataMin[i] = dataMin[i] < INT_MIN ? INT_MIN : dataMin[i]; dataMax[i] = dataMax[i] > INT_MAX ? INT_MAX : dataMax[i]; } applyFilter(type, image, &dataMin, &dataMax); } break; case TULONG: { for (int i = 0; i < 3; i++) { dataMin[i] = dataMin[i] < 0 ? 0 : dataMin[i]; dataMax[i] = dataMax[i] > UINT_MAX ? UINT_MAX : dataMax[i]; } applyFilter(type, image, &dataMin, &dataMax); } break; case TFLOAT: { for (int i = 0; i < 3; i++) { dataMin[i] = dataMin[i] < FLT_MIN ? FLT_MIN : dataMin[i]; dataMax[i] = dataMax[i] > FLT_MAX ? FLT_MAX : dataMax[i]; } applyFilter(type, image, &dataMin, &dataMax); } break; case TLONGLONG: { for (int i = 0; i < 3; i++) { dataMin[i] = dataMin[i] < LLONG_MIN ? LLONG_MIN : dataMin[i]; dataMax[i] = dataMax[i] > LLONG_MAX ? LLONG_MAX : dataMax[i]; } applyFilter(type, image, &dataMin, &dataMax); } break; case TDOUBLE: { for (int i = 0; i < 3; i++) { dataMin[i] = dataMin[i] < DBL_MIN ? DBL_MIN : dataMin[i]; dataMax[i] = dataMax[i] > DBL_MAX ? DBL_MAX : dataMax[i]; } applyFilter(type, image, &dataMin, &dataMax); } break; default: return; } if (min != nullptr) *min = dataMin; if (max != nullptr) *max = dataMax; } template void FITSData::applyFilter(FITSScale type, uint8_t * targetImage, QVector * targetMin, QVector * targetMax) { bool calcStats = false; T * image = nullptr; if (targetImage) image = reinterpret_cast(targetImage); else { image = reinterpret_cast(m_ImageBuffer); calcStats = true; } T min[3], max[3]; for (int i = 0; i < 3; i++) { min[i] = (*targetMin)[i] < std::numeric_limits::min() ? std::numeric_limits::min() : (*targetMin)[i]; max[i] = (*targetMax)[i] > std::numeric_limits::max() ? std::numeric_limits::max() : (*targetMax)[i]; } // Create N threads const uint8_t nThreads = 16; uint32_t width = stats.width; uint32_t height = stats.height; //QTime timer; //timer.start(); switch (type) { case FITS_AUTO: case FITS_LINEAR: case FITS_AUTO_STRETCH: case FITS_HIGH_CONTRAST: case FITS_LOG: case FITS_SQRT: case FITS_HIGH_PASS: { // List of futures QList> futures; QVector coeff(3); if (type == FITS_LOG) { for (int i = 0; i < 3; i++) coeff[i] = max[i] / std::log(1 + max[i]); } else if (type == FITS_SQRT) { for (int i = 0; i < 3; i++) coeff[i] = max[i] / sqrt(max[i]); } for (int n = 0; n < m_Channels; n++) { if (type == FITS_HIGH_PASS) min[n] = stats.mean[n]; uint32_t cStart = n * stats.samples_per_channel; // Calculate how many elements we process per thread uint32_t tStride = stats.samples_per_channel / nThreads; // Calculate the final stride since we can have some left over due to division above uint32_t fStride = tStride + (stats.samples_per_channel - (tStride * nThreads)); T * runningBuffer = image + cStart; if (type == FITS_LOG) { for (int i = 0; i < nThreads; i++) { // Run threads futures.append(QtConcurrent::map(runningBuffer, (runningBuffer + ((i == (nThreads - 1)) ? fStride : tStride)), [min, max, coeff, n](T & a) { a = qBound(min[n], static_cast(round(coeff[n] * std::log(1 + qBound(min[n], a, max[n])))), max[n]); })); runningBuffer += tStride; } } else if (type == FITS_SQRT) { for (int i = 0; i < nThreads; i++) { // Run threads futures.append(QtConcurrent::map(runningBuffer, (runningBuffer + ((i == (nThreads - 1)) ? fStride : tStride)), [min, max, coeff, n](T & a) { a = qBound(min[n], static_cast(round(coeff[n] * a)), max[n]); })); } runningBuffer += tStride; } else { for (int i = 0; i < nThreads; i++) { // Run threads futures.append(QtConcurrent::map(runningBuffer, (runningBuffer + ((i == (nThreads - 1)) ? fStride : tStride)), [min, max, n](T & a) { a = qBound(min[n], a, max[n]); })); runningBuffer += tStride; } } } for (int i = 0; i < nThreads * m_Channels; i++) futures[i].waitForFinished(); if (calcStats) { for (int i = 0; i < 3; i++) { stats.min[i] = min[i]; stats.max[i] = max[i]; } //if (type != FITS_AUTO && type != FITS_LINEAR) runningAverageStdDev(); //QtConcurrent::run(this, &FITSData::runningAverageStdDev); } } break; case FITS_EQUALIZE: { #ifndef KSTARS_LITE if (histogram == nullptr) return; if (!histogram->isConstructed()) histogram->constructHistogram(); T bufferVal = 0; QVector cumulativeFreq = histogram->getCumulativeFrequency(); double coeff = 255.0 / (height * width); uint32_t row = 0; uint32_t index = 0; for (int i = 0; i < m_Channels; i++) { uint32_t offset = i * stats.samples_per_channel; for (uint32_t j = 0; j < height; j++) { row = offset + j * width; for (uint32_t k = 0; k < width; k++) { index = k + row; bufferVal = (image[index] - min[i]) / histogram->getBinWidth(i); if (bufferVal >= cumulativeFreq.size()) bufferVal = cumulativeFreq.size() - 1; image[index] = qBound(min[i], static_cast(round(coeff * cumulativeFreq[bufferVal])), max[i]); } } } #endif } if (calcStats) calculateStats(true); break; // Based on http://www.librow.com/articles/article-1 case FITS_MEDIAN: { uint8_t BBP = stats.bytesPerPixel; auto * extension = new T[(width + 2) * (height + 2)]; // Check memory allocation if (!extension) return; // Create image extension for (uint32_t ch = 0; ch < m_Channels; ch++) { uint32_t offset = ch * stats.samples_per_channel; uint32_t N = width, M = height; for (uint32_t i = 0; i < M; ++i) { memcpy(extension + (N + 2) * (i + 1) + 1, image + (N * i) + offset, N * BBP); extension[(N + 2) * (i + 1)] = image[N * i + offset]; extension[(N + 2) * (i + 2) - 1] = image[N * (i + 1) - 1 + offset]; } // Fill first line of image extension memcpy(extension, extension + N + 2, (N + 2) * BBP); // Fill last line of image extension memcpy(extension + (N + 2) * (M + 1), extension + (N + 2) * M, (N + 2) * BBP); // Call median filter implementation N = width + 2; M = height + 2; // Move window through all elements of the image for (uint32_t m = 1; m < M - 1; ++m) for (uint32_t n = 1; n < N - 1; ++n) { // Pick up window elements int k = 0; float window[9]; memset(&window[0], 0, 9 * sizeof(float)); for (uint32_t j = m - 1; j < m + 2; ++j) for (uint32_t i = n - 1; i < n + 2; ++i) window[k++] = extension[j * N + i]; // Order elements (only half of them) for (uint32_t j = 0; j < 5; ++j) { // Find position of minimum element int mine = j; for (uint32_t l = j + 1; l < 9; ++l) if (window[l] < window[mine]) mine = l; // Put found minimum element in its place const float temp = window[j]; window[j] = window[mine]; window[mine] = temp; } // Get result - the middle element image[(m - 1) * (N - 2) + n - 1 + offset] = window[4]; } } // Free memory delete[] extension; if (calcStats) runningAverageStdDev(); } break; case FITS_ROTATE_CW: rotFITS(90, 0); rotCounter++; break; case FITS_ROTATE_CCW: rotFITS(270, 0); rotCounter--; break; case FITS_FLIP_H: rotFITS(0, 1); flipHCounter++; break; case FITS_FLIP_V: rotFITS(0, 2); flipVCounter++; break; default: break; } } QList FITSData::getStarCentersInSubFrame(QRect subFrame) const { QList starCentersInSubFrame; for (int i = 0; i < starCenters.count(); i++) { int x = static_cast(starCenters[i]->x); int y = static_cast(starCenters[i]->y); if(subFrame.contains(x, y)) { starCentersInSubFrame.append(starCenters[i]); } } return starCentersInSubFrame; } void FITSData::getCenterSelection(int * x, int * y) { if (starCenters.count() == 0) return; auto * pEdge = new Edge(); pEdge->x = *x; pEdge->y = *y; pEdge->width = 1; foreach (Edge * center, starCenters) if (checkCollision(pEdge, center)) { *x = static_cast(center->x); *y = static_cast(center->y); break; } delete (pEdge); } bool FITSData::checkForWCS() { #ifndef KSTARS_LITE #ifdef HAVE_WCSLIB int status = 0; char * header; - int nkeyrec, nreject, nwcs; + int nkeyrec, nreject; + + // Free wcs before re-use + if (m_wcs != nullptr) + wcsvfree(&m_nwcs, &m_wcs); if (fits_hdr2str(fptr, 1, nullptr, 0, &header, &nkeyrec, &status)) { char errmsg[512]; fits_get_errstatus(status, errmsg); lastError = errmsg; return false; } - if ((status = wcspih(header, nkeyrec, WCSHDR_all, -3, &nreject, &nwcs, &wcs)) != 0) + if ((status = wcspih(header, nkeyrec, WCSHDR_all, -3, &nreject, &m_nwcs, &m_wcs)) != 0) { free(header); + wcsvfree(&m_nwcs, &m_wcs); lastError = QString("wcspih ERROR %1: %2.").arg(status).arg(wcshdr_errmsg[status]); return false; } free(header); - if (wcs == nullptr) + if (m_wcs == nullptr) { //fprintf(stderr, "No world coordinate systems found.\n"); lastError = i18n("No world coordinate systems found."); return false; } // FIXME: Call above goes through EVEN if no WCS is present, so we're adding this to return for now. - if (wcs->crpix[0] == 0) + if (m_wcs->crpix[0] == 0) { lastError = i18n("No world coordinate systems found."); return false; } - if ((status = wcsset(wcs)) != 0) + if ((status = wcsset(m_wcs)) != 0) { lastError = QString("wcsset error %1: %2.").arg(status).arg(wcs_errmsg[status]); return false; } HasWCS = true; #endif #endif return HasWCS; } bool FITSData::loadWCS() { #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB) if (WCSLoaded) { qWarning() << "WCS data already loaded"; return true; } qCDebug(KSTARS_FITS) << "Started WCS Data Processing..."; int status = 0; char * header; int nkeyrec, nreject, nwcs, stat[2]; double imgcrd[2], phi = 0, pixcrd[2], theta = 0, world[2]; int w = width(); int h = height(); if (fits_hdr2str(fptr, 1, nullptr, 0, &header, &nkeyrec, &status)) { char errmsg[512]; fits_get_errstatus(status, errmsg); lastError = errmsg; return false; } - if ((status = wcspih(header, nkeyrec, WCSHDR_all, -3, &nreject, &nwcs, &wcs)) != 0) + if ((status = wcspih(header, nkeyrec, WCSHDR_all, -3, &nreject, &nwcs, &m_wcs)) != 0) { free(header); lastError = QString("wcspih ERROR %1: %2.").arg(status).arg(wcshdr_errmsg[status]); return false; } free(header); - if (wcs == nullptr) + if (m_wcs == nullptr) { //fprintf(stderr, "No world coordinate systems found.\n"); lastError = i18n("No world coordinate systems found."); return false; } // FIXME: Call above goes through EVEN if no WCS is present, so we're adding this to return for now. - if (wcs->crpix[0] == 0) + if (m_wcs->crpix[0] == 0) { lastError = i18n("No world coordinate systems found."); return false; } - if ((status = wcsset(wcs)) != 0) + if ((status = wcsset(m_wcs)) != 0) { lastError = QString("wcsset error %1: %2.").arg(status).arg(wcs_errmsg[status]); return false; } delete[] wcs_coord; wcs_coord = new wcs_point[w * h]; if (wcs_coord == nullptr) { lastError = "Not enough memory for WCS data!"; return false; } wcs_point * p = wcs_coord; for (int i = 0; i < h; i++) { for (int j = 0; j < w; j++) { pixcrd[0] = j; pixcrd[1] = i; - if ((status = wcsp2s(wcs, 1, 2, &pixcrd[0], &imgcrd[0], &phi, &theta, &world[0], &stat[0])) != 0) + if ((status = wcsp2s(m_wcs, 1, 2, &pixcrd[0], &imgcrd[0], &phi, &theta, &world[0], &stat[0])) != 0) { lastError = QString("wcsp2s error %1: %2.").arg(status).arg(wcs_errmsg[status]); } else { p->ra = world[0]; p->dec = world[1]; p++; } } } findObjectsInImage(&world[0], phi, theta, &imgcrd[0], &pixcrd[0], &stat[0]); WCSLoaded = true; HasWCS = true; qCDebug(KSTARS_FITS) << "Finished WCS Data processing..."; return true; #else return false; #endif } bool FITSData::wcsToPixel(SkyPoint &wcsCoord, QPointF &wcsPixelPoint, QPointF &wcsImagePoint) { #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB) int status = 0; int stat[2]; double imgcrd[2], worldcrd[2], pixcrd[2], phi[2], theta[2]; - if (wcs == nullptr) + if (m_wcs == nullptr) { lastError = i18n("No world coordinate systems found."); return false; } worldcrd[0] = wcsCoord.ra0().Degrees(); worldcrd[1] = wcsCoord.dec0().Degrees(); - if ((status = wcss2p(wcs, 1, 2, &worldcrd[0], &phi[0], &theta[0], &imgcrd[0], &pixcrd[0], &stat[0])) != 0) + if ((status = wcss2p(m_wcs, 1, 2, &worldcrd[0], &phi[0], &theta[0], &imgcrd[0], &pixcrd[0], &stat[0])) != 0) { lastError = QString("wcss2p error %1: %2.").arg(status).arg(wcs_errmsg[status]); return false; } wcsImagePoint.setX(imgcrd[0]); wcsImagePoint.setY(imgcrd[1]); wcsPixelPoint.setX(pixcrd[0]); wcsPixelPoint.setY(pixcrd[1]); return true; #else Q_UNUSED(wcsCoord); Q_UNUSED(wcsPixelPoint); Q_UNUSED(wcsImagePoint); return false; #endif } bool FITSData::pixelToWCS(const QPointF &wcsPixelPoint, SkyPoint &wcsCoord) { #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB) int status = 0; int stat[2]; double imgcrd[2], phi, pixcrd[2], theta, world[2]; - if (wcs == nullptr) + if (m_wcs == nullptr) { lastError = i18n("No world coordinate systems found."); return false; } pixcrd[0] = wcsPixelPoint.x(); pixcrd[1] = wcsPixelPoint.y(); - if ((status = wcsp2s(wcs, 1, 2, &pixcrd[0], &imgcrd[0], &phi, &theta, &world[0], &stat[0])) != 0) + if ((status = wcsp2s(m_wcs, 1, 2, &pixcrd[0], &imgcrd[0], &phi, &theta, &world[0], &stat[0])) != 0) { lastError = QString("wcsp2s error %1: %2.").arg(status).arg(wcs_errmsg[status]); return false; } else { wcsCoord.setRA0(world[0] / 15.0); wcsCoord.setDec0(world[1]); } return true; #else Q_UNUSED(wcsPixelPoint); Q_UNUSED(wcsCoord); return false; #endif } #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB) void FITSData::findObjectsInImage(double world[], double phi, double theta, double imgcrd[], double pixcrd[], int stat[]) { int w = width(); int h = height(); int status = 0; char date[64]; KSNumbers * num = nullptr; if (fits_read_keyword(fptr, "DATE-OBS", date, nullptr, &status) == 0) { QString tsString(date); tsString = tsString.remove('\'').trimmed(); // Add Zulu time to indicate UTC tsString += "Z"; QDateTime ts = QDateTime::fromString(tsString, Qt::ISODate); if (ts.isValid()) num = new KSNumbers(KStarsDateTime(ts).djd()); } if (num == nullptr) num = new KSNumbers(KStarsData::Instance()->ut().djd()); //Set to current time if the above does not work. SkyMapComposite * map = KStarsData::Instance()->skyComposite(); wcs_point * wcs_coord = getWCSCoord(); if (wcs_coord != nullptr) { int size = w * h; objList.clear(); SkyPoint p1; p1.setRA0(dms(wcs_coord[0].ra)); p1.setDec0(dms(wcs_coord[0].dec)); p1.updateCoordsNow(num); SkyPoint p2; p2.setRA0(dms(wcs_coord[size - 1].ra)); p2.setDec0(dms(wcs_coord[size - 1].dec)); p2.updateCoordsNow(num); QList list = map->findObjectsInArea(p1, p2); foreach (SkyObject * object, list) { int type = object->type(); if (object->name() == "star" || type == SkyObject::PLANET || type == SkyObject::ASTEROID || type == SkyObject::COMET || type == SkyObject::SUPERNOVA || type == SkyObject::MOON || type == SkyObject::SATELLITE) { //DO NOT DISPLAY, at least for now, because these things move and change. } int x = -100; int y = -100; world[0] = object->ra0().Degrees(); world[1] = object->dec0().Degrees(); - if ((status = wcss2p(wcs, 1, 2, &world[0], &phi, &theta, &imgcrd[0], &pixcrd[0], &stat[0])) != 0) + if ((status = wcss2p(m_wcs, 1, 2, &world[0], &phi, &theta, &imgcrd[0], &pixcrd[0], &stat[0])) != 0) { fprintf(stderr, "wcsp2s ERROR %d: %s.\n", status, wcs_errmsg[status]); } else { x = pixcrd[0]; //The X and Y are set to the found position if it does work. y = pixcrd[1]; } if (x > 0 && y > 0 && x < w && y < h) objList.append(new FITSSkyObject(object, x, y)); } } delete (num); } #endif QList FITSData::getSkyObjects() { return objList; } FITSSkyObject::FITSSkyObject(SkyObject * object, int xPos, int yPos) : QObject() { skyObjectStored = object; xLoc = xPos; yLoc = yPos; } SkyObject * FITSSkyObject::skyObject() { return skyObjectStored; } int FITSSkyObject::x() { return xLoc; } int FITSSkyObject::y() { return yLoc; } void FITSSkyObject::setX(int xPos) { xLoc = xPos; } void FITSSkyObject::setY(int yPos) { yLoc = yPos; } int FITSData::getFlipVCounter() const { return flipVCounter; } void FITSData::setFlipVCounter(int value) { flipVCounter = value; } int FITSData::getFlipHCounter() const { return flipHCounter; } void FITSData::setFlipHCounter(int value) { flipHCounter = value; } int FITSData::getRotCounter() const { return rotCounter; } void FITSData::setRotCounter(int value) { rotCounter = value; } /* Rotate an image by 90, 180, or 270 degrees, with an optional * reflection across the vertical or horizontal axis. * verbose generates extra info on stdout. * return nullptr if successful or rotated image. */ template bool FITSData::rotFITS(int rotate, int mirror) { int ny, nx; int x1, y1, x2, y2; uint8_t * rotimage = nullptr; int offset = 0; if (rotate == 1) rotate = 90; else if (rotate == 2) rotate = 180; else if (rotate == 3) rotate = 270; else if (rotate < 0) rotate = rotate + 360; nx = stats.width; ny = stats.height; int BBP = stats.bytesPerPixel; /* Allocate buffer for rotated image */ rotimage = new uint8_t[stats.samples_per_channel * m_Channels * BBP]; if (rotimage == nullptr) { qWarning() << "Unable to allocate memory for rotated image buffer!"; return false; } auto * rotBuffer = reinterpret_cast(rotimage); auto * buffer = reinterpret_cast(m_ImageBuffer); /* Mirror image without rotation */ if (rotate < 45 && rotate > -45) { if (mirror == 1) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (x1 = 0; x1 < nx; x1++) { x2 = nx - x1 - 1; for (y1 = 0; y1 < ny; y1++) rotBuffer[(y1 * nx) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } else if (mirror == 2) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { y2 = ny - y1 - 1; for (x1 = 0; x1 < nx; x1++) rotBuffer[(y2 * nx) + x1 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } else { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { for (x1 = 0; x1 < nx; x1++) rotBuffer[(y1 * nx) + x1 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } } /* Rotate by 90 degrees */ else if (rotate >= 45 && rotate < 135) { if (mirror == 1) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { x2 = ny - y1 - 1; for (x1 = 0; x1 < nx; x1++) { y2 = nx - x1 - 1; rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } } else if (mirror == 2) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { for (x1 = 0; x1 < nx; x1++) rotBuffer[(x1 * ny) + y1 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } else { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { x2 = ny - y1 - 1; for (x1 = 0; x1 < nx; x1++) { y2 = x1; rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } } stats.width = ny; stats.height = nx; } /* Rotate by 180 degrees */ else if (rotate >= 135 && rotate < 225) { if (mirror == 1) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { y2 = ny - y1 - 1; for (x1 = 0; x1 < nx; x1++) rotBuffer[(y2 * nx) + x1 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } else if (mirror == 2) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (x1 = 0; x1 < nx; x1++) { x2 = nx - x1 - 1; for (y1 = 0; y1 < ny; y1++) rotBuffer[(y1 * nx) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } else { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { y2 = ny - y1 - 1; for (x1 = 0; x1 < nx; x1++) { x2 = nx - x1 - 1; rotBuffer[(y2 * nx) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } } } /* Rotate by 270 degrees */ else if (rotate >= 225 && rotate < 315) { if (mirror == 1) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { for (x1 = 0; x1 < nx; x1++) rotBuffer[(x1 * ny) + y1 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } else if (mirror == 2) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { x2 = ny - y1 - 1; for (x1 = 0; x1 < nx; x1++) { y2 = nx - x1 - 1; rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } } else { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { x2 = y1; for (x1 = 0; x1 < nx; x1++) { y2 = nx - x1 - 1; rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } } stats.width = ny; stats.height = nx; } /* If rotating by more than 315 degrees, assume top-bottom reflection */ else if (rotate >= 315 && mirror) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { for (x1 = 0; x1 < nx; x1++) { x2 = y1; y2 = x1; rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } } delete[] m_ImageBuffer; m_ImageBuffer = rotimage; return true; } void FITSData::rotWCSFITS(int angle, int mirror) { int status = 0; char comment[100]; double ctemp1, ctemp2, ctemp3, ctemp4, naxis1, naxis2; int WCS_DECIMALS = 6; naxis1 = stats.width; naxis2 = stats.height; if (fits_read_key_dbl(fptr, "CD1_1", &ctemp1, comment, &status)) { // No WCS keywords return; } /* Reset CROTAn and CD matrix if axes have been exchanged */ if (angle == 90) { if (!fits_read_key_dbl(fptr, "CROTA1", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "CROTA1", ctemp1 + 90.0, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CROTA2", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "CROTA2", ctemp1 + 90.0, WCS_DECIMALS, comment, &status); } status = 0; /* Negate rotation angle if mirrored */ if (mirror != 0) { if (!fits_read_key_dbl(fptr, "CROTA1", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "CROTA1", -ctemp1, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CROTA2", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "CROTA2", -ctemp1, WCS_DECIMALS, comment, &status); status = 0; if (!fits_read_key_dbl(fptr, "LTM1_1", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "LTM1_1", -ctemp1, WCS_DECIMALS, comment, &status); status = 0; if (!fits_read_key_dbl(fptr, "CD1_1", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "CD1_1", -ctemp1, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CD1_2", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "CD1_2", -ctemp1, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CD2_1", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "CD2_1", -ctemp1, WCS_DECIMALS, comment, &status); } status = 0; /* Unbin CRPIX and CD matrix */ if (!fits_read_key_dbl(fptr, "LTM1_1", &ctemp1, comment, &status)) { if (ctemp1 != 1.0) { if (!fits_read_key_dbl(fptr, "LTM2_2", &ctemp2, comment, &status)) if (ctemp1 == ctemp2) { double ltv1 = 0.0; double ltv2 = 0.0; status = 0; if (!fits_read_key_dbl(fptr, "LTV1", <v1, comment, &status)) fits_delete_key(fptr, "LTV1", &status); if (!fits_read_key_dbl(fptr, "LTV2", <v2, comment, &status)) fits_delete_key(fptr, "LTV2", &status); status = 0; if (!fits_read_key_dbl(fptr, "CRPIX1", &ctemp3, comment, &status)) fits_update_key_dbl(fptr, "CRPIX1", (ctemp3 - ltv1) / ctemp1, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CRPIX2", &ctemp3, comment, &status)) fits_update_key_dbl(fptr, "CRPIX2", (ctemp3 - ltv2) / ctemp1, WCS_DECIMALS, comment, &status); status = 0; if (!fits_read_key_dbl(fptr, "CD1_1", &ctemp3, comment, &status)) fits_update_key_dbl(fptr, "CD1_1", ctemp3 / ctemp1, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CD1_2", &ctemp3, comment, &status)) fits_update_key_dbl(fptr, "CD1_2", ctemp3 / ctemp1, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CD2_1", &ctemp3, comment, &status)) fits_update_key_dbl(fptr, "CD2_1", ctemp3 / ctemp1, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CD2_2", &ctemp3, comment, &status)) fits_update_key_dbl(fptr, "CD2_2", ctemp3 / ctemp1, WCS_DECIMALS, comment, &status); status = 0; fits_delete_key(fptr, "LTM1_1", &status); fits_delete_key(fptr, "LTM1_2", &status); } } } status = 0; /* Reset CRPIXn */ if (!fits_read_key_dbl(fptr, "CRPIX1", &ctemp1, comment, &status) && !fits_read_key_dbl(fptr, "CRPIX2", &ctemp2, comment, &status)) { if (mirror != 0) { if (angle == 0) fits_update_key_dbl(fptr, "CRPIX1", naxis1 - ctemp1, WCS_DECIMALS, comment, &status); else if (angle == 90) { fits_update_key_dbl(fptr, "CRPIX1", naxis2 - ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CRPIX2", naxis1 - ctemp1, WCS_DECIMALS, comment, &status); } else if (angle == 180) { fits_update_key_dbl(fptr, "CRPIX1", ctemp1, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CRPIX2", naxis2 - ctemp2, WCS_DECIMALS, comment, &status); } else if (angle == 270) { fits_update_key_dbl(fptr, "CRPIX1", ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CRPIX2", ctemp1, WCS_DECIMALS, comment, &status); } } else { if (angle == 90) { fits_update_key_dbl(fptr, "CRPIX1", naxis2 - ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CRPIX2", ctemp1, WCS_DECIMALS, comment, &status); } else if (angle == 180) { fits_update_key_dbl(fptr, "CRPIX1", naxis1 - ctemp1, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CRPIX2", naxis2 - ctemp2, WCS_DECIMALS, comment, &status); } else if (angle == 270) { fits_update_key_dbl(fptr, "CRPIX1", ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CRPIX2", naxis1 - ctemp1, WCS_DECIMALS, comment, &status); } } } status = 0; /* Reset CDELTn (degrees per pixel) */ if (!fits_read_key_dbl(fptr, "CDELT1", &ctemp1, comment, &status) && !fits_read_key_dbl(fptr, "CDELT2", &ctemp2, comment, &status)) { if (mirror != 0) { if (angle == 0) fits_update_key_dbl(fptr, "CDELT1", -ctemp1, WCS_DECIMALS, comment, &status); else if (angle == 90) { fits_update_key_dbl(fptr, "CDELT1", -ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CDELT2", -ctemp1, WCS_DECIMALS, comment, &status); } else if (angle == 180) { fits_update_key_dbl(fptr, "CDELT1", ctemp1, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CDELT2", -ctemp2, WCS_DECIMALS, comment, &status); } else if (angle == 270) { fits_update_key_dbl(fptr, "CDELT1", ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CDELT2", ctemp1, WCS_DECIMALS, comment, &status); } } else { if (angle == 90) { fits_update_key_dbl(fptr, "CDELT1", -ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CDELT2", ctemp1, WCS_DECIMALS, comment, &status); } else if (angle == 180) { fits_update_key_dbl(fptr, "CDELT1", -ctemp1, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CDELT2", -ctemp2, WCS_DECIMALS, comment, &status); } else if (angle == 270) { fits_update_key_dbl(fptr, "CDELT1", ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CDELT2", -ctemp1, WCS_DECIMALS, comment, &status); } } } /* Reset CD matrix, if present */ ctemp1 = 0.0; ctemp2 = 0.0; ctemp3 = 0.0; ctemp4 = 0.0; status = 0; if (!fits_read_key_dbl(fptr, "CD1_1", &ctemp1, comment, &status)) { fits_read_key_dbl(fptr, "CD1_2", &ctemp2, comment, &status); fits_read_key_dbl(fptr, "CD2_1", &ctemp3, comment, &status); fits_read_key_dbl(fptr, "CD2_2", &ctemp4, comment, &status); status = 0; if (mirror != 0) { if (angle == 0) { fits_update_key_dbl(fptr, "CD1_2", -ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_1", -ctemp3, WCS_DECIMALS, comment, &status); } else if (angle == 90) { fits_update_key_dbl(fptr, "CD1_1", -ctemp4, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD1_2", -ctemp3, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_1", -ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_2", -ctemp1, WCS_DECIMALS, comment, &status); } else if (angle == 180) { fits_update_key_dbl(fptr, "CD1_1", ctemp1, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD1_2", ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_1", -ctemp3, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_2", -ctemp4, WCS_DECIMALS, comment, &status); } else if (angle == 270) { fits_update_key_dbl(fptr, "CD1_1", ctemp4, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD1_2", ctemp3, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_1", ctemp3, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_2", ctemp1, WCS_DECIMALS, comment, &status); } } else { if (angle == 90) { fits_update_key_dbl(fptr, "CD1_1", -ctemp4, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD1_2", -ctemp3, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_1", ctemp1, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_2", ctemp1, WCS_DECIMALS, comment, &status); } else if (angle == 180) { fits_update_key_dbl(fptr, "CD1_1", -ctemp1, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD1_2", -ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_1", -ctemp3, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_2", -ctemp4, WCS_DECIMALS, comment, &status); } else if (angle == 270) { fits_update_key_dbl(fptr, "CD1_1", ctemp4, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD1_2", ctemp3, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_1", -ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_2", -ctemp1, WCS_DECIMALS, comment, &status); } } } /* Delete any polynomial solution */ /* (These could maybe be switched, but I don't want to work them out yet */ status = 0; if (!fits_read_key_dbl(fptr, "CO1_1", &ctemp1, comment, &status)) { int i; char keyword[16]; for (i = 1; i < 13; i++) { sprintf(keyword, "CO1_%d", i); fits_delete_key(fptr, keyword, &status); } for (i = 1; i < 13; i++) { sprintf(keyword, "CO2_%d", i); fits_delete_key(fptr, keyword, &status); } } } uint8_t * FITSData::getImageBuffer() { return m_ImageBuffer; } void FITSData::setImageBuffer(uint8_t * buffer) { delete[] m_ImageBuffer; m_ImageBuffer = buffer; } bool FITSData::checkDebayer() { int status = 0; char bayerPattern[64]; // Let's search for BAYERPAT keyword, if it's not found we return as there is no bayer pattern in this image if (fits_read_keyword(fptr, "BAYERPAT", bayerPattern, nullptr, &status)) return false; if (stats.bitpix != 16 && stats.bitpix != 8) { KSNotification::error(i18n("Only 8 and 16 bits bayered images supported."), i18n("Debayer error")); return false; } QString pattern(bayerPattern); pattern = pattern.remove('\'').trimmed(); if (pattern == "RGGB") debayerParams.filter = DC1394_COLOR_FILTER_RGGB; else if (pattern == "GBRG") debayerParams.filter = DC1394_COLOR_FILTER_GBRG; else if (pattern == "GRBG") debayerParams.filter = DC1394_COLOR_FILTER_GRBG; else if (pattern == "BGGR") debayerParams.filter = DC1394_COLOR_FILTER_BGGR; // We return unless we find a valid pattern else { KSNotification::error(i18n("Unsupported bayer pattern %1.", pattern), i18n("Debayer error")); return false; } fits_read_key(fptr, TINT, "XBAYROFF", &debayerParams.offsetX, nullptr, &status); fits_read_key(fptr, TINT, "YBAYROFF", &debayerParams.offsetY, nullptr, &status); HasDebayer = true; return true; } void FITSData::getBayerParams(BayerParams * param) { param->method = debayerParams.method; param->filter = debayerParams.filter; param->offsetX = debayerParams.offsetX; param->offsetY = debayerParams.offsetY; } void FITSData::setBayerParams(BayerParams * param) { debayerParams.method = param->method; debayerParams.filter = param->filter; debayerParams.offsetX = param->offsetX; debayerParams.offsetY = param->offsetY; } bool FITSData::debayer() { if (bayerBuffer == nullptr) { int anynull = 0, status = 0; bayerBuffer = m_ImageBuffer; if (fits_read_img(fptr, m_DataType, 1, stats.samples_per_channel, nullptr, bayerBuffer, &anynull, &status)) { char errmsg[512]; fits_get_errstatus(status, errmsg); KSNotification::error(i18n("Error reading image: %1", QString(errmsg)), i18n("Debayer error")); return false; } } switch (m_DataType) { case TBYTE: return debayer_8bit(); case TUSHORT: return debayer_16bit(); default: return false; } } bool FITSData::debayer_8bit() { dc1394error_t error_code; int rgb_size = stats.samples_per_channel * 3 * stats.bytesPerPixel; auto * destinationBuffer = new uint8_t[rgb_size]; if (destinationBuffer == nullptr) { KSNotification::error(i18n("Unable to allocate memory for temporary bayer buffer."), i18n("Debayer error")); return false; } int ds1394_height = stats.height; uint8_t * dc1394_source = bayerBuffer; if (debayerParams.offsetY == 1) { dc1394_source += stats.width; ds1394_height--; } if (debayerParams.offsetX == 1) { dc1394_source++; } error_code = dc1394_bayer_decoding_8bit(dc1394_source, destinationBuffer, stats.width, ds1394_height, debayerParams.filter, debayerParams.method); if (error_code != DC1394_SUCCESS) { KSNotification::error(i18n("Debayer failed (%1)", error_code), i18n("Debayer error")); m_Channels = 1; delete[] destinationBuffer; return false; } if (m_Channels == 1) { delete[] m_ImageBuffer; m_ImageBuffer = new uint8_t[rgb_size]; if (m_ImageBuffer == nullptr) { delete[] destinationBuffer; KSNotification::error(i18n("Unable to allocate memory for temporary bayer buffer."), i18n("Debayer error")); return false; } } // Data in R1G1B1, we need to copy them into 3 layers for FITS uint8_t * rBuff = m_ImageBuffer; uint8_t * gBuff = m_ImageBuffer + (stats.width * stats.height); uint8_t * bBuff = m_ImageBuffer + (stats.width * stats.height * 2); int imax = stats.samples_per_channel * 3 - 3; for (int i = 0; i <= imax; i += 3) { *rBuff++ = destinationBuffer[i]; *gBuff++ = destinationBuffer[i + 1]; *bBuff++ = destinationBuffer[i + 2]; } m_Channels = (m_Mode == FITS_NORMAL) ? 3 : 1; delete[] destinationBuffer; bayerBuffer = nullptr; return true; } bool FITSData::debayer_16bit() { dc1394error_t error_code; int rgb_size = stats.samples_per_channel * 3 * stats.bytesPerPixel; auto * destinationBuffer = new uint8_t[rgb_size]; auto * buffer = reinterpret_cast(bayerBuffer); auto * dstBuffer = reinterpret_cast(destinationBuffer); if (destinationBuffer == nullptr) { KSNotification::error(i18n("Unable to allocate memory for temporary bayer buffer."), i18n("Debayer error")); return false; } int ds1394_height = stats.height; uint16_t * dc1394_source = buffer; if (debayerParams.offsetY == 1) { dc1394_source += stats.width; ds1394_height--; } if (debayerParams.offsetX == 1) { dc1394_source++; } error_code = dc1394_bayer_decoding_16bit(dc1394_source, dstBuffer, stats.width, ds1394_height, debayerParams.filter, debayerParams.method, 16); if (error_code != DC1394_SUCCESS) { KSNotification::error(i18n("Debayer failed (%1)", error_code), i18n("Debayer error")); m_Channels = 1; delete[] destinationBuffer; return false; } if (m_Channels == 1) { delete[] m_ImageBuffer; m_ImageBuffer = new uint8_t[rgb_size]; if (m_ImageBuffer == nullptr) { delete[] destinationBuffer; KSNotification::error(i18n("Unable to allocate memory for temporary bayer buffer."), i18n("Debayer error")); return false; } } buffer = reinterpret_cast(m_ImageBuffer); // Data in R1G1B1, we need to copy them into 3 layers for FITS uint16_t * rBuff = buffer; uint16_t * gBuff = buffer + (stats.width * stats.height); uint16_t * bBuff = buffer + (stats.width * stats.height * 2); int imax = stats.samples_per_channel * 3 - 3; for (int i = 0; i <= imax; i += 3) { *rBuff++ = dstBuffer[i]; *gBuff++ = dstBuffer[i + 1]; *bBuff++ = dstBuffer[i + 2]; } m_Channels = (m_Mode == FITS_NORMAL) ? 3 : 1; delete[] destinationBuffer; bayerBuffer = nullptr; return true; } double FITSData::getADU() const { double adu = 0; for (int i = 0; i < m_Channels; i++) adu += stats.mean[i]; return (adu / static_cast(m_Channels)); } /* CannyDetector, Implementation of Canny edge detector in Qt/C++. * Copyright (C) 2015 Gonzalo Exequiel Pedone * * 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 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * Email : hipersayan DOT x AT gmail DOT com * Web-Site: https://github.com/hipersayanX/CannyDetector */ template void FITSData::sobel(QVector &gradient, QVector &direction) { //int size = image.width() * image.height(); gradient.resize(stats.samples_per_channel); direction.resize(stats.samples_per_channel); for (int y = 0; y < stats.height; y++) { size_t yOffset = y * stats.width; const T * grayLine = reinterpret_cast(m_ImageBuffer) + yOffset; const T * grayLine_m1 = y < 1 ? grayLine : grayLine - stats.width; const T * grayLine_p1 = y >= stats.height - 1 ? grayLine : grayLine + stats.width; float * gradientLine = gradient.data() + yOffset; float * directionLine = direction.data() + yOffset; for (int x = 0; x < stats.width; x++) { int x_m1 = x < 1 ? x : x - 1; int x_p1 = x >= stats.width - 1 ? x : x + 1; int gradX = grayLine_m1[x_p1] + 2 * grayLine[x_p1] + grayLine_p1[x_p1] - grayLine_m1[x_m1] - 2 * grayLine[x_m1] - grayLine_p1[x_m1]; int gradY = grayLine_m1[x_m1] + 2 * grayLine_m1[x] + grayLine_m1[x_p1] - grayLine_p1[x_m1] - 2 * grayLine_p1[x] - grayLine_p1[x_p1]; gradientLine[x] = qAbs(gradX) + qAbs(gradY); /* Gradient directions are classified in 4 possible cases * * dir 0 * * x x x * - - - * x x x * * dir 1 * * x x / * x / x * / x x * * dir 2 * * \ x x * x \ x * x x \ * * dir 3 * * x | x * x | x * x | x */ if (gradX == 0 && gradY == 0) directionLine[x] = 0; else if (gradX == 0) directionLine[x] = 3; else { qreal a = 180. * atan(qreal(gradY) / gradX) / M_PI; if (a >= -22.5 && a < 22.5) directionLine[x] = 0; else if (a >= 22.5 && a < 67.5) directionLine[x] = 2; else if (a >= -67.5 && a < -22.5) directionLine[x] = 1; else directionLine[x] = 3; } } } } int FITSData::partition(int width, int height, QVector &gradient, QVector &ids) { int id = 0; for (int y = 1; y < height - 1; y++) { for (int x = 1; x < width - 1; x++) { int index = x + y * width; float val = gradient[index]; if (val > 0 && ids[index] == 0) { trace(width, height, ++id, gradient, ids, x, y); } } } // Return max id return id; } void FITSData::trace(int width, int height, int id, QVector &image, QVector &ids, int x, int y) { int yOffset = y * width; float * cannyLine = image.data() + yOffset; int * idLine = ids.data() + yOffset; if (idLine[x] != 0) return; idLine[x] = id; for (int j = -1; j < 2; j++) { int nextY = y + j; if (nextY < 0 || nextY >= height) continue; float * cannyLineNext = cannyLine + j * width; for (int i = -1; i < 2; i++) { int nextX = x + i; if (i == j || nextX < 0 || nextX >= width) continue; if (cannyLineNext[nextX] > 0) { // Trace neighbors. trace(width, height, id, image, ids, nextX, nextY); } } } } QString FITSData::getLastError() const { return lastError; } bool FITSData::getAutoRemoveTemporaryFITS() const { return autoRemoveTemporaryFITS; } void FITSData::setAutoRemoveTemporaryFITS(bool value) { autoRemoveTemporaryFITS = value; } template void FITSData::convertToQImage(double dataMin, double dataMax, double scale, double zero, QImage &image) { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wcast-align" auto * buffer = (T *)getImageBuffer(); #pragma GCC diagnostic pop const T limit = std::numeric_limits::max(); T bMin = dataMin < 0 ? 0 : dataMin; T bMax = dataMax > limit ? limit : dataMax; uint16_t w = width(); uint16_t h = height(); uint32_t size = w * h; double val; if (channels() == 1) { /* Fill in pixel values using indexed map, linear scale */ for (int j = 0; j < h; j++) { unsigned char * scanLine = image.scanLine(j); for (int i = 0; i < w; i++) { val = qBound(bMin, buffer[j * w + i], bMax); val = val * scale + zero; scanLine[i] = qBound(0, (unsigned char)val, 255); } } } else { double rval = 0, gval = 0, bval = 0; QRgb value; /* Fill in pixel values using indexed map, linear scale */ for (int j = 0; j < h; j++) { auto * scanLine = reinterpret_cast((image.scanLine(j))); for (int i = 0; i < w; i++) { rval = qBound(bMin, buffer[j * w + i], bMax); gval = qBound(bMin, buffer[j * w + i + size], bMax); bval = qBound(bMin, buffer[j * w + i + size * 2], bMax); value = qRgb(rval * scale + zero, gval * scale + zero, bval * scale + zero); scanLine[i] = value; } } } } QImage FITSData::FITSToImage(const QString &filename) { QImage fitsImage; double min, max; FITSData data; QFuture future = data.loadFITS(filename); // Wait synchronously future.waitForFinished(); if (future.result() == false) return fitsImage; data.getMinMax(&min, &max); if (min == max) { fitsImage.fill(Qt::white); return fitsImage; } if (data.channels() == 1) { fitsImage = QImage(data.width(), data.height(), QImage::Format_Indexed8); fitsImage.setColorCount(256); for (int i = 0; i < 256; i++) fitsImage.setColor(i, qRgb(i, i, i)); } else { fitsImage = QImage(data.width(), data.height(), QImage::Format_RGB32); } double dataMin = data.stats.mean[0] - data.stats.stddev[0]; double dataMax = data.stats.mean[0] + data.stats.stddev[0] * 3; double bscale = 255. / (dataMax - dataMin); double bzero = (-dataMin) * (255. / (dataMax - dataMin)); // Long way to do this since we do not want to use templated functions here switch (data.property("dataType").toInt()) { case TBYTE: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; case TSHORT: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; case TUSHORT: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; case TLONG: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; case TULONG: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; case TFLOAT: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; case TLONGLONG: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; case TDOUBLE: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; default: break; } return fitsImage; } bool FITSData::ImageToFITS(const QString &filename, const QString &format, QString &output) { if (QImageReader::supportedImageFormats().contains(format.toLatin1()) == false) { qCCritical(KSTARS_FITS) << "Failed to convert" << filename << "to FITS since format" << format << "is not supported in Qt"; return false; } QImage input; if (input.load(filename, format.toLatin1()) == false) { qCCritical(KSTARS_FITS) << "Failed to open image" << filename; return false; } output = QString(KSPaths::writableLocation(QStandardPaths::TempLocation) + QFileInfo(filename).fileName() + ".fits"); //This section sets up the FITS File fitsfile *fptr = nullptr; int status = 0; long fpixel = 1, naxis = input.allGray() ? 2 : 3, nelements, exposure; long naxes[3] = { input.width(), input.height(), naxis == 3 ? 3 : 1 }; char error_status[512] = {0}; if (fits_create_file(&fptr, QString('!' + output).toLatin1().data(), &status)) { qCCritical(KSTARS_FITS) << "Failed to create FITS file. Error:" << status; return false; } if (fits_create_img(fptr, BYTE_IMG, naxis, naxes, &status)) { qCWarning(KSTARS_FITS) << "fits_create_img failed:" << error_status; status = 0; fits_close_file(fptr, &status); return false; } exposure = 1; fits_update_key(fptr, TLONG, "EXPOSURE", &exposure, "Total Exposure Time", &status); // Gray image if (naxis == 2) { nelements = naxes[0] * naxes[1]; if (fits_write_img(fptr, TBYTE, fpixel, nelements, input.bits(), &status)) { fits_get_errstatus(status, error_status); qCWarning(KSTARS_FITS) << "fits_write_img GRAY failed:" << error_status; status = 0; fits_close_file(fptr, &status); return false; } } // RGB image, we have to convert from ARGB format to R G B for each plane else { nelements = naxes[0] * naxes[1] * 3; uint8_t *srcBuffer = input.bits(); // ARGB uint32_t srcBytes = naxes[0] * naxes[1] * 4 - 4; uint8_t *rgbBuffer = new uint8_t[nelements]; if (rgbBuffer == nullptr) { qCWarning(KSTARS_FITS) << "Not enough memory for RGB buffer"; fits_close_file(fptr, &status); return false; } uint8_t *subR = rgbBuffer; uint8_t *subG = rgbBuffer + naxes[0] * naxes[1]; uint8_t *subB = rgbBuffer + naxes[0] * naxes[1] * 2; for (uint32_t i = 0; i < srcBytes; i += 4) { *subB++ = srcBuffer[i]; *subG++ = srcBuffer[i + 1]; *subR++ = srcBuffer[i + 2]; } if (fits_write_img(fptr, TBYTE, fpixel, nelements, rgbBuffer, &status)) { fits_get_errstatus(status, error_status); qCWarning(KSTARS_FITS) << "fits_write_img RGB failed:" << error_status; status = 0; fits_close_file(fptr, &status); delete [] rgbBuffer; return false; } delete [] rgbBuffer; } if (fits_flush_file(fptr, &status)) { fits_get_errstatus(status, error_status); qCWarning(KSTARS_FITS) << "fits_flush_file failed:" << error_status; status = 0; fits_close_file(fptr, &status); return false; } if (fits_close_file(fptr, &status)) { fits_get_errstatus(status, error_status); qCWarning(KSTARS_FITS) << "fits_close_file failed:" << error_status; return false; } return true; } #if 0 bool FITSData::injectWCS(const QString &newWCSFile, double orientation, double ra, double dec, double pixscale) { int status = 0, exttype = 0; long nelements; fitsfile * new_fptr; char errMsg[512]; qCInfo(KSTARS_FITS) << "Creating new WCS file:" << newWCSFile << "with parameters Orientation:" << orientation << "RA:" << ra << "DE:" << dec << "Pixel Scale:" << pixscale; nelements = stats.samples_per_channel * m_Channels; /* Create a new File, overwriting existing*/ if (fits_create_file(&new_fptr, QString('!' + newWCSFile).toLatin1(), &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } if (fits_movabs_hdu(fptr, 1, &exttype, &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } if (fits_copy_file(fptr, new_fptr, 1, 1, 1, &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } /* close current file */ if (fits_close_file(fptr, &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } status = 0; if (m_isTemporary && autoRemoveTemporaryFITS) { QFile::remove(m_Filename); m_isTemporary = false; qCDebug(KSTARS_FITS) << "Removing FITS File: " << m_Filename; } m_Filename = newWCSFile; m_isTemporary = true; fptr = new_fptr; if (fits_movabs_hdu(fptr, 1, &exttype, &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } /* Write Data */ if (fits_write_img(fptr, m_DataType, 1, nelements, m_ImageBuffer, &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } /* Write keywords */ // Minimum if (fits_update_key(fptr, TDOUBLE, "DATAMIN", &(stats.min), "Minimum value", &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } // Maximum if (fits_update_key(fptr, TDOUBLE, "DATAMAX", &(stats.max), "Maximum value", &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } // NAXIS1 if (fits_update_key(fptr, TUSHORT, "NAXIS1", &(stats.width), "length of data axis 1", &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } // NAXIS2 if (fits_update_key(fptr, TUSHORT, "NAXIS2", &(stats.height), "length of data axis 2", &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } fits_update_key(fptr, TDOUBLE, "OBJCTRA", &ra, "Object RA", &status); fits_update_key(fptr, TDOUBLE, "OBJCTDEC", &dec, "Object DEC", &status); int epoch = 2000; fits_update_key(fptr, TINT, "EQUINOX", &epoch, "Equinox", &status); fits_update_key(fptr, TDOUBLE, "CRVAL1", &ra, "CRVAL1", &status); fits_update_key(fptr, TDOUBLE, "CRVAL2", &dec, "CRVAL1", &status); char radecsys[8] = "FK5"; char ctype1[16] = "RA---TAN"; char ctype2[16] = "DEC--TAN"; fits_update_key(fptr, TSTRING, "RADECSYS", radecsys, "RADECSYS", &status); fits_update_key(fptr, TSTRING, "CTYPE1", ctype1, "CTYPE1", &status); fits_update_key(fptr, TSTRING, "CTYPE2", ctype2, "CTYPE2", &status); double crpix1 = width() / 2.0; double crpix2 = height() / 2.0; fits_update_key(fptr, TDOUBLE, "CRPIX1", &crpix1, "CRPIX1", &status); fits_update_key(fptr, TDOUBLE, "CRPIX2", &crpix2, "CRPIX2", &status); // Arcsecs per Pixel double secpix1 = pixscale; double secpix2 = pixscale; fits_update_key(fptr, TDOUBLE, "SECPIX1", &secpix1, "SECPIX1", &status); fits_update_key(fptr, TDOUBLE, "SECPIX2", &secpix2, "SECPIX2", &status); double degpix1 = secpix1 / 3600.0; double degpix2 = secpix2 / 3600.0; fits_update_key(fptr, TDOUBLE, "CDELT1", °pix1, "CDELT1", &status); fits_update_key(fptr, TDOUBLE, "CDELT2", °pix2, "CDELT2", &status); // Rotation is CW, we need to convert it to CCW per CROTA1 definition double rotation = 360 - orientation; if (rotation > 360) rotation -= 360; fits_update_key(fptr, TDOUBLE, "CROTA1", &rotation, "CROTA1", &status); fits_update_key(fptr, TDOUBLE, "CROTA2", &rotation, "CROTA2", &status); // ISO Date if (fits_write_date(fptr, &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } QString history = QString("Modified by KStars on %1").arg(QDateTime::currentDateTime().toString("yyyy-MM-ddThh:mm:ss")); // History if (fits_write_history(fptr, history.toLatin1(), &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } fits_flush_file(fptr, &status); WCSLoaded = false; qCDebug(KSTARS_FITS) << "Finished creating WCS file: " << newWCSFile; return true; } #endif bool FITSData::injectWCS(double orientation, double ra, double dec, double pixscale) { int status = 0; fits_update_key(fptr, TDOUBLE, "OBJCTRA", &ra, "Object RA", &status); fits_update_key(fptr, TDOUBLE, "OBJCTDEC", &dec, "Object DEC", &status); int epoch = 2000; fits_update_key(fptr, TINT, "EQUINOX", &epoch, "Equinox", &status); fits_update_key(fptr, TDOUBLE, "CRVAL1", &ra, "CRVAL1", &status); fits_update_key(fptr, TDOUBLE, "CRVAL2", &dec, "CRVAL1", &status); char radecsys[8] = "FK5"; char ctype1[16] = "RA---TAN"; char ctype2[16] = "DEC--TAN"; fits_update_key(fptr, TSTRING, "RADECSYS", radecsys, "RADECSYS", &status); fits_update_key(fptr, TSTRING, "CTYPE1", ctype1, "CTYPE1", &status); fits_update_key(fptr, TSTRING, "CTYPE2", ctype2, "CTYPE2", &status); double crpix1 = width() / 2.0; double crpix2 = height() / 2.0; fits_update_key(fptr, TDOUBLE, "CRPIX1", &crpix1, "CRPIX1", &status); fits_update_key(fptr, TDOUBLE, "CRPIX2", &crpix2, "CRPIX2", &status); // Arcsecs per Pixel double secpix1 = pixscale; double secpix2 = pixscale; fits_update_key(fptr, TDOUBLE, "SECPIX1", &secpix1, "SECPIX1", &status); fits_update_key(fptr, TDOUBLE, "SECPIX2", &secpix2, "SECPIX2", &status); double degpix1 = secpix1 / 3600.0; double degpix2 = secpix2 / 3600.0; fits_update_key(fptr, TDOUBLE, "CDELT1", °pix1, "CDELT1", &status); fits_update_key(fptr, TDOUBLE, "CDELT2", °pix2, "CDELT2", &status); // Rotation is CW, we need to convert it to CCW per CROTA1 definition double rotation = 360 - orientation; if (rotation > 360) rotation -= 360; fits_update_key(fptr, TDOUBLE, "CROTA1", &rotation, "CROTA1", &status); fits_update_key(fptr, TDOUBLE, "CROTA2", &rotation, "CROTA2", &status); WCSLoaded = false; qCDebug(KSTARS_FITS) << "Finished update WCS info."; return true; } bool FITSData::contains(const QPointF &point) const { return (point.x() >= 0 && point.y() >= 0 && point.x() <= stats.width && point.y() <= stats.height); } int FITSData::findSEPStars(const QRect &boundary) { int x = 0, y = 0, w = stats.width, h = stats.height, maxRadius = 50; if (!boundary.isNull()) { x = boundary.x(); y = boundary.y(); w = boundary.width(); h = boundary.height(); maxRadius = w; } auto * data = new float[w * h]; switch (stats.bitpix) { case BYTE_IMG: getFloatBuffer(data, x, y, w, h); break; case SHORT_IMG: getFloatBuffer(data, x, y, w, h); break; case USHORT_IMG: getFloatBuffer(data, x, y, w, h); break; case LONG_IMG: getFloatBuffer(data, x, y, w, h); break; case ULONG_IMG: getFloatBuffer(data, x, y, w, h); break; case FLOAT_IMG: delete [] data; data = reinterpret_cast(m_ImageBuffer); break; case LONGLONG_IMG: getFloatBuffer(data, x, y, w, h); break; case DOUBLE_IMG: getFloatBuffer(data, x, y, w, h); break; default: delete [] data; return -1; } float * imback = nullptr; double * flux = nullptr, *fluxerr = nullptr, *area = nullptr; short * flag = nullptr; short flux_flag = 0; int status = 0; sep_bkg * bkg = nullptr; sep_catalog * catalog = nullptr; float conv[] = {1, 2, 1, 2, 4, 2, 1, 2, 1}; double flux_fractions[2] = {0}; double requested_frac[2] = { 0.5, 0.99 }; QList edges; // #0 Create SEP Image structure sep_image im = {data, nullptr, nullptr, SEP_TFLOAT, 0, 0, w, h, 0.0, SEP_NOISE_NONE, 1.0, 0.0}; // #1 Background estimate status = sep_background(&im, 64, 64, 3, 3, 0.0, &bkg); if (status != 0) goto exit; // #2 Background evaluation imback = (float *)malloc((w * h) * sizeof(float)); status = sep_bkg_array(bkg, imback, SEP_TFLOAT); if (status != 0) goto exit; // #3 Background subtraction status = sep_bkg_subarray(bkg, im.data, im.dtype); if (status != 0) goto exit; // #4 Source Extraction // Note that we set deblend_cont = 1.0 to turn off deblending. status = sep_extract(&im, 2 * bkg->globalrms, SEP_THRESH_ABS, 10, conv, 3, 3, SEP_FILTER_CONV, 32, 1.0, 1, 1.0, &catalog); if (status != 0) goto exit; // TODO // Must detect edge detection // Must limit to brightest 100 (by flux) centers // Should probably use ellipse to draw instead of simple circle? // Useful for galaxies and also elenogated stars. for (int i = 0; i < catalog->nobj; i++) { double flux = catalog->flux[i]; // Get HFR sep_flux_radius(&im, catalog->x[i], catalog->y[i], maxRadius, 5, 0, &flux, requested_frac, 2, flux_fractions, &flux_flag); auto * center = new Edge(); center->x = catalog->x[i] + x + 0.5; center->y = catalog->y[i] + y + 0.5; center->val = catalog->peak[i]; center->sum = flux; center->HFR = center->width = flux_fractions[0]; if (flux_fractions[1] < maxRadius) center->width = flux_fractions[1] * 2; edges.append(center); } // Let's sort edges, starting with widest std::sort(edges.begin(), edges.end(), [](const Edge * edge1, const Edge * edge2) -> bool { return edge1->width > edge2->width;}); // Take only the first 100 stars { int starCount = qMin(100, edges.count()); for (int i = 0; i < starCount; i++) starCenters.append(edges[i]); } edges.clear(); qCDebug(KSTARS_FITS) << qSetFieldWidth(10) << "#" << "#X" << "#Y" << "#Flux" << "#Width" << "#HFR"; for (int i = 0; i < starCenters.count(); i++) qCDebug(KSTARS_FITS) << qSetFieldWidth(10) << i << starCenters[i]->x << starCenters[i]->y << starCenters[i]->sum << starCenters[i]->width << starCenters[i]->HFR; exit: if (stats.bitpix != FLOAT_IMG) delete [] data; sep_bkg_free(bkg); sep_catalog_free(catalog); free(imback); free(flux); free(fluxerr); free(area); free(flag); if (status != 0) { char errorMessage[512]; sep_get_errmsg(status, errorMessage); qCritical(KSTARS_FITS) << errorMessage; return -1; } return starCenters.count(); } template void FITSData::getFloatBuffer(float * buffer, int x, int y, int w, int h) { auto * rawBuffer = reinterpret_cast(m_ImageBuffer); float * floatPtr = buffer; int x2 = x + w; int y2 = y + h; for (int y1 = y; y1 < y2; y1++) { int offset = y1 * stats.width; for (int x1 = x; x1 < x2; x1++) { *floatPtr++ = rawBuffer[offset + x1]; } } } void FITSData::saveStatistics(Statistic &other) { other = stats; } void FITSData::restoreStatistics(Statistic &other) { stats = other; } diff --git a/kstars/fitsviewer/fitsdata.h b/kstars/fitsviewer/fitsdata.h index 49093fa21..f7beaedbd 100644 --- a/kstars/fitsviewer/fitsdata.h +++ b/kstars/fitsviewer/fitsdata.h @@ -1,566 +1,567 @@ /*************************************************************************** fitsimage.cpp - FITS Image ------------------- begin : Tue Feb 24 2004 copyright : (C) 2004 by Jasem Mutlaq email : mutlaqja@ikarustech.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. * * * * Some code fragments were adapted from Peter Kirchgessner's FITS plugin* * See http://members.aol.com/pkirchg for more details. * ***************************************************************************/ #pragma once #include "config-kstars.h" #include "bayer.h" #include "fitscommon.h" #ifdef WIN32 // This header must be included before fitsio.h to avoid compiler errors with Visual Studio #include #endif #include #include #include #include #include #ifndef KSTARS_LITE #include #ifdef HAVE_WCSLIB #include #endif #endif #define MINIMUM_PIXEL_RANGE 5 #define MINIMUM_STDVAR 5 class QProgressDialog; class SkyObject; class SkyPoint; class FITSHistogram; typedef struct { float ra; float dec; } wcs_point; class Edge { public: float x; float y; int val; int scanned; float width; float HFR; float sum; }; class FITSSkyObject : public QObject { Q_OBJECT public: explicit FITSSkyObject(SkyObject *object, int xPos, int yPos); SkyObject *skyObject(); int x(); int y(); void setX(int xPos); void setY(int yPos); private: SkyObject *skyObjectStored; int xLoc; int yLoc; }; class FITSData : public QObject { Q_OBJECT // Name of FITS file Q_PROPERTY(QString filename READ filename) // Size of file in bytes Q_PROPERTY(qint64 size READ size) // Width in pixels Q_PROPERTY(quint16 width READ width) // Height in pixels Q_PROPERTY(quint16 height READ height) // FITS MODE --> Normal, Focus, Guide..etc Q_PROPERTY(FITSMode mode MEMBER m_Mode) // 1 channel (grayscale) or 3 channels (RGB) Q_PROPERTY(quint8 channels READ channels) // Data type (BYTE, SHORT, INT..etc) Q_PROPERTY(quint32 dataType MEMBER m_DataType) // Bits per pixel Q_PROPERTY(quint8 bpp READ bpp WRITE setBPP) // Does FITS have WSC header? Q_PROPERTY(bool hasWCS READ hasWCS) // Does FITS have bayer data? Q_PROPERTY(bool hasDebyaer READ hasDebayer) public: explicit FITSData(FITSMode fitsMode = FITS_NORMAL); explicit FITSData(const FITSData *other); ~FITSData(); /** Structure to hold FITS Header records */ typedef struct { QString key; /** FITS Header Key */ QVariant value; /** FITS Header Value */ QString comment; /** FITS Header Comment, if any */ } Record; /// Stats struct to hold statisical data about the FITS data typedef struct { double min[3] = {0}, max[3] = {0}; double mean[3] = {0}; double stddev[3] = {0}; double median[3] = {0}; double SNR { 0 }; int bitpix { 8 }; int bytesPerPixel { 1 }; int ndim { 2 }; int64_t size { 0 }; uint32_t samples_per_channel { 0 }; uint16_t width { 0 }; uint16_t height { 0 }; } Statistic; /** * @brief loadFITS Loading FITS file asynchronously. * @param inFilename Path to FITS file (or compressed fits.gz) * @param silent If set, error messages are ignored. If set to false, the error message will get displayed in a popup. * @return A QFuture that can be watched until the async operation is complete. */ QFuture loadFITS(const QString &inFilename, bool silent = true); /** * @brief loadFITSFromMemory Loading FITS from memory buffer. * @param inFilename Potential future path to FITS file (or compressed fits.gz), stored in a fitsdata class variable * @param fits_buffer The memory buffer containing the fits data. * @param fits_buffer_size The size in bytes of the buffer. * @param silent If set, error messages are ignored. If set to false, the error message will get displayed in a popup. * @return bool indicating success or failure. */ bool loadFITSFromMemory(const QString &inFilename, void *fits_buffer, size_t fits_buffer_size, bool silent); /* Save FITS */ int saveFITS(const QString &newFilename); /* Rescale image lineary from image_buffer, fit to window if desired */ int rescale(FITSZoom type); /* Calculate stats */ void calculateStats(bool refresh = false); /* Check if a particular point exists within the image */ bool contains(const QPointF &point) const; // Access functions void clearImageBuffers(); void setImageBuffer(uint8_t *buffer); uint8_t *getImageBuffer(); // Statistics void saveStatistics(Statistic &other); void restoreStatistics(Statistic &other); uint16_t width() const { return stats.width; } uint16_t height() const { return stats.height; } int64_t size() const { return stats.size; } int channels() const { return m_Channels; } double getMin(uint8_t channel = 0) const { return stats.min[channel]; } double getMax(uint8_t channel = 0) const { return stats.max[channel]; } void setMinMax(double newMin, double newMax, uint8_t channel = 0); void getMinMax(double *min, double *max, uint8_t channel = 0) const { *min = stats.min[channel]; *max = stats.max[channel]; } void setStdDev(double value, uint8_t channel = 0) { stats.stddev[channel] = value; } double getStdDev(uint8_t channel = 0) const { return stats.stddev[channel]; } void setMean(double value, uint8_t channel = 0) { stats.mean[channel] = value; } double getMean(uint8_t channel = 0) const { return stats.mean[channel]; } void setMedian(double val, uint8_t channel = 0) { stats.median[channel] = val; } double getMedian(uint8_t channel = 0) const { return stats.median[channel]; } int getBytesPerPixel() const { return stats.bytesPerPixel; } void setSNR(double val) { stats.SNR = val; } double getSNR() const { return stats.SNR; } void setBPP(uint8_t value) { stats.bitpix = value; } uint32_t bpp() const { return stats.bitpix; } double getADU() const; // FITS Record bool getRecordValue(const QString &key, QVariant &value) const; const QList &getRecords() const { return records; } // Star Detection - Native KStars implementation void setStarAlgorithm(StarAlgorithm algorithm) { starAlgorithm = algorithm; } int getDetectedStars() const { return starCenters.count(); } bool areStarsSearched() const { return starsSearched; } void appendStar(Edge *newCenter) { starCenters.append(newCenter); } QList getStarCenters() const { return starCenters; } QList getStarCentersInSubFrame(QRect subFrame) const; int findStars(StarAlgorithm algorithm = ALGORITHM_CENTROID, const QRect &trackingBox = QRect()); void getCenterSelection(int *x, int *y); int findOneStar(const QRect &boundary); // Star Detection - Partially customized Canny edge detection algorithm static int findCannyStar(FITSData *data, const QRect &boundary = QRect()); template static int findCannyStar(FITSData *data, const QRect &boundary); // Use SEP (Sextractor Library) to find stars template void getFloatBuffer(float *buffer, int x, int y, int w, int h); int findSEPStars(const QRect &boundary = QRect()); // Apply ring filter to searched stars int filterStars(const float innerRadius, const float outerRadius); // Half Flux Radius Edge *getMaxHFRStar() const { return maxHFRStar; } double getHFR(HFRType type = HFR_AVERAGE); double getHFR(int x, int y); // WCS // Check if image has valid WCS header information and set HasWCS accordingly. Call in loadFITS() bool checkForWCS(); // Does image have valid WCS? bool hasWCS() { return HasWCS; } // Load WCS data bool loadWCS(); // Is WCS Image loaded? bool isWCSLoaded() { return WCSLoaded; } wcs_point *getWCSCoord() { return wcs_coord; } /** * @brief wcsToPixel Given J2000 (RA0,DE0) coordinates. Find in the image the corresponding pixel coordinates. * @param wcsCoord Coordinates of target * @param wcsPixelPoint Return XY FITS coordinates * @param wcsImagePoint Return XY Image coordinates * @return True if conversion is successful, false otherwise. */ bool wcsToPixel(SkyPoint &wcsCoord, QPointF &wcsPixelPoint, QPointF &wcsImagePoint); /** * @brief pixelToWCS Convert Pixel coordinates to J2000 world coordinates * @param wcsPixelPoint Pixel coordinates in XY Image space. * @param wcsCoord Store back WCS world coordinate in wcsCoord * @return True if successful, false otherwise. */ bool pixelToWCS(const QPointF &wcsPixelPoint, SkyPoint &wcsCoord); /** * @brief injectWCS Add WCS keywords to file * @param orientation Solver orientation, degrees E of N. * @param ra J2000 Right Ascension * @param dec J2000 Declination * @param pixscale Pixel scale in arcsecs per pixel * @return True if file is successfully updated with WCS info. */ bool injectWCS(double orientation, double ra, double dec, double pixscale); // Debayer bool hasDebayer() { return HasDebayer; } bool debayer(); bool debayer_8bit(); bool debayer_16bit(); void getBayerParams(BayerParams *param); void setBayerParams(BayerParams *param); // Histogram #ifndef KSTARS_LITE void setHistogram(FITSHistogram *inHistogram) { histogram = inHistogram; } #endif // Filter void applyFilter(FITSScale type, uint8_t *image = nullptr, QVector *targetMin = nullptr, QVector *targetMax = nullptr); // Rotation counter. We keep count to rotate WCS keywords on save int getRotCounter() const; void setRotCounter(int value); // Filename const QString &filename() const { return m_Filename; } const QString &compressedFilename() const { return m_compressedFilename; } bool isTempFile() const { return m_isTemporary; } bool isCompressed() const { return m_isCompressed; } // Horizontal flip counter. We keep count to rotate WCS keywords on save int getFlipHCounter() const; void setFlipHCounter(int value); // Horizontal flip counter. We keep count to rotate WCS keywords on save int getFlipVCounter() const; void setFlipVCounter(int value); #ifndef KSTARS_LITE #ifdef HAVE_WCSLIB void findObjectsInImage(double world[], double phi, double theta, double imgcrd[], double pixcrd[], int stat[]); #endif #endif QList getSkyObjects(); QList objList; //Does this need to be public?? // Create autostretch image from FITS File static QImage FITSToImage(const QString &filename); /** * @brief ImageToFITS Convert an image file with supported format to a FITS file. * @param filename full path to filename without extension * @param format file extension. Supported formats are whatever supported by Qt (e.g. PNG, JPG,..etc) * @param output Output temporary file path. The created file is generated by the function and store in output. * @return True if conversion is successful, false otherwise. */ static bool ImageToFITS(const QString &filename, const QString &format, QString &output); bool getAutoRemoveTemporaryFITS() const; void setAutoRemoveTemporaryFITS(bool value); QString getLastError() const; signals: void converted(QImage); private: void loadCommon(const QString &inFilename); bool privateLoad(void *fits_buffer, size_t fits_buffer_size, bool silent); void rotWCSFITS(int angle, int mirror); bool checkCollision(Edge *s1, Edge *s2); int calculateMinMax(bool refresh = false); bool checkDebayer(); void readWCSKeys(); // FITS Record bool parseHeader(); //int getFITSRecord(QString &recordList, int &nkeys); // Templated functions template bool debayer(); template bool rotFITS(int rotate, int mirror); // Apply Filter template void applyFilter(FITSScale type, uint8_t *targetImage, QVector * min = nullptr, QVector * max = nullptr); // Star Detect - Centroid template int findCentroid(const QRect &boundary, int initStdDev, int minEdgeWidth); int findCentroid(const QRect &boundary = QRect(), int initStdDev = MINIMUM_STDVAR, int minEdgeWidth = MINIMUM_PIXEL_RANGE); // Star Detect - Threshold template int findOneStar(const QRect &boundary); template void calculateMinMax(); template QPair getParitionMinMax(uint32_t start, uint32_t stride); /* Calculate running average & standard deviation using Welford’s method for computing variance */ template void runningAverageStdDev(); template QPair getSquaredSumAndMean(uint32_t start, uint32_t stride); // Sobel detector by Gonzalo Exequiel Pedone template void sobel(QVector &gradient, QVector &direction); template void convertToQImage(double dataMin, double dataMax, double scale, double zero, QImage &image); // Give unique IDs to each contiguous region int partition(int width, int height, QVector &gradient, QVector &ids); void trace(int width, int height, int id, QVector &image, QVector &ids, int x, int y); #ifndef KSTARS_LITE FITSHistogram *histogram { nullptr }; // Pointer to the FITS data histogram #endif /// Pointer to CFITSIO FITS file struct fitsfile *fptr { nullptr }; /// FITS image data type (TBYTE, TUSHORT, TINT, TFLOAT, TLONG, TDOUBLE) uint32_t m_DataType { 0 }; /// Number of channels uint8_t m_Channels { 1 }; /// Generic data image buffer uint8_t *m_ImageBuffer { nullptr }; /// Is this a temporary file or one loaded from disk? bool m_isTemporary { false }; /// is this file compress (.fits.fz)? bool m_isCompressed { false }; /// Did we search for stars yet? bool starsSearched { false }; ///Star Selection Algorithm StarAlgorithm starAlgorithm { ALGORITHM_GRADIENT }; /// Do we have WCS keywords in this FITS data? bool HasWCS { false }; /// Is the image debayarable? bool HasDebayer { false }; /// Is WCS data loaded? bool WCSLoaded { false }; /// Do we need to mark stars for the user? bool markStars { false }; /// Our very own file name QString m_Filename, m_compressedFilename; /// FITS Mode (Normal, WCS, Guide, Focus..etc) FITSMode m_Mode; /// How many times the image was rotated? Useful for WCS keywords rotation on save. int rotCounter { 0 }; /// How many times the image was flipped horizontally? int flipHCounter { 0 }; /// How many times the image was flipped vertically? int flipVCounter { 0 }; /// Pointer to WCS coordinate data, if any. wcs_point *wcs_coord { nullptr }; /// WCS Struct - struct wcsprm *wcs + struct wcsprm *m_wcs { nullptr }; + int m_nwcs = 0; /// All the stars we detected, if any. QList starCenters; QList localStarCenters; /// The biggest fattest star in the image. Edge *maxHFRStar { nullptr }; uint8_t *bayerBuffer { nullptr }; /// Bayer parameters BayerParams debayerParams; Statistic stats; // A list of header records QList records; /// Remove temporary files after closing bool autoRemoveTemporaryFITS { true }; QString lastError; static const QString m_TemporaryPath; }; diff --git a/kstars/fitsviewer/fitsview.cpp b/kstars/fitsviewer/fitsview.cpp index 4b3c2ad30..9f677cb24 100644 --- a/kstars/fitsviewer/fitsview.cpp +++ b/kstars/fitsviewer/fitsview.cpp @@ -1,1811 +1,1820 @@ /* FITS View Copyright (C) 2003-2017 Jasem Mutlaq Copyright (C) 2016-2017 Robert Lancaster 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 "config-kstars.h" #include "fitsview.h" #include "fitsdata.h" #include "fitslabel.h" #include "kspopupmenu.h" #include "kstarsdata.h" #include "ksutils.h" #include "Options.h" #include "skymap.h" #include "fits_debug.h" #include "stretch.h" #ifdef HAVE_INDI #include "basedevice.h" #include "indi/indilistener.h" #endif #include #include #include #include #include #include #include #define BASE_OFFSET 50 #define ZOOM_DEFAULT 100.0f #define ZOOM_MIN 10 #define ZOOM_MAX 400 #define ZOOM_LOW_INCR 10 #define ZOOM_HIGH_INCR 50 -namespace { +namespace +{ // Derive the Green and Blue stretch parameters from their previous values and the // changes made to the Red parameters. We apply the same offsets used for Red to the // other channels' parameters, but clip them. -void ComputeGBStretchParams(const StretchParams &newParams, StretchParams* params) { +void ComputeGBStretchParams(const StretchParams &newParams, StretchParams* params) +{ float shadow_diff = newParams.grey_red.shadows - params->grey_red.shadows; float highlight_diff = newParams.grey_red.highlights - params->grey_red.highlights; float midtones_diff = newParams.grey_red.midtones - params->grey_red.midtones; params->green.shadows = params->green.shadows + shadow_diff; params->green.shadows = KSUtils::clamp(params->green.shadows, 0.0f, 1.0f); params->green.highlights = params->green.highlights + highlight_diff; params->green.highlights = KSUtils::clamp(params->green.highlights, 0.0f, 1.0f); params->green.midtones = params->green.midtones + midtones_diff; params->green.midtones = std::max(params->green.midtones, 0.0f); params->blue.shadows = params->blue.shadows + shadow_diff; params->blue.shadows = KSUtils::clamp(params->blue.shadows, 0.0f, 1.0f); params->blue.highlights = params->blue.highlights + highlight_diff; params->blue.highlights = KSUtils::clamp(params->blue.highlights, 0.0f, 1.0f); params->blue.midtones = params->blue.midtones + midtones_diff; params->blue.midtones = std::max(params->blue.midtones, 0.0f); } } // namespace // Runs the stretch checking the variables to see which parameters to use. // We call stretch even if we're not stretching, as the stretch code still // converts the image to the uint8 output image which will be displayed. // In that case, it will use an identity stretch. void FITSView::doStretch(FITSData *data, QImage *outputImage) { if (outputImage->isNull()) return; Stretch stretch(static_cast(data->width()), static_cast(data->height()), data->channels(), data->property("dataType").toInt()); StretchParams tempParams; if (!stretchImage) tempParams = StretchParams(); // Keeping it linear else if (autoStretch) { // Compute new auto-stretch params. stretchParams = stretch.computeParams(data->getImageBuffer()); tempParams = stretchParams; } else // Use the existing stretch params. tempParams = stretchParams; stretch.setParams(tempParams); stretch.run(data->getImageBuffer(), outputImage, sampling); } // Store stretch parameters, and turn on stretching if it isn't already on. -void FITSView::setStretchParams(const StretchParams& params) +void FITSView::setStretchParams(const StretchParams ¶ms) { if (imageData->channels() == 3) ComputeGBStretchParams(params, &stretchParams); stretchParams.grey_red = params.grey_red; stretchParams.grey_red.shadows = std::max(stretchParams.grey_red.shadows, 0.0f); stretchParams.grey_red.highlights = std::max(stretchParams.grey_red.highlights, 0.0f); stretchParams.grey_red.midtones = std::max(stretchParams.grey_red.midtones, 0.0f); autoStretch = false; stretchImage = true; if (image_frame != nullptr && rescale(ZOOM_KEEP_LEVEL)) - updateFrame(); + updateFrame(); } // Turn on or off stretching, and if on, use whatever parameters are currently stored. void FITSView::setStretch(bool onOff) { if (stretchImage != onOff) { stretchImage = onOff; if (image_frame != nullptr && rescale(ZOOM_KEEP_LEVEL)) updateFrame(); } } // Turn on stretching, using automatically generated parameters. void FITSView::setAutoStretchParams() { stretchImage = true; autoStretch = true; if (image_frame != nullptr && rescale(ZOOM_KEEP_LEVEL)) updateFrame(); } FITSView::FITSView(QWidget * parent, FITSMode fitsMode, FITSScale filterType) : QScrollArea(parent), zoomFactor(1.2) { // stretchImage is whether to stretch or not--the stretch may or may not use automatically generated parameters. // The user may enter his/her own. stretchImage = Options::autoStretch(); // autoStretch means use automatically-generated parameters. This is the default, unless the user overrides // by adjusting the stretchBar's sliders. autoStretch = true; grabGesture(Qt::PinchGesture); image_frame.reset(new FITSLabel(this)); filter = filterType; mode = fitsMode; setBackgroundRole(QPalette::Dark); markerCrosshair.setX(0); markerCrosshair.setY(0); setBaseSize(740, 530); connect(image_frame.get(), SIGNAL(newStatus(QString, FITSBar)), this, SIGNAL(newStatus(QString, FITSBar))); connect(image_frame.get(), SIGNAL(pointSelected(int, int)), this, SLOT(processPointSelection(int, int))); connect(image_frame.get(), SIGNAL(markerSelected(int, int)), this, SLOT(processMarkerSelection(int, int))); connect(&wcsWatcher, SIGNAL(finished()), this, SLOT(syncWCSState())); connect(&fitsWatcher, &QFutureWatcher::finished, this, &FITSView::loadInFrame); image_frame->setMouseTracking(true); setCursorMode( selectCursor); //This is the default mode because the Focus and Align FitsViews should not be in dragMouse mode noImageLabel = new QLabel(); noImage.load(":/images/noimage.png"); noImageLabel->setPixmap(noImage); noImageLabel->setAlignment(Qt::AlignCenter); this->setWidget(noImageLabel); redScopePixmap = QPixmap(":/icons/center_telescope_red.svg").scaled(32, 32, Qt::KeepAspectRatio, Qt::FastTransformation); magentaScopePixmap = QPixmap(":/icons/center_telescope_magenta.svg").scaled(32, 32, Qt::KeepAspectRatio, Qt::FastTransformation); } FITSView::~FITSView() { fitsWatcher.waitForFinished(); wcsWatcher.waitForFinished(); delete (imageData); } /** This method looks at what mouse mode is currently selected and updates the cursor to match. */ void FITSView::updateMouseCursor() { if (cursorMode == dragCursor) { if (horizontalScrollBar()->maximum() > 0 || verticalScrollBar()->maximum() > 0) { if (!image_frame->getMouseButtonDown()) viewport()->setCursor(Qt::PointingHandCursor); else viewport()->setCursor(Qt::ClosedHandCursor); } else viewport()->setCursor(Qt::CrossCursor); } else if (cursorMode == selectCursor) { viewport()->setCursor(Qt::CrossCursor); } else if (cursorMode == scopeCursor) { viewport()->setCursor(QCursor(redScopePixmap, 10, 10)); } else if (cursorMode == crosshairCursor) { viewport()->setCursor(QCursor(magentaScopePixmap, 10, 10)); } } /** This is how the mouse mode gets set. The default for a FITSView in a FITSViewer should be the dragMouse The default for a FITSView in the Focus or Align module should be the selectMouse The different defaults are accomplished by putting making the actual default mouseMode the selectMouse, but when a FITSViewer loads an image, it immediately makes it the dragMouse. */ void FITSView::setCursorMode(CursorMode mode) { cursorMode = mode; updateMouseCursor(); if (mode == scopeCursor && imageHasWCS()) { if (!imageData->isWCSLoaded() && !wcsWatcher.isRunning()) { QFuture future = QtConcurrent::run(imageData, &FITSData::loadWCS); wcsWatcher.setFuture(future); } } } void FITSView::resizeEvent(QResizeEvent * event) { if ((imageData == nullptr) && noImageLabel != nullptr) { noImageLabel->setPixmap( noImage.scaled(width() - 20, height() - 20, Qt::KeepAspectRatio, Qt::FastTransformation)); noImageLabel->setFixedSize(width() - 5, height() - 5); } QScrollArea::resizeEvent(event); } void FITSView::loadFITS(const QString &inFilename, bool silent) { if (floatingToolBar != nullptr) { floatingToolBar->setVisible(true); } bool setBayerParams = false; BayerParams param; if ((imageData != nullptr) && imageData->hasDebayer()) { setBayerParams = true; imageData->getBayerParams(¶m); } // In case image is still loading, wait until it is done. fitsWatcher.waitForFinished(); // In case loadWCS is still running for previous image data, let's wait until it's over wcsWatcher.waitForFinished(); delete imageData; imageData = nullptr; filterStack.clear(); filterStack.push(FITS_NONE); if (filter != FITS_NONE) filterStack.push(filter); imageData = new FITSData(mode); if (setBayerParams) imageData->setBayerParams(¶m); fitsWatcher.setFuture(imageData->loadFITS(inFilename, silent)); } bool FITSView::loadFITSFromData(FITSData *data, const QString &inFilename) { Q_UNUSED(inFilename) if (imageData != nullptr) { - delete imageData; - imageData = nullptr; + delete imageData; + imageData = nullptr; } if (floatingToolBar != nullptr) { floatingToolBar->setVisible(true); } // In case loadWCS is still running for previous image data, let's wait until it's over wcsWatcher.waitForFinished(); filterStack.clear(); filterStack.push(FITS_NONE); if (filter != FITS_NONE) filterStack.push(filter); // Takes control of the objects passed in. imageData = data; return processData(); } bool FITSView::processData() { // Set current width and height if (!imageData) return false; currentWidth = imageData->width(); currentHeight = imageData->height(); int image_width = currentWidth; int image_height = currentHeight; image_frame->setSize(image_width, image_height); // Init the display image - initDisplayImage(); + // JM 2020.01.08: Disabling as proposed by Hy + //initDisplayImage(); imageData->applyFilter(filter); // Rescale to fits window on first load if (firstLoad) { currentZoom = 100; if (rescale(ZOOM_FIT_WINDOW) == false) { m_LastError = i18n("Rescaling image failed."); return false; } firstLoad = false; } else { if (rescale(ZOOM_KEEP_LEVEL) == false) { m_LastError = i18n("Rescaling image failed."); return false; } } setAlignment(Qt::AlignCenter); // Load WCS data now if selected and image contains valid WCS header if (imageData->hasWCS() && Options::autoWCS() && (mode == FITS_NORMAL || mode == FITS_ALIGN) && !wcsWatcher.isRunning()) { QFuture future = QtConcurrent::run(imageData, &FITSData::loadWCS); wcsWatcher.setFuture(future); } else syncWCSState(); if (isVisible()) emit newStatus(QString("%1x%2").arg(image_width).arg(image_height), FITS_RESOLUTION); if (showStarProfile) { if(floatingToolBar != nullptr) toggleProfileAction->setChecked(true); //Need to wait till the Focus module finds stars, if its the Focus module. QTimer::singleShot(100, this, SLOT(viewStarProfile())); } scaledImage = QImage(); updateFrame(); return true; } void FITSView::loadInFrame() { // Check if the loading was OK if (fitsWatcher.result() == false) { m_LastError = imageData->getLastError(); emit failed(); return; } // Notify if there is debayer data. emit debayerToggled(imageData->hasDebayer()); if (processData()) - emit loaded(); + emit loaded(); else - emit failed(); + emit failed(); } int FITSView::saveFITS(const QString &newFilename) { return imageData->saveFITS(newFilename); } bool FITSView::rescale(FITSZoom type) { switch (imageData->property("dataType").toInt()) { case TBYTE: return rescale(type); case TSHORT: return rescale(type); case TUSHORT: return rescale(type); case TLONG: return rescale(type); case TULONG: return rescale(type); case TFLOAT: return rescale(type); case TLONGLONG: return rescale(type); case TDOUBLE: return rescale(type); default: break; } return false; } FITSView::CursorMode FITSView::getCursorMode() { return cursorMode; } void FITSView::enterEvent(QEvent * event) { Q_UNUSED(event) if ((floatingToolBar != nullptr) && (imageData != nullptr)) { QPointer eff = new QGraphicsOpacityEffect(this); floatingToolBar->setGraphicsEffect(eff); QPointer a = new QPropertyAnimation(eff, "opacity"); a->setDuration(500); a->setStartValue(0.2); a->setEndValue(1); a->setEasingCurve(QEasingCurve::InBack); a->start(QPropertyAnimation::DeleteWhenStopped); } } void FITSView::leaveEvent(QEvent * event) { Q_UNUSED(event) if ((floatingToolBar != nullptr) && (imageData != nullptr)) { QPointer eff = new QGraphicsOpacityEffect(this); floatingToolBar->setGraphicsEffect(eff); QPointer a = new QPropertyAnimation(eff, "opacity"); a->setDuration(500); a->setStartValue(1); a->setEndValue(0.2); a->setEasingCurve(QEasingCurve::OutBack); a->start(QPropertyAnimation::DeleteWhenStopped); } } template bool FITSView::rescale(FITSZoom type) { - if (rawImage.isNull()) - return false; + // JM 2020.01.08: Disabling as proposed by Hy + // if (rawImage.isNull()) + // return false; if (!imageData) return false; int image_width = imageData->width(); int image_height = imageData->height(); currentWidth = image_width; currentHeight = image_height; if (isVisible()) - emit newStatus(QString("%1x%2").arg(image_width).arg(image_height), FITS_RESOLUTION); + emit newStatus(QString("%1x%2").arg(image_width).arg(image_height), FITS_RESOLUTION); switch (type) { case ZOOM_FIT_WINDOW: if ((image_width > width() || image_height > height())) { double w = baseSize().width() - BASE_OFFSET; double h = baseSize().height() - BASE_OFFSET; if (!firstLoad) { w = viewport()->rect().width() - BASE_OFFSET; h = viewport()->rect().height() - BASE_OFFSET; } // Find the zoom level which will enclose the current FITS in the current window size double zoomX = floor((w / static_cast(currentWidth)) * 100.); double zoomY = floor((h / static_cast(currentHeight)) * 100.); (zoomX < zoomY) ? currentZoom = zoomX : currentZoom = zoomY; currentWidth = image_width * (currentZoom / ZOOM_DEFAULT); currentHeight = image_height * (currentZoom / ZOOM_DEFAULT); if (currentZoom <= ZOOM_MIN) emit actionUpdated("view_zoom_out", false); } else { currentZoom = 100; currentWidth = image_width; currentHeight = image_height; } break; case ZOOM_KEEP_LEVEL: { currentWidth = image_width * (currentZoom / ZOOM_DEFAULT); currentHeight = image_height * (currentZoom / ZOOM_DEFAULT); } break; default: currentZoom = 100; break; } initDisplayImage(); image_frame->setScaledContents(true); doStretch(imageData, &rawImage); scaledImage = QImage(); setWidget(image_frame.get()); // This is needed by fitstab, even if the zoom doesn't change, to change the stretch UI. emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM); return true; } void FITSView::ZoomIn() { if (currentZoom >= ZOOM_DEFAULT && Options::limitedResourcesMode()) { emit newStatus(i18n("Cannot zoom in further due to active limited resources mode."), FITS_MESSAGE); return; } if (currentZoom < ZOOM_DEFAULT) currentZoom += ZOOM_LOW_INCR; else currentZoom += ZOOM_HIGH_INCR; emit actionUpdated("view_zoom_out", true); if (currentZoom >= ZOOM_MAX) { currentZoom = ZOOM_MAX; emit actionUpdated("view_zoom_in", false); } if (!imageData) return; currentWidth = imageData->width() * (currentZoom / ZOOM_DEFAULT); currentHeight = imageData->height() * (currentZoom / ZOOM_DEFAULT); updateFrame(); emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM); } void FITSView::ZoomOut() { if (currentZoom <= ZOOM_DEFAULT) currentZoom -= ZOOM_LOW_INCR; else currentZoom -= ZOOM_HIGH_INCR; if (currentZoom <= ZOOM_MIN) { currentZoom = ZOOM_MIN; emit actionUpdated("view_zoom_out", false); } emit actionUpdated("view_zoom_in", true); if (!imageData) return; currentWidth = imageData->width() * (currentZoom / ZOOM_DEFAULT); currentHeight = imageData->height() * (currentZoom / ZOOM_DEFAULT); updateFrame(); emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM); } void FITSView::ZoomToFit() { if (rawImage.isNull() == false) { rescale(ZOOM_FIT_WINDOW); updateFrame(); } } void FITSView::setStarFilterRange(float const innerRadius, float const outerRadius) { starFilter.innerRadius = innerRadius; starFilter.outerRadius = outerRadius; } int FITSView::filterStars() { return starFilter.used() ? imageData->filterStars(starFilter.innerRadius, starFilter.outerRadius) : imageData->getStarCenters().count(); } void FITSView::updateFrame() { bool ok = false; if (toggleStretchAction) - toggleStretchAction->setChecked(stretchImage); - + toggleStretchAction->setChecked(stretchImage); + if (currentZoom != ZOOM_DEFAULT) { // Only scale when necessary if (scaledImage.isNull() || currentWidth != lastWidth || currentHeight != lastHeight) { scaledImage = rawImage.scaled(currentWidth, currentHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation); lastWidth = currentWidth; lastHeight = currentHeight; } ok = displayPixmap.convertFromImage(scaledImage); } else ok = displayPixmap.convertFromImage(rawImage); if (!ok) return; QPainter painter(&displayPixmap); drawOverlay(&painter); if (starFilter.used()) { double const diagonal = std::sqrt(currentWidth * currentWidth + currentHeight * currentHeight) / 2; int const innerRadius = std::lround(diagonal * starFilter.innerRadius); int const outerRadius = std::lround(diagonal * starFilter.outerRadius); QPoint const center(currentWidth / 2, currentHeight / 2); painter.save(); painter.setPen(QPen(Qt::blue, 1, Qt::DashLine)); painter.setOpacity(0.7); painter.setBrush(QBrush(Qt::transparent)); painter.drawEllipse(center, outerRadius, outerRadius); painter.setBrush(QBrush(Qt::blue, Qt::FDiagPattern)); painter.drawEllipse(center, innerRadius, innerRadius); painter.restore(); } image_frame->setPixmap(displayPixmap); image_frame->resize(currentWidth, currentHeight); } void FITSView::ZoomDefault() { if (image_frame != nullptr) { emit actionUpdated("view_zoom_out", true); emit actionUpdated("view_zoom_in", true); currentZoom = ZOOM_DEFAULT; currentWidth = imageData->width(); currentHeight = imageData->height(); updateFrame(); emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM); update(); } } void FITSView::drawOverlay(QPainter * painter) { painter->setRenderHint(QPainter::Antialiasing, Options::useAntialias()); if (trackingBoxEnabled && getCursorMode() != FITSView::scopeCursor) drawTrackingBox(painter); if (!markerCrosshair.isNull()) drawMarker(painter); if (showCrosshair) drawCrosshair(painter); if (showObjects) drawObjectNames(painter); if (showEQGrid) drawEQGrid(painter); if (showPixelGrid) drawPixelGrid(painter); if (markStars) drawStarCentroid(painter); } void FITSView::updateMode(FITSMode fmode) { mode = fmode; } void FITSView::drawMarker(QPainter * painter) { painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")), 2)); painter->setBrush(Qt::NoBrush); float pxperdegree = (currentZoom / ZOOM_DEFAULT) * (57.3 / 1.8); float s1 = 0.5 * pxperdegree; float s2 = pxperdegree; float s3 = 2.0 * pxperdegree; float x0 = markerCrosshair.x() * (currentZoom / ZOOM_DEFAULT); float y0 = markerCrosshair.y() * (currentZoom / ZOOM_DEFAULT); float x1 = x0 - 0.5 * s1; float y1 = y0 - 0.5 * s1; float x2 = x0 - 0.5 * s2; float y2 = y0 - 0.5 * s2; float x3 = x0 - 0.5 * s3; float y3 = y0 - 0.5 * s3; //Draw radial lines painter->drawLine(QPointF(x1, y0), QPointF(x3, y0)); painter->drawLine(QPointF(x0 + s2, y0), QPointF(x0 + 0.5 * s1, y0)); painter->drawLine(QPointF(x0, y1), QPointF(x0, y3)); painter->drawLine(QPointF(x0, y0 + 0.5 * s1), QPointF(x0, y0 + s2)); //Draw circles at 0.5 & 1 degrees painter->drawEllipse(QRectF(x1, y1, s1, s1)); painter->drawEllipse(QRectF(x2, y2, s2, s2)); } void FITSView::drawStarCentroid(QPainter * painter) { float const ratio = currentZoom / ZOOM_DEFAULT; if (showStarsHFR) { QFont painterFont; // If we need to print the HFR out, give an arbitrarily sized font to the painter painterFont.setPointSizeF(painterFont.pointSizeF() * 3 * ratio); painter->setFont(painterFont); } painter->setPen(QPen(Qt::red, 2)); QFontMetrics const fontMetrics = painter->fontMetrics(); QRect const boundingRect(0, 0, painter->device()->width(), painter->device()->height()); foreach (auto const &starCenter, imageData->getStarCenters()) { int const x1 = std::round((starCenter->x - starCenter->width / 2.0f) * ratio); int const y1 = std::round((starCenter->y - starCenter->width / 2.0f) * ratio); int const w = std::round(starCenter->width * ratio); // Draw a circle around the detected star painter->drawEllipse(x1, y1, w, w); if (showStarsHFR) { // Ask the painter how large will the HFR text be QString const hfr = QString("%1").arg(starCenter->HFR, 0, 'f', 2); QSize const hfrSize = fontMetrics.size(Qt::TextSingleLine, hfr); // Store the HFR text in a rect - QPoint const hfrBottomLeft(x1+w+5, y1+w/2); + QPoint const hfrBottomLeft(x1 + w + 5, y1 + w / 2); QRect const hfrRect(hfrBottomLeft.x(), hfrBottomLeft.y() - hfrSize.height(), hfrSize.width(), hfrSize.height()); // Render the HFR text only if it can be displayed entirely if (boundingRect.contains(hfrRect)) { painter->setPen(QPen(Qt::red, 3)); painter->drawText(hfrBottomLeft, hfr); painter->setPen(QPen(Qt::red, 2)); } } } } void FITSView::drawTrackingBox(QPainter * painter) { painter->setPen(QPen(Qt::green, 2)); if (trackingBox.isNull()) return; int x1 = trackingBox.x() * (currentZoom / ZOOM_DEFAULT); int y1 = trackingBox.y() * (currentZoom / ZOOM_DEFAULT); int w = trackingBox.width() * (currentZoom / ZOOM_DEFAULT); int h = trackingBox.height() * (currentZoom / ZOOM_DEFAULT); painter->drawRect(x1, y1, w, h); } /** This Method draws a large Crosshair in the center of the image, it is like a set of axes. */ void FITSView::drawCrosshair(QPainter * painter) { if (!imageData) return; int image_width = imageData->width(); int image_height = imageData->height(); float scale = (currentZoom / ZOOM_DEFAULT); QPointF c = QPointF((qreal)image_width / 2 * scale, (qreal)image_height / 2 * scale); float midX = (float)image_width / 2 * scale; float midY = (float)image_height / 2 * scale; float maxX = (float)image_width * scale; float maxY = (float)image_height * scale; float r = 50 * scale; painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")))); //Horizontal Line to Circle painter->drawLine(0, midY, midX - r, midY); //Horizontal Line past Circle painter->drawLine(midX + r, midY, maxX, midY); //Vertical Line to Circle painter->drawLine(midX, 0, midX, midY - r); //Vertical Line past Circle painter->drawLine(midX, midY + r, midX, maxY); //Circles painter->drawEllipse(c, r, r); painter->drawEllipse(c, r / 2, r / 2); } /** This method is intended to draw a pixel grid onto the image. It first determines useful information from the image. Then it draws the axes on the image if the crosshairs are not displayed. Finally it draws the gridlines so that there will be 4 Gridlines on either side of the axes. Note: This has to start drawing at the center not at the edges because the center axes must be in the center of the image. */ void FITSView::drawPixelGrid(QPainter * painter) { float scale = (currentZoom / ZOOM_DEFAULT); float width = imageData->width() * scale; float height = imageData->height() * scale; float cX = width / 2; float cY = height / 2; float deltaX = width / 10; float deltaY = height / 10; //draw the Axes painter->setPen(QPen(Qt::red)); painter->drawText(cX - 30, height - 5, QString::number((int)((cX) / scale))); painter->drawText(width - 30, cY - 5, QString::number((int)((cY) / scale))); if (!showCrosshair) { painter->drawLine(cX, 0, cX, height); painter->drawLine(0, cY, width, cY); } painter->setPen(QPen(Qt::gray)); //Start one iteration past the Center and draw 4 lines on either side of 0 for (int x = deltaX; x < cX - deltaX; x += deltaX) { painter->drawText(cX + x - 30, height - 5, QString::number((int)((cX + x) / scale))); painter->drawText(cX - x - 30, height - 5, QString::number((int)((cX - x) / scale))); painter->drawLine(cX - x, 0, cX - x, height); painter->drawLine(cX + x, 0, cX + x, height); } //Start one iteration past the Center and draw 4 lines on either side of 0 for (int y = deltaY; y < cY - deltaY; y += deltaY) { painter->drawText(width - 30, cY + y - 5, QString::number((int)((cY + y) / scale))); painter->drawText(width - 30, cY - y - 5, QString::number((int)((cY - y) / scale))); painter->drawLine(0, cY + y, width, cY + y); painter->drawLine(0, cY - y, width, cY - y); } } bool FITSView::imageHasWCS() { if (imageData != nullptr) return imageData->hasWCS(); return false; } void FITSView::drawObjectNames(QPainter * painter) { painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("FITSObjectLabelColor")))); float scale = (currentZoom / ZOOM_DEFAULT); foreach (FITSSkyObject * listObject, imageData->getSkyObjects()) { painter->drawRect(listObject->x() * scale - 5, listObject->y() * scale - 5, 10, 10); painter->drawText(listObject->x() * scale + 10, listObject->y() * scale + 10, listObject->skyObject()->name()); } } /** This method will paint EQ Gridlines in an overlay if there is WCS data present. It determines the minimum and maximum RA and DEC, then it uses that information to judge which gridLines to draw. Then it calls the drawEQGridlines methods below to draw gridlines at those specific RA and Dec values. */ void FITSView::drawEQGrid(QPainter * painter) { float scale = (currentZoom / ZOOM_DEFAULT); int image_width = imageData->width(); int image_height = imageData->height(); if (imageData->hasWCS()) { wcs_point * wcs_coord = imageData->getWCSCoord(); if (wcs_coord != nullptr) { int size = image_width * image_height; double maxRA = -1000; double minRA = 1000; double maxDec = -1000; double minDec = 1000; for (int i = 0; i < (size); i++) { double ra = wcs_coord[i].ra; double dec = wcs_coord[i].dec; if (ra > maxRA) maxRA = ra; if (ra < minRA) minRA = ra; if (dec > maxDec) maxDec = dec; if (dec < minDec) minDec = dec; } auto minDecMinutes = (int)(minDec * 12); //This will force the Dec Scale to 5 arc minutes in the loop auto maxDecMinutes = (int)(maxDec * 12); auto minRAMinutes = (int)(minRA / 15.0 * 120.0); //This will force the scale to 1/2 minutes of RA in the loop from 0 to 50 degrees auto maxRAMinutes = (int)(maxRA / 15.0 * 120.0); double raConvert = 15 / 120.0; //This will undo the calculation above to retrieve the actual RA. double decConvert = 1.0 / 12.0; //This will undo the calculation above to retrieve the actual DEC. if (maxDec > 50 || minDec < -50) { minRAMinutes = (int)(minRA / 15.0 * 60.0); //This will force the scale to 1 min of RA from 50 to 80 degrees maxRAMinutes = (int)(maxRA / 15.0 * 60.0); raConvert = 15 / 60.0; } if (maxDec > 80 || minDec < -80) { minRAMinutes = (int)(minRA / 15.0 * 30); //This will force the scale to 2 min of RA from 80 to 85 degrees maxRAMinutes = (int)(maxRA / 15.0 * 30); raConvert = 15 / 30.0; } if (maxDec > 85 || minDec < -85) { minRAMinutes = (int)(minRA / 15.0 * 6); //This will force the scale to 10 min of RA from 85 to 89 degrees maxRAMinutes = (int)(maxRA / 15.0 * 6); raConvert = 15 / 6.0; } if (maxDec >= 89.25 || minDec <= -89.25) { minRAMinutes = (int)(minRA / 15); //This will force the scale to whole hours of RA in the loop really close to the poles maxRAMinutes = (int)(maxRA / 15); raConvert = 15; } painter->setPen(QPen(Qt::yellow)); QPointF pixelPoint, imagePoint, pPoint; //This section draws the RA Gridlines for (int targetRA = minRAMinutes; targetRA <= maxRAMinutes; targetRA++) { painter->setPen(QPen(Qt::yellow)); double target = targetRA * raConvert; if (eqGridPoints.count() != 0) eqGridPoints.clear(); double increment = std::abs((maxDec - minDec) / 100.0); //This will determine how many points to use to create the RA Line for (double targetDec = minDec; targetDec <= maxDec; targetDec += increment) { SkyPoint pointToGet(target / 15.0, targetDec); bool inImage = imageData->wcsToPixel(pointToGet, pixelPoint, imagePoint); if (inImage) { QPointF pt(pixelPoint.x() * scale, pixelPoint.y() * scale); eqGridPoints.append(pt); } } if (eqGridPoints.count() > 1) { for (int i = 1; i < eqGridPoints.count(); i++) painter->drawLine(eqGridPoints.value(i - 1), eqGridPoints.value(i)); QPointF pt = getPointForGridLabel(); if (pt.x() != -100) { if (maxDec > 50 || maxDec < -50) painter->drawText(pt.x(), pt.y(), QString::number(dms(target).hour()) + "h " + QString::number(dms(target).minute()) + '\''); else painter->drawText(pt.x() - 20, pt.y(), QString::number(dms(target).hour()) + "h " + QString::number(dms(target).minute()) + "' " + QString::number(dms(target).second()) + "''"); } } } //This section draws the DEC Gridlines for (int targetDec = minDecMinutes; targetDec <= maxDecMinutes; targetDec++) { if (eqGridPoints.count() != 0) eqGridPoints.clear(); double increment = std::abs((maxRA - minRA) / 100.0); //This will determine how many points to use to create the Dec Line double target = targetDec * decConvert; for (double targetRA = minRA; targetRA <= maxRA; targetRA += increment) { SkyPoint pointToGet(targetRA / 15, targetDec * decConvert); bool inImage = imageData->wcsToPixel(pointToGet, pixelPoint, imagePoint); if (inImage) { QPointF pt(pixelPoint.x() * scale, pixelPoint.y() * scale); eqGridPoints.append(pt); } } if (eqGridPoints.count() > 1) { for (int i = 1; i < eqGridPoints.count(); i++) painter->drawLine(eqGridPoints.value(i - 1), eqGridPoints.value(i)); QPointF pt = getPointForGridLabel(); if (pt.x() != -100) painter->drawText(pt.x(), pt.y(), QString::number(dms(target).degree()) + "° " + QString::number(dms(target).arcmin()) + '\''); } } //This Section Draws the North Celestial Pole if present SkyPoint NCP(0, 90); bool NCPtest = imageData->wcsToPixel(NCP, pPoint, imagePoint); if (NCPtest) { bool NCPinImage = (pPoint.x() > 0 && pPoint.x() < image_width) && (pPoint.y() > 0 && pPoint.y() < image_height); if (NCPinImage) { painter->fillRect(pPoint.x() * scale - 2, pPoint.y() * scale - 2, 4, 4, KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")); painter->drawText(pPoint.x() * scale + 15, pPoint.y() * scale + 15, i18nc("North Celestial Pole", "NCP")); } } //This Section Draws the South Celestial Pole if present SkyPoint SCP(0, -90); bool SCPtest = imageData->wcsToPixel(SCP, pPoint, imagePoint); if (SCPtest) { bool SCPinImage = (pPoint.x() > 0 && pPoint.x() < image_width) && (pPoint.y() > 0 && pPoint.y() < image_height); if (SCPinImage) { painter->fillRect(pPoint.x() * scale - 2, pPoint.y() * scale - 2, 4, 4, KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")); painter->drawText(pPoint.x() * scale + 15, pPoint.y() * scale + 15, i18nc("South Celestial Pole", "SCP")); } } } } } bool FITSView::pointIsInImage(QPointF pt, bool scaled) { int image_width = imageData->width(); int image_height = imageData->height(); float scale = (currentZoom / ZOOM_DEFAULT); if (scaled) return pt.x() < image_width * scale && pt.y() < image_height * scale && pt.x() > 0 && pt.y() > 0; else return pt.x() < image_width && pt.y() < image_height && pt.x() > 0 && pt.y() > 0; } QPointF FITSView::getPointForGridLabel() { int image_width = imageData->width(); int image_height = imageData->height(); float scale = (currentZoom / ZOOM_DEFAULT); //These get the maximum X and Y points in the list that are in the image QPointF maxXPt(image_width * scale / 2, image_height * scale / 2); for (auto &p : eqGridPoints) { if (p.x() > maxXPt.x() && pointIsInImage(p, true)) maxXPt = p; } QPointF maxYPt(image_width * scale / 2, image_height * scale / 2); for (auto &p : eqGridPoints) { if (p.y() > maxYPt.y() && pointIsInImage(p, true)) maxYPt = p; } QPointF minXPt(image_width * scale / 2, image_height * scale / 2); for (auto &p : eqGridPoints) { if (p.x() < minXPt.x() && pointIsInImage(p, true)) minXPt = p; } QPointF minYPt(image_width * scale / 2, image_height * scale / 2); for (auto &p : eqGridPoints) { if (p.y() < minYPt.y() && pointIsInImage(p, true)) minYPt = p; } //This gives preference to points that are on the right hand side and bottom. //But if the line doesn't intersect the right or bottom, it then tries for the top and left. //If no points are found in the image, it returns a point off the screen //If all else fails, like in the case of a circle on the image, it returns the far right point. if (image_width * scale - maxXPt.x() < 10) { return QPointF( image_width * scale - 50, maxXPt.y() - 10); //This will draw the text on the right hand side, up and to the left of the point where the line intersects } if (image_height * scale - maxYPt.y() < 10) return QPointF( maxYPt.x() - 40, image_height * scale - 10); //This will draw the text on the bottom side, up and to the left of the point where the line intersects if (minYPt.y() * scale < 30) return QPointF( minYPt.x() + 10, 20); //This will draw the text on the top side, down and to the right of the point where the line intersects if (minXPt.x() * scale < 30) return QPointF( 10, minXPt.y() + 20); //This will draw the text on the left hand side, down and to the right of the point where the line intersects if (maxXPt.x() == image_width * scale / 2 && maxXPt.y() == image_height * scale / 2) return QPointF(-100, -100); //All of the points were off the screen return QPoint(maxXPt.x() - 40, maxXPt.y() - 10); } void FITSView::setFirstLoad(bool value) { firstLoad = value; } QPixmap &FITSView::getTrackingBoxPixmap(uint8_t margin) { if (trackingBox.isNull()) return trackingBoxPixmap; int x1 = (trackingBox.x() - margin) * (currentZoom / ZOOM_DEFAULT); int y1 = (trackingBox.y() - margin) * (currentZoom / ZOOM_DEFAULT); int w = (trackingBox.width() + margin * 2) * (currentZoom / ZOOM_DEFAULT); int h = (trackingBox.height() + margin * 2) * (currentZoom / ZOOM_DEFAULT); trackingBoxPixmap = image_frame->grab(QRect(x1, y1, w, h)); return trackingBoxPixmap; } void FITSView::setTrackingBox(const QRect &rect) { if (rect != trackingBox) { trackingBox = rect; updateFrame(); if(showStarProfile) viewStarProfile(); } } void FITSView::resizeTrackingBox(int newSize) { int x = trackingBox.x() + trackingBox.width() / 2; int y = trackingBox.y() + trackingBox.height() / 2; int delta = newSize / 2; setTrackingBox(QRect( x - delta, y - delta, newSize, newSize)); } bool FITSView::isImageStretched() { return stretchImage; } bool FITSView::isCrosshairShown() { return showCrosshair; } bool FITSView::isEQGridShown() { return showEQGrid; } bool FITSView::areObjectsShown() { return showObjects; } bool FITSView::isPixelGridShown() { return showPixelGrid; } void FITSView::toggleCrosshair() { showCrosshair = !showCrosshair; updateFrame(); } void FITSView::toggleEQGrid() { showEQGrid = !showEQGrid; if (!imageData->isWCSLoaded() && !wcsWatcher.isRunning()) { QFuture future = QtConcurrent::run(imageData, &FITSData::loadWCS); wcsWatcher.setFuture(future); return; } if (image_frame != nullptr) updateFrame(); } void FITSView::toggleObjects() { showObjects = !showObjects; if (!imageData->isWCSLoaded() && !wcsWatcher.isRunning()) { QFuture future = QtConcurrent::run(imageData, &FITSData::loadWCS); wcsWatcher.setFuture(future); return; } if (image_frame != nullptr) updateFrame(); } void FITSView::toggleStars() { toggleStars(!markStars); if (image_frame != nullptr) updateFrame(); } void FITSView::toggleStretch() { stretchImage = !stretchImage; if (image_frame != nullptr && rescale(ZOOM_KEEP_LEVEL)) - updateFrame(); + updateFrame(); } void FITSView::toggleStarProfile() { #ifdef HAVE_DATAVISUALIZATION showStarProfile = !showStarProfile; if(showStarProfile && trackingBoxEnabled) viewStarProfile(); if(toggleProfileAction) toggleProfileAction->setChecked(showStarProfile); if(showStarProfile) { //The tracking box is already on for Guide and Focus Views, but off for Normal and Align views. //So for Normal and Align views, we need to set up the tracking box. if(mode == FITS_NORMAL || mode == FITS_ALIGN) { setCursorMode(selectCursor); connect(this, SIGNAL(trackingStarSelected(int, int)), this, SLOT(move3DTrackingBox(int, int))); trackingBox = QRect(0, 0, 128, 128); setTrackingBoxEnabled(true); if(starProfileWidget) connect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)), this, SLOT(resizeTrackingBox(int))); } if(starProfileWidget) connect(starProfileWidget, SIGNAL(rejected()), this, SLOT(toggleStarProfile())); } else { //This shuts down the tracking box for Normal and Align Views //It doesn't touch Guide and Focus Views because they still need a tracking box if(mode == FITS_NORMAL || mode == FITS_ALIGN) { if(getCursorMode() == selectCursor) setCursorMode(dragCursor); disconnect(this, SIGNAL(trackingStarSelected(int, int)), this, SLOT(move3DTrackingBox(int, int))); setTrackingBoxEnabled(false); if(starProfileWidget) disconnect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)), this, SLOT(resizeTrackingBox(int))); } if(starProfileWidget) { disconnect(starProfileWidget, SIGNAL(rejected()), this, SLOT(toggleStarProfile())); starProfileWidget->close(); starProfileWidget = nullptr; } emit starProfileWindowClosed(); } updateFrame(); #endif } void FITSView::move3DTrackingBox(int x, int y) { int boxSize = trackingBox.width(); QRect starRect = QRect(x - boxSize / 2, y - boxSize / 2, boxSize, boxSize); setTrackingBox(starRect); } void FITSView::viewStarProfile() { #ifdef HAVE_DATAVISUALIZATION if(!trackingBoxEnabled) { setTrackingBoxEnabled(true); setTrackingBox(QRect(0, 0, 128, 128)); } if(!starProfileWidget) { starProfileWidget = new StarProfileViewer(this); //This is a band-aid to fix a QT bug with createWindowContainer //It will set the cursor of the Window containing the view that called the Star Profile method to the Arrow Cursor //Note that Ekos Manager is a QDialog and FitsViewer is a KXmlGuiWindow QWidget * superParent = this->parentWidget(); while(superParent->parentWidget() != 0 && !superParent->inherits("QDialog") && !superParent->inherits("KXmlGuiWindow")) superParent = superParent->parentWidget(); superParent->setCursor(Qt::ArrowCursor); //This is the end of the band-aid connect(starProfileWidget, SIGNAL(rejected()), this, SLOT(toggleStarProfile())); if(mode == FITS_ALIGN || mode == FITS_NORMAL) { starProfileWidget->enableTrackingBox(true); imageData->setStarAlgorithm(ALGORITHM_CENTROID); connect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)), this, SLOT(resizeTrackingBox(int))); } } QList starCenters = imageData->getStarCentersInSubFrame(trackingBox); if(starCenters.size() == 0) { // FIXME, the following does not work anymore. //imageData->findStars(&trackingBox, true); // FIXME replacing it with this imageData->findStars(ALGORITHM_CENTROID, trackingBox); starCenters = imageData->getStarCentersInSubFrame(trackingBox); } starProfileWidget->loadData(imageData, trackingBox, starCenters); starProfileWidget->show(); starProfileWidget->raise(); if(markStars) updateFrame(); //this is to update for the marked stars #endif } void FITSView::togglePixelGrid() { showPixelGrid = !showPixelGrid; updateFrame(); } int FITSView::findStars(StarAlgorithm algorithm, const QRect &searchBox) { int count = 0; if(trackingBoxEnabled) count = imageData->findStars(algorithm, trackingBox); else count = imageData->findStars(algorithm, searchBox); return count; } void FITSView::toggleStars(bool enable) { markStars = enable; if (markStars && !imageData->areStarsSearched()) { QApplication::setOverrideCursor(Qt::WaitCursor); emit newStatus(i18n("Finding stars..."), FITS_MESSAGE); qApp->processEvents(); int count = findStars(); if (count >= 0 && isVisible()) emit newStatus(i18np("1 star detected.", "%1 stars detected.", count), FITS_MESSAGE); QApplication::restoreOverrideCursor(); } } void FITSView::processPointSelection(int x, int y) { emit trackingStarSelected(x, y); } void FITSView::processMarkerSelection(int x, int y) { markerCrosshair.setX(x); markerCrosshair.setY(y); updateFrame(); } void FITSView::setTrackingBoxEnabled(bool enable) { if (enable != trackingBoxEnabled) { trackingBoxEnabled = enable; //updateFrame(); } } void FITSView::wheelEvent(QWheelEvent * event) { //This attempts to send the wheel event back to the Scroll Area if it was taken from a trackpad //It should still do the zoom if it is a mouse wheel if (event->source() == Qt::MouseEventSynthesizedBySystem) { QScrollArea::wheelEvent(event); } else { QPoint mouseCenter = getImagePoint(event->pos()); if (event->angleDelta().y() > 0) ZoomIn(); else ZoomOut(); event->accept(); cleanUpZoom(mouseCenter); } } /** This method is intended to keep key locations in an image centered on the screen while zooming. If there is a marker or tracking box, it centers on those. If not, it uses the point called viewCenter that was passed as a parameter. */ void FITSView::cleanUpZoom(QPoint viewCenter) { int x0 = 0; int y0 = 0; double scale = (currentZoom / ZOOM_DEFAULT); if (!markerCrosshair.isNull()) { x0 = markerCrosshair.x() * scale; y0 = markerCrosshair.y() * scale; } else if (trackingBoxEnabled) { x0 = trackingBox.center().x() * scale; y0 = trackingBox.center().y() * scale; } else { x0 = viewCenter.x() * scale; y0 = viewCenter.y() * scale; } ensureVisible(x0, y0, width() / 2, height() / 2); updateMouseCursor(); } /** This method converts a point from the ViewPort Coordinate System to the Image Coordinate System. */ QPoint FITSView::getImagePoint(QPoint viewPortPoint) { QWidget * w = widget(); if (w == nullptr) return QPoint(0, 0); double scale = (currentZoom / ZOOM_DEFAULT); QPoint widgetPoint = w->mapFromParent(viewPortPoint); QPoint imagePoint = QPoint(widgetPoint.x() / scale, widgetPoint.y() / scale); return imagePoint; } void FITSView::initDisplayImage() { + if (!rawImage.isNull() && + rawImage.width() == imageData->width() && + rawImage.height() == imageData->height()) + return; + // Account for leftover when sampling. Thus a 5-wide image sampled by 2 // would result in a width of 3 (samples 0, 2 and 4). int w = (imageData->width() + sampling - 1) / sampling; int h = (imageData->height() + sampling - 1) / sampling; if (imageData->channels() == 1) { rawImage = QImage(w, h, QImage::Format_Indexed8); rawImage.setColorCount(256); for (int i = 0; i < 256; i++) rawImage.setColor(i, qRgb(i, i, i)); } else { rawImage = QImage(w, h, QImage::Format_RGB32); } } /** The Following two methods allow gestures to work with trackpads. Specifically, we are targeting the pinch events, so that if one is generated, Then the pinchTriggered method will be called. If the event is not a pinch gesture, then the event is passed back to the other event handlers. */ bool FITSView::event(QEvent * event) { if (event->type() == QEvent::Gesture) return gestureEvent(dynamic_cast(event)); return QScrollArea::event(event); } bool FITSView::gestureEvent(QGestureEvent * event) { if (QGesture * pinch = event->gesture(Qt::PinchGesture)) pinchTriggered(dynamic_cast(pinch)); return true; } /** This Method works with Trackpads to use the pinch gesture to scroll in and out It stores a point to keep track of the location where the gesture started so that while you are zooming, it tries to keep that initial point centered in the view. **/ void FITSView::pinchTriggered(QPinchGesture * gesture) { if (!zooming) { zoomLocation = getImagePoint(mapFromGlobal(QCursor::pos())); zooming = true; } if (gesture->state() == Qt::GestureFinished) { zooming = false; } zoomTime++; //zoomTime is meant to slow down the zooming with a pinch gesture. if (zoomTime > 10000) //This ensures zoomtime never gets too big. zoomTime = 0; if (zooming && (zoomTime % 10 == 0)) //zoomTime is set to slow it by a factor of 10. { if (gesture->totalScaleFactor() > 1) ZoomIn(); else ZoomOut(); } cleanUpZoom(zoomLocation); } /*void FITSView::handleWCSCompletion() { //bool hasWCS = wcsWatcher.result(); if(imageData->hasWCS()) this->updateFrame(); emit wcsToggled(imageData->hasWCS()); }*/ void FITSView::syncWCSState() { bool hasWCS = imageData->hasWCS(); bool wcsLoaded = imageData->isWCSLoaded(); if (hasWCS && wcsLoaded) this->updateFrame(); emit wcsToggled(hasWCS); if (toggleEQGridAction != nullptr) toggleEQGridAction->setEnabled(hasWCS); if (toggleObjectsAction != nullptr) toggleObjectsAction->setEnabled(hasWCS); if (centerTelescopeAction != nullptr) centerTelescopeAction->setEnabled(hasWCS); } void FITSView::createFloatingToolBar() { if (floatingToolBar != nullptr) return; floatingToolBar = new QToolBar(this); auto * eff = new QGraphicsOpacityEffect(this); floatingToolBar->setGraphicsEffect(eff); eff->setOpacity(0.2); floatingToolBar->setVisible(false); floatingToolBar->setStyleSheet( "QToolBar{background: rgba(150, 150, 150, 210); border:none; color: yellow}" "QToolButton{background: transparent; border:none; color: yellow}" "QToolButton:hover{background: rgba(200, 200, 200, 255);border:solid; color: yellow}" "QToolButton:checked{background: rgba(110, 110, 110, 255);border:solid; color: yellow}"); floatingToolBar->setFloatable(true); floatingToolBar->setIconSize(QSize(25, 25)); //floatingToolBar->setMovable(true); QAction * action = nullptr; floatingToolBar->addAction(QIcon::fromTheme("zoom-in"), i18n("Zoom In"), this, SLOT(ZoomIn())); floatingToolBar->addAction(QIcon::fromTheme("zoom-out"), i18n("Zoom Out"), this, SLOT(ZoomOut())); floatingToolBar->addAction(QIcon::fromTheme("zoom-fit-best"), i18n("Default Zoom"), this, SLOT(ZoomDefault())); floatingToolBar->addAction(QIcon::fromTheme("zoom-fit-width"), i18n("Zoom to Fit"), this, SLOT(ZoomToFit())); toggleStretchAction = floatingToolBar->addAction(QIcon::fromTheme("transform-move"), - i18n("Toggle Stretch"), - this, SLOT(toggleStretch())); + i18n("Toggle Stretch"), + this, SLOT(toggleStretch())); toggleStretchAction->setCheckable(true); - + floatingToolBar->addSeparator(); action = floatingToolBar->addAction(QIcon::fromTheme("crosshairs"), i18n("Show Cross Hairs"), this, SLOT(toggleCrosshair())); action->setCheckable(true); action = floatingToolBar->addAction(QIcon::fromTheme("map-flat"), i18n("Show Pixel Gridlines"), this, SLOT(togglePixelGrid())); action->setCheckable(true); toggleStarsAction = floatingToolBar->addAction(QIcon::fromTheme("kstars_stars"), i18n("Detect Stars in Image"), this, SLOT(toggleStars())); toggleStarsAction->setCheckable(true); #ifdef HAVE_DATAVISUALIZATION toggleProfileAction = floatingToolBar->addAction(QIcon::fromTheme("star-profile", QIcon(":/icons/star_profile.svg")), i18n("View Star Profile"), this, SLOT(toggleStarProfile())); toggleProfileAction->setCheckable(true); #endif if (mode == FITS_NORMAL || mode == FITS_ALIGN) { floatingToolBar->addSeparator(); toggleEQGridAction = floatingToolBar->addAction(QIcon::fromTheme("kstars_grid"), i18n("Show Equatorial Gridlines"), this, SLOT(toggleEQGrid())); toggleEQGridAction->setCheckable(true); toggleEQGridAction->setEnabled(false); toggleObjectsAction = floatingToolBar->addAction(QIcon::fromTheme("help-hint"), i18n("Show Objects in Image"), this, SLOT(toggleObjects())); toggleObjectsAction->setCheckable(true); toggleEQGridAction->setEnabled(false); centerTelescopeAction = floatingToolBar->addAction(QIcon::fromTheme("center_telescope", QIcon(":/icons/center_telescope.svg")), i18n("Center Telescope"), this, SLOT(centerTelescope())); centerTelescopeAction->setCheckable(true); centerTelescopeAction->setEnabled(false); } } /** This methood either enables or disables the scope mouse mode so you can slew your scope to coordinates just by clicking the mouse on a spot in the image. */ void FITSView::centerTelescope() { if (imageHasWCS()) { if (getCursorMode() == FITSView::scopeCursor) { setCursorMode(lastMouseMode); } else { lastMouseMode = getCursorMode(); setCursorMode(FITSView::scopeCursor); } updateFrame(); } updateScopeButton(); } void FITSView::updateScopeButton() { if (centerTelescopeAction != nullptr) { if (getCursorMode() == FITSView::scopeCursor) { centerTelescopeAction->setChecked(true); } else { centerTelescopeAction->setChecked(false); } } } /** This method just verifies if INDI is online, a telescope present, and is connected */ bool FITSView::isTelescopeActive() { #ifdef HAVE_INDI if (INDIListener::Instance()->size() == 0) { return false; } foreach (ISD::GDInterface * gd, INDIListener::Instance()->getDevices()) { INDI::BaseDevice * bd = gd->getBaseDevice(); if (gd->getType() != KSTARS_TELESCOPE) continue; if (bd == nullptr) continue; return bd->isConnected(); } return false; #else return false; #endif } void FITSView::setStarsEnabled(bool enable) { markStars = enable; if (floatingToolBar != nullptr) { foreach (QAction * action, floatingToolBar->actions()) { if (action->text() == i18n("Detect Stars in Image")) { action->setChecked(markStars); break; } } } } void FITSView::setStarsHFREnabled(bool enable) { showStarsHFR = enable; } diff --git a/kstars/indi/indiccd.cpp b/kstars/indi/indiccd.cpp index 8a4dee578..1e2b864cd 100644 --- a/kstars/indi/indiccd.cpp +++ b/kstars/indi/indiccd.cpp @@ -1,2530 +1,2531 @@ /* INDI CCD 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 "indiccd.h" #include "config-kstars.h" #include "indi_debug.h" #include "clientmanager.h" #include "driverinfo.h" #include "guimanager.h" #include "kspaths.h" #include "kstars.h" #include "kstarsdata.h" #include "Options.h" #include "streamwg.h" //#include "ekos/manager.h" #ifdef HAVE_CFITSIO #include "fitsviewer/fitsdata.h" #endif #include #include #include #include #include const QStringList RAWFormats = { "cr2", "cr3", "crw", "nef", "raf", "dng", "arw" }; namespace { void addFITSKeywords(const QString &filename, const QString &filter_used) { #ifdef HAVE_CFITSIO int status = 0; if (filter_used.isEmpty() == false) { QString filt(filter_used); QString key_comment("Filter name"); filt.replace(' ', '_'); fitsfile *fptr = nullptr; // Use open diskfile as it does not use extended file names which has problems opening // files with [ ] or ( ) in their names. if (fits_open_diskfile(&fptr, filename.toLatin1(), READONLY, &status)) { fits_report_error(stderr, status); return; } if (fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status)) { fits_report_error(stderr, status); return; } if (fits_update_key_str(fptr, "FILTER", filt.toLatin1().data(), key_comment.toLatin1().data(), &status)) { fits_report_error(stderr, status); return; } fits_close_file(fptr, &status); } #endif } // Internal function to write an image blob to disk. bool WriteImageFileInternal(const QString &filename, char *buffer, const size_t size, bool add_fits_keywords, const QString &filter) { QFile file(filename); if (!file.open(QIODevice::WriteOnly)) { qCCritical(KSTARS_INDI) << "ISD:CCD Error: Unable to open write file: " << filename; return false; } size_t n = 0; QDataStream out(&file); for (size_t nr = 0; nr < size; nr += n) n = out.writeRawData(buffer + nr, size - nr); file.close(); if (add_fits_keywords) addFITSKeywords(filename, filter); return true; } // Internal function to write a temporary file image blob to disk. bool WriteTempImageFile(const QString &format, char * buffer, size_t size, QString *filename) { QTemporaryFile tmpFile(QDir::tempPath() + "/fitsXXXXXX" + format); tmpFile.setAutoRemove(false); if (!tmpFile.open()) { qCCritical(KSTARS_INDI) << "ISD:CCD Error: Unable to open tempfile: " << tmpFile.fileName(); return false; } QDataStream out(&tmpFile); size_t n = 0; for (size_t nr = 0; nr < size; nr += n) n = out.writeRawData(buffer + nr, size - nr); tmpFile.close(); *filename = tmpFile.fileName(); return true; } } namespace ISD { CCDChip::CCDChip(ISD::CCD *ccd, ChipType cType) { baseDevice = ccd->getBaseDevice(); clientManager = ccd->getDriverInfo()->getClientManager(); parentCCD = ccd; type = cType; } FITSView *CCDChip::getImageView(FITSMode imageType) { switch (imageType) { case FITS_NORMAL: return normalImage; case FITS_FOCUS: return focusImage; case FITS_GUIDE: return guideImage; case FITS_CALIBRATE: return calibrationImage; case FITS_ALIGN: return alignImage; } return nullptr; } void CCDChip::setImageView(FITSView *image, FITSMode imageType) { switch (imageType) { case FITS_NORMAL: normalImage = image; break; case FITS_FOCUS: focusImage = image; break; case FITS_GUIDE: guideImage = image; break; case FITS_CALIBRATE: calibrationImage = image; break; case FITS_ALIGN: alignImage = image; break; } if (image) imageData = image->getImageData(); } bool CCDChip::getFrameMinMax(int *minX, int *maxX, int *minY, int *maxY, int *minW, int *maxW, int *minH, int *maxH) { INumberVectorProperty *frameProp = nullptr; switch (type) { case PRIMARY_CCD: frameProp = baseDevice->getNumber("CCD_FRAME"); break; case GUIDE_CCD: frameProp = baseDevice->getNumber("GUIDER_FRAME"); break; } if (frameProp == nullptr) return false; INumber *arg = IUFindNumber(frameProp, "X"); if (arg == nullptr) return false; if (minX) *minX = arg->min; if (maxX) *maxX = arg->max; arg = IUFindNumber(frameProp, "Y"); if (arg == nullptr) return false; if (minY) *minY = arg->min; if (maxY) *maxY = arg->max; arg = IUFindNumber(frameProp, "WIDTH"); if (arg == nullptr) return false; if (minW) *minW = arg->min; if (maxW) *maxW = arg->max; arg = IUFindNumber(frameProp, "HEIGHT"); if (arg == nullptr) return false; if (minH) *minH = arg->min; if (maxH) *maxH = arg->max; return true; } bool CCDChip::setImageInfo(uint16_t width, uint16_t height, double pixelX, double pixelY, uint8_t bitdepth) { INumberVectorProperty *ccdInfoProp = nullptr; switch (type) { case PRIMARY_CCD: ccdInfoProp = baseDevice->getNumber("CCD_INFO"); break; case GUIDE_CCD: ccdInfoProp = baseDevice->getNumber("GUIDER_INFO"); break; } if (ccdInfoProp == nullptr) return false; ccdInfoProp->np[0].value = width; ccdInfoProp->np[1].value = height; ccdInfoProp->np[2].value = std::hypotf(pixelX, pixelY); ccdInfoProp->np[3].value = pixelX; ccdInfoProp->np[4].value = pixelY; ccdInfoProp->np[5].value = bitdepth; clientManager->sendNewNumber(ccdInfoProp); return true; } bool CCDChip::getImageInfo(uint16_t &width, uint16_t &height, double &pixelX, double &pixelY, uint8_t &bitdepth) { INumberVectorProperty *ccdInfoProp = nullptr; switch (type) { case PRIMARY_CCD: ccdInfoProp = baseDevice->getNumber("CCD_INFO"); break; case GUIDE_CCD: ccdInfoProp = baseDevice->getNumber("GUIDER_INFO"); break; } if (ccdInfoProp == nullptr) return false; width = ccdInfoProp->np[0].value; height = ccdInfoProp->np[1].value; pixelX = ccdInfoProp->np[2].value; pixelY = ccdInfoProp->np[3].value; bitdepth = ccdInfoProp->np[5].value; return true; } bool CCDChip::getBayerInfo(uint16_t &offsetX, uint16_t &offsetY, QString &pattern) { ITextVectorProperty * bayerTP = baseDevice->getText("CCD_CFA"); if (!bayerTP) return false; offsetX = QString(bayerTP->tp[0].text).toInt(); offsetY = QString(bayerTP->tp[1].text).toInt(); pattern = QString(bayerTP->tp[2].text); return true; } bool CCDChip::getFrame(int *x, int *y, int *w, int *h) { INumberVectorProperty *frameProp = nullptr; switch (type) { case PRIMARY_CCD: frameProp = baseDevice->getNumber("CCD_FRAME"); break; case GUIDE_CCD: frameProp = baseDevice->getNumber("GUIDER_FRAME"); break; } if (frameProp == nullptr) return false; INumber *arg = IUFindNumber(frameProp, "X"); if (arg == nullptr) return false; *x = arg->value; arg = IUFindNumber(frameProp, "Y"); if (arg == nullptr) return false; *y = arg->value; arg = IUFindNumber(frameProp, "WIDTH"); if (arg == nullptr) return false; *w = arg->value; arg = IUFindNumber(frameProp, "HEIGHT"); if (arg == nullptr) return false; *h = arg->value; return true; } bool CCDChip::resetFrame() { INumberVectorProperty *frameProp = nullptr; switch (type) { case PRIMARY_CCD: frameProp = baseDevice->getNumber("CCD_FRAME"); break; case GUIDE_CCD: frameProp = baseDevice->getNumber("GUIDER_FRAME"); break; } if (frameProp == nullptr) return false; INumber *xarg = IUFindNumber(frameProp, "X"); INumber *yarg = IUFindNumber(frameProp, "Y"); INumber *warg = IUFindNumber(frameProp, "WIDTH"); INumber *harg = IUFindNumber(frameProp, "HEIGHT"); if (xarg && yarg && warg && harg) { if (xarg->value == xarg->min && yarg->value == yarg->min && warg->value == warg->max && harg->value == harg->max) return false; xarg->value = xarg->min; yarg->value = yarg->min; warg->value = warg->max; harg->value = harg->max; clientManager->sendNewNumber(frameProp); return true; } return false; } bool CCDChip::setFrame(int x, int y, int w, int h, bool force) { INumberVectorProperty *frameProp = nullptr; switch (type) { case PRIMARY_CCD: frameProp = baseDevice->getNumber("CCD_FRAME"); break; case GUIDE_CCD: frameProp = baseDevice->getNumber("GUIDER_FRAME"); break; } if (frameProp == nullptr) return false; INumber *xarg = IUFindNumber(frameProp, "X"); INumber *yarg = IUFindNumber(frameProp, "Y"); INumber *warg = IUFindNumber(frameProp, "WIDTH"); INumber *harg = IUFindNumber(frameProp, "HEIGHT"); if (xarg && yarg && warg && harg) { if (!force && xarg->value == x && yarg->value == y && warg->value == w && harg->value == h) return true; xarg->value = x; yarg->value = y; warg->value = w; harg->value = h; clientManager->sendNewNumber(frameProp); return true; } return false; } bool CCDChip::capture(double exposure) { //qCDebug(KSTARS_INDI) << "IndiCCD: capture()" << (type==PRIMARY_CCD?"CCD":"Guide"); INumberVectorProperty *expProp = nullptr; switch (type) { case PRIMARY_CCD: expProp = baseDevice->getNumber("CCD_EXPOSURE"); break; case GUIDE_CCD: expProp = baseDevice->getNumber("GUIDER_EXPOSURE"); break; } if (expProp == nullptr) return false; // If we have exposure presets, let's limit the exposure value // to the preset values if it falls within their range of max/min if (Options::forceDSLRPresets()) { QMap exposurePresets = parentCCD->getExposurePresets(); if (!exposurePresets.isEmpty()) { double min, max; QPair minmax = parentCCD->getExposurePresetsMinMax(); min = minmax.first; max = minmax.second; if (exposure > min && exposure < max) { double diff = 1e6; double closestMatch = exposure; for (auto oneValue : exposurePresets.values()) { double newDiff = std::fabs(exposure - oneValue); if (newDiff < diff) { closestMatch = oneValue; diff = newDiff; } } qCDebug(KSTARS_INDI) << "Requested exposure" << exposure << "closes match is" << closestMatch; exposure = closestMatch; } } } // clone the INumberVectorProperty, to avoid modifications to the same // property from two threads INumber n; strcpy(n.name, expProp->np[0].name); n.value = exposure; std::unique_ptr newExpProp(new INumberVectorProperty()); strncpy(newExpProp->device, expProp->device, MAXINDIDEVICE); strncpy(newExpProp->name, expProp->name, MAXINDINAME); strncpy(newExpProp->label, expProp->label, MAXINDILABEL); newExpProp->np = &n; newExpProp->nnp = 1; clientManager->sendNewNumber(newExpProp.get()); return true; } bool CCDChip::abortExposure() { ISwitchVectorProperty *abortProp = nullptr; switch (type) { case PRIMARY_CCD: abortProp = baseDevice->getSwitch("CCD_ABORT_EXPOSURE"); break; case GUIDE_CCD: abortProp = baseDevice->getSwitch("GUIDER_ABORT_EXPOSURE"); break; } if (abortProp == nullptr) return false; ISwitch *abort = IUFindSwitch(abortProp, "ABORT"); if (abort == nullptr) return false; abort->s = ISS_ON; clientManager->sendNewSwitch(abortProp); return true; } bool CCDChip::canBin() const { return CanBin; } void CCDChip::setCanBin(bool value) { CanBin = value; } bool CCDChip::canSubframe() const { return CanSubframe; } void CCDChip::setCanSubframe(bool value) { CanSubframe = value; } bool CCDChip::canAbort() const { return CanAbort; } void CCDChip::setCanAbort(bool value) { CanAbort = value; } FITSData *CCDChip::getImageData() const { return imageData; } int CCDChip::getISOIndex() const { ISwitchVectorProperty *isoProp = baseDevice->getSwitch("CCD_ISO"); if (isoProp == nullptr) return -1; return IUFindOnSwitchIndex(isoProp); } bool CCDChip::setISOIndex(int value) { ISwitchVectorProperty *isoProp = baseDevice->getSwitch("CCD_ISO"); if (isoProp == nullptr) return false; IUResetSwitch(isoProp); isoProp->sp[value].s = ISS_ON; clientManager->sendNewSwitch(isoProp); return true; } QStringList CCDChip::getISOList() const { QStringList isoList; ISwitchVectorProperty *isoProp = baseDevice->getSwitch("CCD_ISO"); if (isoProp == nullptr) return isoList; for (int i = 0; i < isoProp->nsp; i++) isoList << isoProp->sp[i].label; return isoList; } bool CCDChip::isCapturing() { INumberVectorProperty *expProp = nullptr; switch (type) { case PRIMARY_CCD: expProp = baseDevice->getNumber("CCD_EXPOSURE"); break; case GUIDE_CCD: expProp = baseDevice->getNumber("GUIDER_EXPOSURE"); break; } if (expProp == nullptr) return false; return (expProp->s == IPS_BUSY); } bool CCDChip::setFrameType(const QString &name) { CCDFrameType fType = FRAME_LIGHT; if (name == "FRAME_LIGHT" || name == "Light") fType = FRAME_LIGHT; else if (name == "FRAME_DARK" || name == "Dark") fType = FRAME_DARK; else if (name == "FRAME_BIAS" || name == "Bias") fType = FRAME_BIAS; else if (name == "FRAME_FLAT" || name == "Flat") fType = FRAME_FLAT; else { qCWarning(KSTARS_INDI) << name << " frame type is unknown." ; return false; } return setFrameType(fType); } bool CCDChip::setFrameType(CCDFrameType fType) { ISwitchVectorProperty *frameProp = nullptr; if (type == PRIMARY_CCD) frameProp = baseDevice->getSwitch("CCD_FRAME_TYPE"); else frameProp = baseDevice->getSwitch("GUIDER_FRAME_TYPE"); if (frameProp == nullptr) return false; ISwitch *ccdFrame = nullptr; if (fType == FRAME_LIGHT) ccdFrame = IUFindSwitch(frameProp, "FRAME_LIGHT"); else if (fType == FRAME_DARK) ccdFrame = IUFindSwitch(frameProp, "FRAME_DARK"); else if (fType == FRAME_BIAS) ccdFrame = IUFindSwitch(frameProp, "FRAME_BIAS"); else if (fType == FRAME_FLAT) ccdFrame = IUFindSwitch(frameProp, "FRAME_FLAT"); if (ccdFrame == nullptr) return false; if (ccdFrame->s == ISS_ON) return true; if (fType != FRAME_LIGHT) captureMode = FITS_CALIBRATE; IUResetSwitch(frameProp); ccdFrame->s = ISS_ON; clientManager->sendNewSwitch(frameProp); return true; } CCDFrameType CCDChip::getFrameType() { CCDFrameType fType = FRAME_LIGHT; ISwitchVectorProperty *frameProp = nullptr; if (type == PRIMARY_CCD) frameProp = baseDevice->getSwitch("CCD_FRAME_TYPE"); else frameProp = baseDevice->getSwitch("GUIDER_FRAME_TYPE"); if (frameProp == nullptr) return fType; ISwitch *ccdFrame = nullptr; ccdFrame = IUFindOnSwitch(frameProp); if (ccdFrame == nullptr) { qCWarning(KSTARS_INDI) << "ISD:CCD Cannot find active frame in CCD!"; return fType; } if (!strcmp(ccdFrame->name, "FRAME_LIGHT")) fType = FRAME_LIGHT; else if (!strcmp(ccdFrame->name, "FRAME_DARK")) fType = FRAME_DARK; else if (!strcmp(ccdFrame->name, "FRAME_FLAT")) fType = FRAME_FLAT; else if (!strcmp(ccdFrame->name, "FRAME_BIAS")) fType = FRAME_BIAS; return fType; } bool CCDChip::setBinning(CCDBinType binType) { switch (binType) { case SINGLE_BIN: return setBinning(1, 1); case DOUBLE_BIN: return setBinning(2, 2); case TRIPLE_BIN: return setBinning(3, 3); case QUADRAPLE_BIN: return setBinning(4, 4); } return false; } CCDBinType CCDChip::getBinning() { CCDBinType binType = SINGLE_BIN; INumberVectorProperty *binProp = nullptr; switch (type) { case PRIMARY_CCD: binProp = baseDevice->getNumber("CCD_BINNING"); break; case GUIDE_CCD: binProp = baseDevice->getNumber("GUIDER_BINNING"); break; } if (binProp == nullptr) return binType; INumber *horBin = nullptr, *verBin = nullptr; horBin = IUFindNumber(binProp, "HOR_BIN"); verBin = IUFindNumber(binProp, "VER_BIN"); if (!horBin || !verBin) return binType; switch (static_cast(horBin->value)) { case 2: binType = DOUBLE_BIN; break; case 3: binType = TRIPLE_BIN; break; case 4: binType = QUADRAPLE_BIN; break; default: break; } return binType; } bool CCDChip::getBinning(int *bin_x, int *bin_y) { INumberVectorProperty *binProp = nullptr; *bin_x = *bin_y = 1; switch (type) { case PRIMARY_CCD: binProp = baseDevice->getNumber("CCD_BINNING"); break; case GUIDE_CCD: binProp = baseDevice->getNumber("GUIDER_BINNING"); break; } if (binProp == nullptr) return false; INumber *horBin = nullptr, *verBin = nullptr; horBin = IUFindNumber(binProp, "HOR_BIN"); verBin = IUFindNumber(binProp, "VER_BIN"); if (!horBin || !verBin) return false; *bin_x = horBin->value; *bin_y = verBin->value; return true; } bool CCDChip::getMaxBin(int *max_xbin, int *max_ybin) { if (!max_xbin || !max_ybin) return false; INumberVectorProperty *binProp = nullptr; *max_xbin = *max_ybin = 1; switch (type) { case PRIMARY_CCD: binProp = baseDevice->getNumber("CCD_BINNING"); break; case GUIDE_CCD: binProp = baseDevice->getNumber("GUIDER_BINNING"); break; } if (binProp == nullptr) return false; INumber *horBin = nullptr, *verBin = nullptr; horBin = IUFindNumber(binProp, "HOR_BIN"); verBin = IUFindNumber(binProp, "VER_BIN"); if (!horBin || !verBin) return false; *max_xbin = horBin->max; *max_ybin = verBin->max; return true; } bool CCDChip::setBinning(int bin_x, int bin_y) { INumberVectorProperty *binProp = nullptr; switch (type) { case PRIMARY_CCD: binProp = baseDevice->getNumber("CCD_BINNING"); break; case GUIDE_CCD: binProp = baseDevice->getNumber("GUIDER_BINNING"); break; } if (binProp == nullptr) return false; INumber *horBin = nullptr, *verBin = nullptr; horBin = IUFindNumber(binProp, "HOR_BIN"); verBin = IUFindNumber(binProp, "VER_BIN"); if (!horBin || !verBin) return false; if (horBin->value == bin_x && verBin->value == bin_y) return true; if (bin_x > horBin->max || bin_y > verBin->max) return false; horBin->value = bin_x; verBin->value = bin_y; clientManager->sendNewNumber(binProp); return true; } CCD::CCD(GDInterface *iPtr) : DeviceDecorator(iPtr) { primaryChip.reset(new CCDChip(this, CCDChip::PRIMARY_CCD)); readyTimer.reset(new QTimer()); readyTimer.get()->setInterval(250); readyTimer.get()->setSingleShot(true); connect(readyTimer.get(), &QTimer::timeout, this, &CCD::ready); m_Media.reset(new WSMedia(this)); connect(m_Media.get(), &WSMedia::newFile, this, &CCD::setWSBLOB); connect(clientManager, &ClientManager::newBLOBManager, this, &CCD::setBLOBManager, Qt::UniqueConnection); m_LastNotificationTS = QDateTime::currentDateTime(); } CCD::~CCD() { if (m_ImageViewerWindow) m_ImageViewerWindow->close(); if (fileWriteThread.isRunning()) fileWriteThread.waitForFinished(); if (fileWriteBuffer != nullptr) - delete fileWriteBuffer; + delete [] fileWriteBuffer; } void CCD::setBLOBManager(const char *device, INDI::Property *prop) { if (!strcmp(device, getDeviceName())) emit newBLOBManager(prop); } void CCD::registerProperty(INDI::Property *prop) { if (isConnected()) readyTimer.get()->start(); if (!strcmp(prop->getName(), "GUIDER_EXPOSURE")) { HasGuideHead = true; guideChip.reset(new CCDChip(this, CCDChip::GUIDE_CCD)); } else if (!strcmp(prop->getName(), "CCD_FRAME_TYPE")) { ISwitchVectorProperty *ccdFrame = prop->getSwitch(); primaryChip->clearFrameTypes(); for (int i = 0; i < ccdFrame->nsp; i++) primaryChip->addFrameLabel(ccdFrame->sp[i].label); } else if (!strcmp(prop->getName(), "CCD_FRAME")) { INumberVectorProperty *np = prop->getNumber(); if (np && np->p != IP_RO) primaryChip->setCanSubframe(true); } else if (!strcmp(prop->getName(), "GUIDER_FRAME")) { INumberVectorProperty *np = prop->getNumber(); if (np && np->p != IP_RO) guideChip->setCanSubframe(true); } else if (!strcmp(prop->getName(), "CCD_BINNING")) { INumberVectorProperty *np = prop->getNumber(); if (np && np->p != IP_RO) primaryChip->setCanBin(true); } else if (!strcmp(prop->getName(), "GUIDER_BINNING")) { INumberVectorProperty *np = prop->getNumber(); if (np && np->p != IP_RO) guideChip->setCanBin(true); } else if (!strcmp(prop->getName(), "CCD_ABORT_EXPOSURE")) { ISwitchVectorProperty *sp = prop->getSwitch(); if (sp && sp->p != IP_RO) primaryChip->setCanAbort(true); } else if (!strcmp(prop->getName(), "GUIDER_ABORT_EXPOSURE")) { ISwitchVectorProperty *sp = prop->getSwitch(); if (sp && sp->p != IP_RO) guideChip->setCanAbort(true); } else if (!strcmp(prop->getName(), "CCD_TEMPERATURE")) { INumberVectorProperty *np = prop->getNumber(); HasCooler = true; CanCool = (np->p != IP_RO); if (np) emit newTemperatureValue(np->np[0].value); } else if (!strcmp(prop->getName(), "CCD_COOLER")) { // Can turn cooling on/off HasCoolerControl = true; } else if (!strcmp(prop->getName(), "CCD_VIDEO_STREAM")) { // Has Video Stream HasVideoStream = true; } else if (!strcmp(prop->getName(), "CCD_TRANSFER_FORMAT")) { ISwitchVectorProperty *sp = prop->getSwitch(); if (sp) { ISwitch *format = IUFindSwitch(sp, "FORMAT_NATIVE"); if (format && format->s == ISS_ON) transferFormat = FORMAT_NATIVE; else transferFormat = FORMAT_FITS; } } else if (!strcmp(prop->getName(), "CCD_EXPOSURE_PRESETS")) { ISwitchVectorProperty *svp = prop->getSwitch(); if (svp) { bool ok = false; for (int i = 0; i < svp->nsp; i++) { QString key = QString(svp->sp[i].label); double value = key.toDouble(&ok); if (!ok) { QStringList parts = key.split("/"); if (parts.count() == 2) { bool numOk = false, denOk = false; double numerator = parts[0].toDouble(&numOk); double denominator = parts[1].toDouble(&denOk); if (numOk && denOk && denominator > 0) { ok = true; value = numerator / denominator; } } } if (ok) m_ExposurePresets.insert(key, value); double min = 1e6, max = 1e-6; for (auto oneValue : m_ExposurePresets.values()) { if (oneValue < min) min = oneValue; if (oneValue > max) max = oneValue; } m_ExposurePresetsMinMax = qMakePair(min, max); } } } else if (!strcmp(prop->getName(), "CCD_EXPOSURE_LOOP")) { ISwitchVectorProperty *sp = prop->getSwitch(); if (sp) { ISwitch *looping = IUFindSwitch(sp, "LOOP_ON"); if (looping && looping->s == ISS_ON) IsLooping = true; else IsLooping = false; } } else if (!strcmp(prop->getName(), "TELESCOPE_TYPE")) { ISwitchVectorProperty *sp = prop->getSwitch(); if (sp) { ISwitch *format = IUFindSwitch(sp, "TELESCOPE_PRIMARY"); if (format && format->s == ISS_ON) telescopeType = TELESCOPE_PRIMARY; else telescopeType = TELESCOPE_GUIDE; } } else if (!strcmp(prop->getName(), "CCD_WEBSOCKET_SETTINGS")) { INumberVectorProperty *np = prop->getNumber(); m_Media->setURL(QUrl(QString("ws://%1:%2").arg(clientManager->getHost()).arg(np->np[0].value))); m_Media->connectServer(); } else if (!strcmp(prop->getName(), "CCD1")) { IBLOBVectorProperty *bp = prop->getBLOB(); primaryCCDBLOB = bp->bp; primaryCCDBLOB->bvp = bp; } // try to find gain property, if any else if (gainN == nullptr && prop->getType() == INDI_NUMBER) { // Since gain is spread among multiple property depending on the camera providing it // we need to search in all possible number properties INumberVectorProperty *gainNP = prop->getNumber(); if (gainNP) { for (int i = 0; i < gainNP->nnp; i++) { QString name = QString(gainNP->np[i].name).toLower(); QString label = QString(gainNP->np[i].label).toLower(); if (name == "gain" || label == "gain") { gainN = gainNP->np + i; gainPerm = gainNP->p; break; } } } } DeviceDecorator::registerProperty(prop); } void CCD::removeProperty(INDI::Property *prop) { if (!strcmp(prop->getName(), "CCD_WEBSOCKET_SETTINGS")) { m_Media->disconnectServer(); } DeviceDecorator::removeProperty(prop); } void CCD::processLight(ILightVectorProperty *lvp) { DeviceDecorator::processLight(lvp); } void CCD::processNumber(INumberVectorProperty *nvp) { if (!strcmp(nvp->name, "CCD_EXPOSURE")) { INumber *np = IUFindNumber(nvp, "CCD_EXPOSURE_VALUE"); if (np) emit newExposureValue(primaryChip.get(), np->value, nvp->s); if (nvp->s == IPS_ALERT) emit captureFailed(); } else if (!strcmp(nvp->name, "CCD_TEMPERATURE")) { HasCooler = true; INumber *np = IUFindNumber(nvp, "CCD_TEMPERATURE_VALUE"); if (np) emit newTemperatureValue(np->value); } else if (!strcmp(nvp->name, "GUIDER_EXPOSURE")) { INumber *np = IUFindNumber(nvp, "GUIDER_EXPOSURE_VALUE"); if (np) emit newExposureValue(guideChip.get(), np->value, nvp->s); } else if (!strcmp(nvp->name, "FPS")) { emit newFPS(nvp->np[0].value, nvp->np[1].value); } else if (!strcmp(nvp->name, "CCD_RAPID_GUIDE_DATA")) { double dx = -1, dy = -1, fit = -1; INumber *np = nullptr; if (nvp->s == IPS_ALERT) { emit newGuideStarData(primaryChip.get(), -1, -1, -1); } else { np = IUFindNumber(nvp, "GUIDESTAR_X"); if (np) dx = np->value; np = IUFindNumber(nvp, "GUIDESTAR_Y"); if (np) dy = np->value; np = IUFindNumber(nvp, "GUIDESTAR_FIT"); if (np) fit = np->value; if (dx >= 0 && dy >= 0 && fit >= 0) emit newGuideStarData(primaryChip.get(), dx, dy, fit); } } else if (!strcmp(nvp->name, "GUIDER_RAPID_GUIDE_DATA")) { double dx = -1, dy = -1, fit = -1; INumber *np = nullptr; if (nvp->s == IPS_ALERT) { emit newGuideStarData(guideChip.get(), -1, -1, -1); } else { np = IUFindNumber(nvp, "GUIDESTAR_X"); if (np) dx = np->value; np = IUFindNumber(nvp, "GUIDESTAR_Y"); if (np) dy = np->value; np = IUFindNumber(nvp, "GUIDESTAR_FIT"); if (np) fit = np->value; if (dx >= 0 && dy >= 0 && fit >= 0) emit newGuideStarData(guideChip.get(), dx, dy, fit); } } DeviceDecorator::processNumber(nvp); } void CCD::processSwitch(ISwitchVectorProperty *svp) { if (!strcmp(svp->name, "CCD_COOLER")) { // Can turn cooling on/off HasCoolerControl = true; emit coolerToggled(svp->sp[0].s == ISS_ON); } else if (QString(svp->name).endsWith("VIDEO_STREAM")) { // If BLOB is not enabled for this camera, then ignore all VIDEO_STREAM calls. if (isBLOBEnabled() == false) return; HasVideoStream = true; if (streamWindow.get() == nullptr && svp->sp[0].s == ISS_ON) { streamWindow.reset(new StreamWG(this)); INumberVectorProperty *streamFrame = baseDevice->getNumber("CCD_STREAM_FRAME"); INumber *w = nullptr, *h = nullptr; if (streamFrame) { w = IUFindNumber(streamFrame, "WIDTH"); h = IUFindNumber(streamFrame, "HEIGHT"); } if (w && h) { streamW = w->value; streamH = h->value; } else { // Only use CCD dimensions if we are receiving raw stream and not stream of images (i.e. mjpeg..etc) IBLOBVectorProperty *rawBP = baseDevice->getBLOB("CCD1"); if (rawBP) { int x = 0, y = 0, w = 0, h = 0; int binx = 0, biny = 0; primaryChip->getFrame(&x, &y, &w, &h); primaryChip->getBinning(&binx, &biny); streamW = w / binx; streamH = h / biny; } } streamWindow->setSize(streamW, streamH); } if (streamWindow.get() != nullptr) { connect(streamWindow.get(), &StreamWG::hidden, this, &CCD::StreamWindowHidden, Qt::UniqueConnection); connect(streamWindow.get(), &StreamWG::imageChanged, this, &CCD::newVideoFrame, Qt::UniqueConnection); streamWindow->enableStream(svp->sp[0].s == ISS_ON); emit videoStreamToggled(svp->sp[0].s == ISS_ON); } } else if (!strcmp(svp->name, "CCD_TRANSFER_FORMAT")) { ISwitch *format = IUFindSwitch(svp, "FORMAT_NATIVE"); if (format && format->s == ISS_ON) transferFormat = FORMAT_NATIVE; else transferFormat = FORMAT_FITS; } else if (!strcmp(svp->name, "RECORD_STREAM")) { ISwitch *recordOFF = IUFindSwitch(svp, "RECORD_OFF"); if (recordOFF && recordOFF->s == ISS_ON) { emit videoRecordToggled(false); KNotification::event(QLatin1String("RecordingStopped"), i18n("Video Recording Stopped")); } else { emit videoRecordToggled(true); KNotification::event(QLatin1String("RecordingStarted"), i18n("Video Recording Started")); } } else if (!strcmp(svp->name, "TELESCOPE_TYPE")) { ISwitch *format = IUFindSwitch(svp, "TELESCOPE_PRIMARY"); if (format && format->s == ISS_ON) telescopeType = TELESCOPE_PRIMARY; else telescopeType = TELESCOPE_GUIDE; } else if (!strcmp(svp->name, "CCD_EXPOSURE_LOOP")) { ISwitch *looping = IUFindSwitch(svp, "LOOP_ON"); if (looping && looping->s == ISS_ON) IsLooping = true; else IsLooping = false; } else if (!strcmp(svp->name, "CONNECTION")) { ISwitch *dSwitch = IUFindSwitch(svp, "DISCONNECT"); if (dSwitch && dSwitch->s == ISS_ON && streamWindow.get() != nullptr) { streamWindow->enableStream(false); emit videoStreamToggled(false); streamWindow->close(); streamWindow.reset(); } //emit switchUpdated(svp); //return; } DeviceDecorator::processSwitch(svp); } void CCD::processText(ITextVectorProperty *tvp) { if (!strcmp(tvp->name, "CCD_FILE_PATH")) { IText *filepath = IUFindText(tvp, "FILE_PATH"); if (filepath) emit newRemoteFile(QString(filepath->text)); } DeviceDecorator::processText(tvp); } void CCD::setWSBLOB(const QByteArray &message, const QString &extension) { if (!primaryCCDBLOB) return; primaryCCDBLOB->blob = const_cast(message.data()); primaryCCDBLOB->size = message.size(); strncpy(primaryCCDBLOB->format, extension.toLatin1().constData(), MAXINDIFORMAT); processBLOB(primaryCCDBLOB); // Disassociate primaryCCDBLOB->blob = nullptr; } void CCD::processStream(IBLOB *bp) { if (streamWindow->isStreamEnabled() == false) return; INumberVectorProperty *streamFrame = baseDevice->getNumber("CCD_STREAM_FRAME"); INumber *w = nullptr, *h = nullptr; if (streamFrame) { w = IUFindNumber(streamFrame, "WIDTH"); h = IUFindNumber(streamFrame, "HEIGHT"); } if (w && h) { streamW = w->value; streamH = h->value; } else { int x, y, w, h; int binx, biny; primaryChip->getFrame(&x, &y, &w, &h); primaryChip->getBinning(&binx, &biny); streamW = w / binx; streamH = h / biny; } streamWindow->setSize(streamW, streamH); streamWindow->show(); streamWindow->newFrame(bp); } bool CCD::generateFilename(const QString &format, bool batch_mode, QString *filename) { QString currentDir; if (batch_mode) currentDir = fitsDir.isEmpty() ? Options::fitsDir() : fitsDir; else currentDir = KSPaths::writableLocation(QStandardPaths::TempLocation); if (QDir(currentDir).exists() == false) QDir().mkpath(currentDir); if (currentDir.endsWith('/') == false) currentDir.append('/'); // 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 ts = QDateTime::currentDateTime().toString("yyyy-MM-ddThh-mm-ss"); if (seqPrefix.contains("_ISO8601")) { QString finalPrefix = seqPrefix; finalPrefix.replace("ISO8601", ts); *filename = currentDir + finalPrefix + QString("_%1%2").arg(QString().sprintf("%03d", nextSequenceID), format); } else *filename = currentDir + seqPrefix + (seqPrefix.isEmpty() ? "" : "_") + QString("%1%2").arg(QString().sprintf("%03d", nextSequenceID), format); QFile test_file(*filename); if (!test_file.open(QIODevice::WriteOnly)) { qCCritical(KSTARS_INDI) << "ISD:CCD Error: Unable to open " << test_file.fileName(); return false; } test_file.close(); return true; } bool CCD::WriteImageFile(IBLOB *bp, const QString &format, bool is_fits, bool batch_mode, QString *filename) { if (!generateFilename(format, batch_mode, filename)) return false; // TODO: Not yet threading the writes for non-fits files. // Would need to deal with the raw conversion, etc. if (is_fits) { // Check if the last write is still ongoing, and if so wait. // It is using the fileWriteBuffer. if (fileWriteThread.isRunning()) { fileWriteThread.waitForFinished(); } // Will write blob data in a separate thread, and can't depend on the blob // memory, so copy it first. // Check buffer size. if (fileWriteBufferSize < bp->size) { - if (fileWriteBuffer != nullptr) delete fileWriteBuffer; + if (fileWriteBuffer != nullptr) + delete [] fileWriteBuffer; fileWriteBufferSize = bp->size; fileWriteBuffer = new char[fileWriteBufferSize]; } // Copy memory, and write file on a separate thread. // Probably too late to return an error if the file couldn't write. memcpy(fileWriteBuffer, bp->blob, bp->size); fileWriteThread = QtConcurrent::run(WriteImageFileInternal, *filename, fileWriteBuffer, bp->size, is_fits, filter); filter = ""; } else { if (!WriteImageFileInternal(*filename, static_cast(bp->blob), bp->size, false, filter)) return false; } return true; } void CCD::setupFITSViewerWindows() { normalTabID = calibrationTabID = focusTabID = guideTabID = alignTabID = -1; if (Options::singleWindowCapturedFITS()) m_FITSViewerWindows = KStars::Instance()->genericFITSViewer(); else { m_FITSViewerWindows = new FITSViewer(Options::independentWindowFITS() ? nullptr : KStars::Instance()); KStars::Instance()->addFITSViewer(m_FITSViewerWindows); } connect(m_FITSViewerWindows, &FITSViewer::closed, [&](int tabIndex) { if (tabIndex == normalTabID) normalTabID = -1; else if (tabIndex == calibrationTabID) calibrationTabID = -1; else if (tabIndex == focusTabID) focusTabID = -1; else if (tabIndex == guideTabID) guideTabID = -1; else if (tabIndex == alignTabID) alignTabID = -1; }); } void CCD::processBLOB(IBLOB *bp) { // Ignore write-only BLOBs since we only receive it for state-change if (bp->bvp->p == IP_WO || bp->size == 0) return; BType = BLOB_OTHER; QString format = QString(bp->format).toLower(); // If stream, process it first if (format.contains("stream") && streamWindow.get() != nullptr) { processStream(bp); return; } // Format without leading . (.jpg --> jpg) QString shortFormat = format.mid(1); // If it's not FITS or an image, don't process it. if ((QImageReader::supportedImageFormats().contains(shortFormat.toLatin1()))) BType = BLOB_IMAGE; else if (format.contains("fits")) BType = BLOB_FITS; else if (RAWFormats.contains(shortFormat)) BType = BLOB_RAW; if (BType == BLOB_OTHER) { DeviceDecorator::processBLOB(bp); return; } CCDChip *targetChip = nullptr; if (!strcmp(bp->name, "CCD2")) targetChip = guideChip.get(); else targetChip = primaryChip.get(); //qCDebug(KSTARS_INDI) << "processBLOB() mode " << targetChip->getCaptureMode(); // Create temporary name if ANY of the following conditions are met: // 1. file is preview or batch mode is not enabled // 2. file type is not FITS_NORMAL (focus, guide..etc) QString filename; if (targetChip->isBatchMode() == false || targetChip->getCaptureMode() != FITS_NORMAL) { if (!WriteTempImageFile(format, static_cast(bp->blob), bp->size, &filename)) { emit BLOBUpdated(nullptr); return; } if (BType == BLOB_FITS) addFITSKeywords(filename, filter); } // Create file name for others else { if (!WriteImageFile(bp, format, BType == BLOB_FITS, targetChip->isBatchMode(), &filename)) { emit BLOBUpdated(nullptr); return; } } // store file name strncpy(BLOBFilename, filename.toLatin1(), MAXINDIFILENAME); bp->aux0 = targetChip; bp->aux1 = &BType; bp->aux2 = BLOBFilename; if (targetChip->getCaptureMode() == FITS_NORMAL && targetChip->isBatchMode() == true) { KStars::Instance()->statusBar()->showMessage(i18n("%1 file saved to %2", shortFormat.toUpper(), filename), 0); qCInfo(KSTARS_INDI) << shortFormat.toUpper() << "file saved to" << filename; } // Don't spam, just one notification per 3 seconds if (QDateTime::currentDateTime().secsTo(m_LastNotificationTS) <= -3) { KNotification::event(QLatin1String("FITSReceived"), i18n("Image file is received")); m_LastNotificationTS = QDateTime::currentDateTime(); } // Check if we need to process RAW or regular image. Anything but FITS. if (BType == BLOB_IMAGE || BType == BLOB_RAW) { bool useFITSViewer = Options::autoImageToFITS() && (Options::useFITSViewer() || (Options::useDSLRImageViewer() == false && targetChip->isBatchMode() == false)); bool useDSLRViewer = (Options::useDSLRImageViewer() || targetChip->isBatchMode() == false); // For raw image, we only process them to JPG if we need to open them in the image viewer if (BType == BLOB_RAW && (useFITSViewer || useDSLRViewer)) { QString rawFileName = filename; rawFileName = rawFileName.remove(0, rawFileName.lastIndexOf(QLatin1String("/"))); QString templateName = QString("%1/%2.XXXXXX").arg(QDir::tempPath(), rawFileName); QTemporaryFile imgPreview(templateName); imgPreview.setAutoRemove(false); imgPreview.open(); imgPreview.close(); QString preview_filename = imgPreview.fileName(); QString errorMessage; if (KSUtils::RAWToJPEG(filename, preview_filename, errorMessage) == false) { KStars::Instance()->statusBar()->showMessage(errorMessage); emit BLOBUpdated(bp); return; } // Remove tempeorary CR2 files if (targetChip->isBatchMode() == false) QFile::remove(filename); filename = preview_filename; format = ".jpg"; shortFormat = "jpg"; } // Convert to FITS if checked. QString output; if (useFITSViewer && (FITSData::ImageToFITS(filename, shortFormat, output))) { if (BType == BLOB_RAW || targetChip->isBatchMode() == false) QFile::remove(filename); filename = output; BType = BLOB_FITS; emit previewFITSGenerated(output); } else if (useDSLRViewer) { if (m_ImageViewerWindow.isNull()) m_ImageViewerWindow = new ImageViewer(getDeviceName(), KStars::Instance()); m_ImageViewerWindow->loadImage(filename); emit previewJPEGGenerated(filename, m_ImageViewerWindow->metadata()); } } // Unless we have cfitsio, we're done. #ifdef HAVE_CFITSIO if (BType == BLOB_FITS) { FITSMode captureMode = targetChip->getCaptureMode(); // Get or Create FITSViewer if we are using FITSViewer // or if capture mode is calibrate since for now we are forced to open the file in the viewer // this should be fixed in the future and should only use FITSData if (Options::useFITSViewer() || targetChip->isBatchMode() == false) { if (m_FITSViewerWindows.isNull() && (captureMode == FITS_NORMAL || captureMode == FITS_CALIBRATE)) setupFITSViewerWindows(); } FITSData *blob_fits_data = new FITSData(targetChip->getCaptureMode()); if (!blob_fits_data->loadFITSFromMemory(filename, bp->blob, bp->size, false)) { // If reading the blob fails, we treat it the same as exposure failure // and recapture again if possible qCCritical(KSTARS_INDI) << "failed reading FITS memory buffer"; emit newExposureValue(targetChip, 0, IPS_ALERT); return; } switch (captureMode) { case FITS_NORMAL: case FITS_CALIBRATE: { // Check if we need to display the image if (Options::useFITSViewer() || targetChip->isBatchMode() == false) { bool success; int tabIndex; int *tabID = (captureMode == FITS_NORMAL) ? &normalTabID : &calibrationTabID; QUrl fileURL = QUrl::fromLocalFile(filename); FITSScale captureFilter = targetChip->getCaptureFilter(); if (*tabID == -1 || Options::singlePreviewFITS() == false) { // If image is preview and we should display all captured images in a // single tab called "Preview", then set the title to "Preview", // Otherwise, the title will be the captured image name QString previewTitle; if (targetChip->isBatchMode() == false && Options::singlePreviewFITS()) { // If we are displaying all images from all cameras in a single FITS // Viewer window, then we prefix the camera name to the "Preview" string if (Options::singleWindowCapturedFITS()) previewTitle = i18n("%1 Preview", getDeviceName()); else // Otherwise, just use "Preview" previewTitle = i18n("Preview"); } success = m_FITSViewerWindows->addFITSFromData( blob_fits_data, fileURL, &tabIndex, captureMode, captureFilter, previewTitle); } else success = m_FITSViewerWindows->updateFITSFromData( blob_fits_data, fileURL, *tabID, &tabIndex, captureFilter); if (!success) { // If opening file fails, we treat it the same as exposure failure // and recapture again if possible qCCritical(KSTARS_INDI) << "error adding/updating FITS"; emit newExposureValue(targetChip, 0, IPS_ALERT); return; } *tabID = tabIndex; targetChip->setImageView(m_FITSViewerWindows->getView(tabIndex), captureMode); if (Options::focusFITSOnNewImage()) m_FITSViewerWindows->raise(); emit BLOBUpdated(bp); } else // If not displayed in FITS Viewer then we just inform that a blob was received. emit BLOBUpdated(bp); } break; case FITS_FOCUS: case FITS_GUIDE: case FITS_ALIGN: loadImageInView(bp, targetChip, blob_fits_data); break; } } else emit BLOBUpdated(bp); #endif } void CCD::loadImageInView(IBLOB *bp, ISD::CCDChip *targetChip, FITSData *data) { FITSMode mode = targetChip->getCaptureMode(); FITSView *view = targetChip->getImageView(mode); QString filename = QString(static_cast(bp->aux2)); if (view) { view->setFilter(targetChip->getCaptureFilter()); if (!view->loadFITSFromData(data, filename)) { emit newExposureValue(targetChip, 0, IPS_ALERT); return; } // FITSViewer is shown if: // Image in preview mode, or useFITSViewer is true; AND // Image type is either NORMAL or CALIBRATION since the rest have their dedicated windows. // NORMAL is used for raw INDI drivers without Ekos. if ( (Options::useFITSViewer() || targetChip->isBatchMode() == false) && (mode == FITS_NORMAL || mode == FITS_CALIBRATE)) m_FITSViewerWindows->show(); emit BLOBUpdated(bp); } } CCD::TransferFormat CCD::getTargetTransferFormat() const { return targetTransferFormat; } void CCD::setTargetTransferFormat(const TransferFormat &value) { targetTransferFormat = value; } void CCD::FITSViewerDestroyed() { m_FITSViewerWindows = nullptr; normalTabID = calibrationTabID = focusTabID = guideTabID = alignTabID = -1; } void CCD::StreamWindowHidden() { if (baseDevice->isConnected()) { // We can have more than one *_VIDEO_STREAM property active so disable them all ISwitchVectorProperty *streamSP = baseDevice->getSwitch("CCD_VIDEO_STREAM"); if (streamSP) { IUResetSwitch(streamSP); streamSP->sp[0].s = ISS_OFF; streamSP->sp[1].s = ISS_ON; streamSP->s = IPS_IDLE; clientManager->sendNewSwitch(streamSP); } streamSP = baseDevice->getSwitch("VIDEO_STREAM"); if (streamSP) { IUResetSwitch(streamSP); streamSP->sp[0].s = ISS_OFF; streamSP->sp[1].s = ISS_ON; streamSP->s = IPS_IDLE; clientManager->sendNewSwitch(streamSP); } streamSP = baseDevice->getSwitch("AUX_VIDEO_STREAM"); if (streamSP) { IUResetSwitch(streamSP); streamSP->sp[0].s = ISS_OFF; streamSP->sp[1].s = ISS_ON; streamSP->s = IPS_IDLE; clientManager->sendNewSwitch(streamSP); } } if (streamWindow.get() != nullptr) streamWindow->disconnect(); } bool CCD::hasGuideHead() { return HasGuideHead; } bool CCD::hasCooler() { return HasCooler; } bool CCD::hasCoolerControl() { return HasCoolerControl; } bool CCD::setCoolerControl(bool enable) { if (HasCoolerControl == false) return false; ISwitchVectorProperty *coolerSP = baseDevice->getSwitch("CCD_COOLER"); if (coolerSP == nullptr) return false; // Cooler ON/OFF ISwitch *coolerON = IUFindSwitch(coolerSP, "COOLER_ON"); ISwitch *coolerOFF = IUFindSwitch(coolerSP, "COOLER_OFF"); if (coolerON == nullptr || coolerOFF == nullptr) return false; coolerON->s = enable ? ISS_ON : ISS_OFF; coolerOFF->s = enable ? ISS_OFF : ISS_ON; clientManager->sendNewSwitch(coolerSP); return true; } CCDChip *CCD::getChip(CCDChip::ChipType cType) { switch (cType) { case CCDChip::PRIMARY_CCD: return primaryChip.get(); case CCDChip::GUIDE_CCD: return guideChip.get(); } return nullptr; } bool CCD::setRapidGuide(CCDChip *targetChip, bool enable) { ISwitchVectorProperty *rapidSP = nullptr; ISwitch *enableS = nullptr; if (targetChip == primaryChip.get()) rapidSP = baseDevice->getSwitch("CCD_RAPID_GUIDE"); else rapidSP = baseDevice->getSwitch("GUIDER_RAPID_GUIDE"); if (rapidSP == nullptr) return false; enableS = IUFindSwitch(rapidSP, "ENABLE"); if (enableS == nullptr) return false; // Already updated, return OK if ((enable && enableS->s == ISS_ON) || (!enable && enableS->s == ISS_OFF)) return true; IUResetSwitch(rapidSP); rapidSP->sp[0].s = enable ? ISS_ON : ISS_OFF; rapidSP->sp[1].s = enable ? ISS_OFF : ISS_ON; clientManager->sendNewSwitch(rapidSP); return true; } bool CCD::configureRapidGuide(CCDChip *targetChip, bool autoLoop, bool sendImage, bool showMarker) { ISwitchVectorProperty *rapidSP = nullptr; ISwitch *autoLoopS = nullptr, *sendImageS = nullptr, *showMarkerS = nullptr; if (targetChip == primaryChip.get()) rapidSP = baseDevice->getSwitch("CCD_RAPID_GUIDE_SETUP"); else rapidSP = baseDevice->getSwitch("GUIDER_RAPID_GUIDE_SETUP"); if (rapidSP == nullptr) return false; autoLoopS = IUFindSwitch(rapidSP, "AUTO_LOOP"); sendImageS = IUFindSwitch(rapidSP, "SEND_IMAGE"); showMarkerS = IUFindSwitch(rapidSP, "SHOW_MARKER"); if (!autoLoopS || !sendImageS || !showMarkerS) return false; // If everything is already set, let's return. if (((autoLoop && autoLoopS->s == ISS_ON) || (!autoLoop && autoLoopS->s == ISS_OFF)) && ((sendImage && sendImageS->s == ISS_ON) || (!sendImage && sendImageS->s == ISS_OFF)) && ((showMarker && showMarkerS->s == ISS_ON) || (!showMarker && showMarkerS->s == ISS_OFF))) return true; autoLoopS->s = autoLoop ? ISS_ON : ISS_OFF; sendImageS->s = sendImage ? ISS_ON : ISS_OFF; showMarkerS->s = showMarker ? ISS_ON : ISS_OFF; clientManager->sendNewSwitch(rapidSP); return true; } void CCD::updateUploadSettings(const QString &remoteDir) { QString filename = seqPrefix + (seqPrefix.isEmpty() ? "" : "_") + QString("XXX"); ITextVectorProperty *uploadSettingsTP = nullptr; IText *uploadT = nullptr; uploadSettingsTP = baseDevice->getText("UPLOAD_SETTINGS"); if (uploadSettingsTP) { uploadT = IUFindText(uploadSettingsTP, "UPLOAD_DIR"); if (uploadT && remoteDir.isEmpty() == false) IUSaveText(uploadT, remoteDir.toLatin1().constData()); uploadT = IUFindText(uploadSettingsTP, "UPLOAD_PREFIX"); if (uploadT) IUSaveText(uploadT, filename.toLatin1().constData()); clientManager->sendNewText(uploadSettingsTP); } } CCD::UploadMode CCD::getUploadMode() { ISwitchVectorProperty *uploadModeSP = nullptr; uploadModeSP = baseDevice->getSwitch("UPLOAD_MODE"); if (uploadModeSP == nullptr) { qWarning() << "No UPLOAD_MODE in CCD driver. Please update driver to INDI compliant CCD driver."; return UPLOAD_CLIENT; } if (uploadModeSP) { ISwitch *modeS = nullptr; modeS = IUFindSwitch(uploadModeSP, "UPLOAD_CLIENT"); if (modeS && modeS->s == ISS_ON) return UPLOAD_CLIENT; modeS = IUFindSwitch(uploadModeSP, "UPLOAD_LOCAL"); if (modeS && modeS->s == ISS_ON) return UPLOAD_LOCAL; modeS = IUFindSwitch(uploadModeSP, "UPLOAD_BOTH"); if (modeS && modeS->s == ISS_ON) return UPLOAD_BOTH; } // Default return UPLOAD_CLIENT; } bool CCD::setUploadMode(UploadMode mode) { ISwitchVectorProperty *uploadModeSP = nullptr; ISwitch *modeS = nullptr; uploadModeSP = baseDevice->getSwitch("UPLOAD_MODE"); if (uploadModeSP == nullptr) { qWarning() << "No UPLOAD_MODE in CCD driver. Please update driver to INDI compliant CCD driver."; return false; } switch (mode) { case UPLOAD_CLIENT: modeS = IUFindSwitch(uploadModeSP, "UPLOAD_CLIENT"); if (modeS == nullptr) return false; if (modeS->s == ISS_ON) return true; break; case UPLOAD_BOTH: modeS = IUFindSwitch(uploadModeSP, "UPLOAD_BOTH"); if (modeS == nullptr) return false; if (modeS->s == ISS_ON) return true; break; case UPLOAD_LOCAL: modeS = IUFindSwitch(uploadModeSP, "UPLOAD_LOCAL"); if (modeS == nullptr) return false; if (modeS->s == ISS_ON) return true; break; } IUResetSwitch(uploadModeSP); modeS->s = ISS_ON; clientManager->sendNewSwitch(uploadModeSP); return true; } bool CCD::getTemperature(double *value) { if (HasCooler == false) return false; INumberVectorProperty *temperatureNP = baseDevice->getNumber("CCD_TEMPERATURE"); if (temperatureNP == nullptr) return false; *value = temperatureNP->np[0].value; return true; } bool CCD::setTemperature(double value) { INumberVectorProperty *nvp = baseDevice->getNumber("CCD_TEMPERATURE"); if (nvp == nullptr) return false; INumber *np = IUFindNumber(nvp, "CCD_TEMPERATURE_VALUE"); if (np == nullptr) return false; np->value = value; clientManager->sendNewNumber(nvp); return true; } bool CCD::setTransformFormat(CCD::TransferFormat format) { if (format == transferFormat) return true; ISwitchVectorProperty *svp = baseDevice->getSwitch("CCD_TRANSFER_FORMAT"); if (svp == nullptr) return false; ISwitch *formatFITS = IUFindSwitch(svp, "FORMAT_FITS"); ISwitch *formatNative = IUFindSwitch(svp, "FORMAT_NATIVE"); if (formatFITS == nullptr || formatNative == nullptr) return false; transferFormat = format; formatFITS->s = (transferFormat == FORMAT_FITS) ? ISS_ON : ISS_OFF; formatNative->s = (transferFormat == FORMAT_FITS) ? ISS_OFF : ISS_ON; clientManager->sendNewSwitch(svp); return true; } bool CCD::setTelescopeType(TelescopeType type) { if (type == telescopeType) return true; ISwitchVectorProperty *svp = baseDevice->getSwitch("TELESCOPE_TYPE"); if (svp == nullptr) return false; ISwitch *typePrimary = IUFindSwitch(svp, "TELESCOPE_PRIMARY"); ISwitch *typeGuide = IUFindSwitch(svp, "TELESCOPE_GUIDE"); if (typePrimary == nullptr || typeGuide == nullptr) return false; telescopeType = type; typePrimary->s = (telescopeType == TELESCOPE_PRIMARY) ? ISS_ON : ISS_OFF; typeGuide->s = (telescopeType == TELESCOPE_PRIMARY) ? ISS_OFF : ISS_ON; clientManager->sendNewSwitch(svp); setConfig(SAVE_CONFIG); return true; } bool CCD::setVideoStreamEnabled(bool enable) { if (HasVideoStream == false) return false; ISwitchVectorProperty *svp = baseDevice->getSwitch("CCD_VIDEO_STREAM"); if (svp == nullptr) return false; // If already on and enable is set or vice versa no need to change anything we return true if ((enable && svp->sp[0].s == ISS_ON) || (!enable && svp->sp[1].s == ISS_ON)) return true; svp->sp[0].s = enable ? ISS_ON : ISS_OFF; svp->sp[1].s = enable ? ISS_OFF : ISS_ON; clientManager->sendNewSwitch(svp); return true; } bool CCD::resetStreamingFrame() { INumberVectorProperty *frameProp = baseDevice->getNumber("CCD_STREAM_FRAME"); if (frameProp == nullptr) return false; INumber *xarg = IUFindNumber(frameProp, "X"); INumber *yarg = IUFindNumber(frameProp, "Y"); INumber *warg = IUFindNumber(frameProp, "WIDTH"); INumber *harg = IUFindNumber(frameProp, "HEIGHT"); if (xarg && yarg && warg && harg) { if (xarg->value == xarg->min && yarg->value == yarg->min && warg->value == warg->max && harg->value == harg->max) return false; xarg->value = xarg->min; yarg->value = yarg->min; warg->value = warg->max; harg->value = harg->max; clientManager->sendNewNumber(frameProp); return true; } return false; } bool CCD::setStreamingFrame(int x, int y, int w, int h) { INumberVectorProperty *frameProp = baseDevice->getNumber("CCD_STREAM_FRAME"); if (frameProp == nullptr) return false; INumber *xarg = IUFindNumber(frameProp, "X"); INumber *yarg = IUFindNumber(frameProp, "Y"); INumber *warg = IUFindNumber(frameProp, "WIDTH"); INumber *harg = IUFindNumber(frameProp, "HEIGHT"); if (xarg && yarg && warg && harg) { if (xarg->value == x && yarg->value == y && warg->value == w && harg->value == h) return true; // N.B. We add offset since the X, Y are relative to whatever streaming frame is currently active xarg->value = qBound(xarg->min, static_cast(x) + xarg->value, xarg->max); yarg->value = qBound(yarg->min, static_cast(y) + yarg->value, yarg->max); warg->value = qBound(warg->min, static_cast(w), warg->max); harg->value = qBound(harg->min, static_cast(h), harg->max); clientManager->sendNewNumber(frameProp); return true; } return false; } bool CCD::isStreamingEnabled() { if (HasVideoStream == false || streamWindow.get() == nullptr) return false; return streamWindow->isStreamEnabled(); } bool CCD::setSERNameDirectory(const QString &filename, const QString &directory) { ITextVectorProperty *tvp = baseDevice->getText("RECORD_FILE"); if (tvp == nullptr) return false; IText *filenameT = IUFindText(tvp, "RECORD_FILE_NAME"); IText *dirT = IUFindText(tvp, "RECORD_FILE_DIR"); if (filenameT == nullptr || dirT == nullptr) return false; IUSaveText(filenameT, filename.toLatin1().data()); IUSaveText(dirT, directory.toLatin1().data()); clientManager->sendNewText(tvp); return true; } bool CCD::getSERNameDirectory(QString &filename, QString &directory) { ITextVectorProperty *tvp = baseDevice->getText("RECORD_FILE"); if (tvp == nullptr) return false; IText *filenameT = IUFindText(tvp, "RECORD_FILE_NAME"); IText *dirT = IUFindText(tvp, "RECORD_FILE_DIR"); if (filenameT == nullptr || dirT == nullptr) return false; filename = QString(filenameT->text); directory = QString(dirT->text); return true; } bool CCD::startRecording() { ISwitchVectorProperty *svp = baseDevice->getSwitch("RECORD_STREAM"); if (svp == nullptr) return false; ISwitch *recordON = IUFindSwitch(svp, "RECORD_ON"); if (recordON == nullptr) return false; if (recordON->s == ISS_ON) return true; IUResetSwitch(svp); recordON->s = ISS_ON; clientManager->sendNewSwitch(svp); return true; } bool CCD::startDurationRecording(double duration) { INumberVectorProperty *nvp = baseDevice->getNumber("RECORD_OPTIONS"); if (nvp == nullptr) return false; INumber *durationN = IUFindNumber(nvp, "RECORD_DURATION"); if (durationN == nullptr) return false; ISwitchVectorProperty *svp = baseDevice->getSwitch("RECORD_STREAM"); if (svp == nullptr) return false; ISwitch *recordON = IUFindSwitch(svp, "RECORD_DURATION_ON"); if (recordON == nullptr) return false; if (recordON->s == ISS_ON) return true; durationN->value = duration; clientManager->sendNewNumber(nvp); IUResetSwitch(svp); recordON->s = ISS_ON; clientManager->sendNewSwitch(svp); return true; } bool CCD::startFramesRecording(uint32_t frames) { INumberVectorProperty *nvp = baseDevice->getNumber("RECORD_OPTIONS"); if (nvp == nullptr) return false; INumber *frameN = IUFindNumber(nvp, "RECORD_FRAME_TOTAL"); ISwitchVectorProperty *svp = baseDevice->getSwitch("RECORD_STREAM"); if (frameN == nullptr || svp == nullptr) return false; ISwitch *recordON = IUFindSwitch(svp, "RECORD_FRAME_ON"); if (recordON == nullptr) return false; if (recordON->s == ISS_ON) return true; frameN->value = frames; clientManager->sendNewNumber(nvp); IUResetSwitch(svp); recordON->s = ISS_ON; clientManager->sendNewSwitch(svp); return true; } bool CCD::stopRecording() { ISwitchVectorProperty *svp = baseDevice->getSwitch("RECORD_STREAM"); if (svp == nullptr) return false; ISwitch *recordOFF = IUFindSwitch(svp, "RECORD_OFF"); if (recordOFF == nullptr) return false; // If already set if (recordOFF->s == ISS_ON) return true; IUResetSwitch(svp); recordOFF->s = ISS_ON; clientManager->sendNewSwitch(svp); return true; } bool CCD::setFITSHeader(const QMap &values) { ITextVectorProperty *tvp = baseDevice->getText("FITS_HEADER"); if (tvp == nullptr) return false; QMapIterator i(values); while (i.hasNext()) { i.next(); IText *headerT = IUFindText(tvp, i.key().toLatin1().data()); if (headerT == nullptr) continue; IUSaveText(headerT, i.value().toLatin1().data()); } clientManager->sendNewText(tvp); return true; } bool CCD::setGain(double value) { if (gainN == nullptr) return false; gainN->value = value; clientManager->sendNewNumber(gainN->nvp); return true; } bool CCD::getGain(double *value) { if (gainN == nullptr) return false; *value = gainN->value; return true; } bool CCD::getGainMinMaxStep(double *min, double *max, double *step) { if (gainN == nullptr) return false; *min = gainN->min; *max = gainN->max; *step = gainN->step; return true; } bool CCD::isBLOBEnabled() { return (clientManager->isBLOBEnabled(getDeviceName(), "CCD1")); } bool CCD::setBLOBEnabled(bool enable, const QString &prop) { clientManager->setBLOBEnabled(enable, getDeviceName(), prop); return true; } bool CCD::setExposureLoopingEnabled(bool enable) { // Set value immediately IsLooping = enable; ISwitchVectorProperty *svp = baseDevice->getSwitch("CCD_EXPOSURE_LOOP"); if (svp == nullptr) return false; svp->sp[0].s = enable ? ISS_ON : ISS_OFF; svp->sp[1].s = enable ? ISS_OFF : ISS_ON; clientManager->sendNewSwitch(svp); return true; } bool CCD::setExposureLoopCount(uint32_t count) { INumberVectorProperty *nvp = baseDevice->getNumber("CCD_EXPOSURE_LOOP_COUNT"); if (nvp == nullptr) return false; nvp->np[0].value = count; clientManager->sendNewNumber(nvp); return true; } bool CCD::setStreamExposure(double duration) { INumberVectorProperty *nvp = baseDevice->getNumber("STREAMING_EXPOSURE"); if (nvp == nullptr) return false; nvp->np[0].value = duration; clientManager->sendNewNumber(nvp); return true; } bool CCD::getStreamExposure(double *duration) { INumberVectorProperty *nvp = baseDevice->getNumber("STREAMING_EXPOSURE"); if (nvp == nullptr) return false; *duration = nvp->np[0].value; return true; } bool CCD::isCoolerOn() { ISwitchVectorProperty *svp = baseDevice->getSwitch("CCD_COOLER"); if (svp == nullptr) return false; return (svp->sp[0].s == ISS_ON); } }