diff --git a/kstars/ekos/observatory/observatory.cpp b/kstars/ekos/observatory/observatory.cpp index c777522b6..f281320fa 100644 --- a/kstars/ekos/observatory/observatory.cpp +++ b/kstars/ekos/observatory/observatory.cpp @@ -1,654 +1,857 @@ /* Ekos Observatory Module Copyright (C) Wolfgang Reissenberger 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 "kstarsdata.h" #include "observatory.h" #include "ekos_observatory_debug.h" namespace Ekos { Observatory::Observatory() { setupUi(this); // status control mObservatoryModel = new ObservatoryModel(); setObseratoryStatusControl(mObservatoryModel->statusControl()); // update UI for status control connect(useDomeCB, &QCheckBox::clicked, this, &Ekos::Observatory::statusControlSettingsChanged); connect(useShutterCB, &QCheckBox::clicked, this, &Ekos::Observatory::statusControlSettingsChanged); connect(useWeatherCB, &QCheckBox::clicked, this, &Ekos::Observatory::statusControlSettingsChanged); connect(mObservatoryModel, &Ekos::ObservatoryModel::newStatus, this, &Ekos::Observatory::observatoryStatusChanged); // ready button deactivated // connect(statusReadyButton, &QPushButton::clicked, mObservatoryModel, &Ekos::ObservatoryModel::makeReady); statusReadyButton->setEnabled(false); setDomeModel(new ObservatoryDomeModel()); setWeatherModel(new ObservatoryWeatherModel()); - statusDefinitionBox->setVisible(true); - statusDefinitionBox->setEnabled(true); - // make invisible, since not implemented yet - weatherWarningSchedulerCB->setVisible(false); - weatherAlertSchedulerCB->setVisible(false); - // initialize the weather sensor data group box - sensorDataBoxLayout = new QFormLayout(); - sensorDataBox->setLayout(sensorDataBoxLayout); } void Observatory::setObseratoryStatusControl(ObservatoryStatusControl control) { if (mObservatoryModel != nullptr) { useDomeCB->setChecked(control.useDome); useShutterCB->setChecked(control.useShutter); useWeatherCB->setChecked(control.useWeather); } } void Observatory::setDomeModel(ObservatoryDomeModel *model) { mObservatoryModel->setDomeModel(model); if (model != nullptr) { connect(model, &Ekos::ObservatoryDomeModel::ready, this, &Ekos::Observatory::initDome); connect(model, &Ekos::ObservatoryDomeModel::disconnected, this, &Ekos::Observatory::shutdownDome); connect(model, &Ekos::ObservatoryDomeModel::newStatus, this, &Ekos::Observatory::setDomeStatus); connect(model, &Ekos::ObservatoryDomeModel::newParkStatus, this, &Ekos::Observatory::setDomeParkStatus); connect(model, &Ekos::ObservatoryDomeModel::newShutterStatus, this, &Ekos::Observatory::setShutterStatus); connect(model, &Ekos::ObservatoryDomeModel::azimuthPositionChanged, this, &Ekos::Observatory::domeAzimuthChanged); connect(model, &Ekos::ObservatoryDomeModel::newAutoSyncStatus, this, &Ekos::Observatory::showAutoSync); // motion controls connect(motionMoveAbsButton, &QCheckBox::clicked, [this]() { mObservatoryModel->getDomeModel()->setAzimuthPosition(absoluteMotionSB->value()); }); connect(motionMoveRelButton, &QCheckBox::clicked, [this]() { mObservatoryModel->getDomeModel()->setRelativePosition(relativeMotionSB->value()); }); // abort button connect(motionAbortButton, &QPushButton::clicked, model, &ObservatoryDomeModel::abort); // weather controls connect(weatherWarningShutterCB, &QCheckBox::clicked, this, &Observatory::weatherWarningSettingsChanged); connect(weatherWarningDomeCB, &QCheckBox::clicked, this, &Observatory::weatherWarningSettingsChanged); connect(weatherWarningDelaySB, static_cast(&QSpinBox::valueChanged), [this](int i) { Q_UNUSED(i); weatherWarningSettingsChanged(); }); connect(weatherAlertShutterCB, &QCheckBox::clicked, this, &Observatory::weatherAlertSettingsChanged); connect(weatherAlertDomeCB, &QCheckBox::clicked, this, &Observatory::weatherAlertSettingsChanged); connect(weatherAlertDelaySB, static_cast(&QSpinBox::valueChanged), [this](int i) { Q_UNUSED(i); weatherAlertSettingsChanged(); }); } else { shutdownDome(); disconnect(weatherWarningShutterCB, &QCheckBox::clicked, this, &Observatory::weatherWarningSettingsChanged); disconnect(weatherWarningDomeCB, &QCheckBox::clicked, this, &Observatory::weatherWarningSettingsChanged); connect(weatherWarningDelaySB, static_cast(&QSpinBox::valueChanged), [this](int i) { Q_UNUSED(i); weatherWarningSettingsChanged(); }); disconnect(weatherAlertShutterCB, &QCheckBox::clicked, this, &Observatory::weatherAlertSettingsChanged); disconnect(weatherAlertDomeCB, &QCheckBox::clicked, this, &Observatory::weatherAlertSettingsChanged); connect(weatherAlertDelaySB, static_cast(&QSpinBox::valueChanged), [this](int i) { Q_UNUSED(i); weatherWarningSettingsChanged(); }); } } void Observatory::initDome() { domeBox->setEnabled(true); if (getDomeModel() != nullptr) { connect(getDomeModel(), &Ekos::ObservatoryDomeModel::newLog, this, &Ekos::Observatory::appendLogText); // dome motion buttons connect(motionCWButton, &QPushButton::clicked, [ = ](bool checked) { getDomeModel()->moveDome(true, checked); }); connect(motionCCWButton, &QPushButton::clicked, [ = ](bool checked) { getDomeModel()->moveDome(false, checked); }); if (getDomeModel()->canPark()) { connect(domePark, &QPushButton::clicked, getDomeModel(), &Ekos::ObservatoryDomeModel::park); connect(domeUnpark, &QPushButton::clicked, getDomeModel(), &Ekos::ObservatoryDomeModel::unpark); domePark->setEnabled(true); domeUnpark->setEnabled(true); } else { domePark->setEnabled(false); domeUnpark->setEnabled(false); } if (getDomeModel()->isRolloffRoof()) { SlavingBox->setVisible(false); domeAzimuthPosition->setText(i18nc("Not Applicable", "N/A")); enableMotionControl(true); } else { // initialize the dome motion controls domeAzimuthChanged(getDomeModel()->azimuthPosition()); // slaving showAutoSync(getDomeModel()->isAutoSync()); connect(slavingEnableButton, &QPushButton::clicked, this, [this]() { enableAutoSync(true); }); connect(slavingDisableButton, &QPushButton::clicked, this, [this]() { enableAutoSync(false); }); } // shutter handling if (getDomeModel()->hasShutter()) { shutterBox->setVisible(true); shutterBox->setEnabled(true); connect(shutterOpen, &QPushButton::clicked, getDomeModel(), &Ekos::ObservatoryDomeModel::openShutter); connect(shutterClosed, &QPushButton::clicked, getDomeModel(), &Ekos::ObservatoryDomeModel::closeShutter); shutterClosed->setEnabled(true); shutterOpen->setEnabled(true); setShutterStatus(getDomeModel()->shutterStatus()); useShutterCB->setVisible(true); } else { shutterBox->setVisible(false); weatherWarningShutterCB->setVisible(false); weatherAlertShutterCB->setVisible(false); useShutterCB->setVisible(false); } // abort button should always be available motionAbortButton->setEnabled(true); + + statusDefinitionBox->setVisible(true); + statusDefinitionBox->setEnabled(true); + // update the dome parking status setDomeParkStatus(getDomeModel()->parkStatus()); } } void Observatory::shutdownDome() { domeBox->setEnabled(false); shutterBox->setEnabled(false); shutterBox->setVisible(false); domePark->setEnabled(false); domeUnpark->setEnabled(false); shutterClosed->setEnabled(false); shutterOpen->setEnabled(false); disconnect(domePark, &QPushButton::clicked, getDomeModel(), &Ekos::ObservatoryDomeModel::park); disconnect(domeUnpark, &QPushButton::clicked, getDomeModel(), &Ekos::ObservatoryDomeModel::unpark); } void Observatory::setDomeStatus(ISD::Dome::Status status) { qCDebug(KSTARS_EKOS_OBSERVATORY) << "Setting dome status to " << status; switch (status) { case ISD::Dome::DOME_ERROR: appendLogText(i18n("%1 error. See INDI log for details.", getDomeModel()->isRolloffRoof() ? i18n("Rolloff roof") : i18n("Dome"))); motionCWButton->setChecked(false); motionCCWButton->setChecked(false); break; case ISD::Dome::DOME_IDLE: motionCWButton->setChecked(false); motionCWButton->setEnabled(true); motionCCWButton->setChecked(false); motionCCWButton->setEnabled(true); appendLogText(i18n("%1 is idle.", getDomeModel()->isRolloffRoof() ? i18n("Rolloff roof") : i18n("Dome"))); break; case ISD::Dome::DOME_MOVING_CW: motionCWButton->setChecked(true); motionCWButton->setEnabled(false); motionCCWButton->setChecked(false); motionCCWButton->setEnabled(true); if (getDomeModel()->isRolloffRoof()) { domeAzimuthPosition->setText(i18n("Opening")); toggleButtons(domeUnpark, i18n("Unparking"), domePark, i18n("Park")); appendLogText(i18n("Rolloff roof opening...")); } else { appendLogText(i18n("Dome is moving clockwise...")); } break; case ISD::Dome::DOME_MOVING_CCW: motionCWButton->setChecked(false); motionCWButton->setEnabled(true); motionCCWButton->setChecked(true); motionCCWButton->setEnabled(false); if (getDomeModel()->isRolloffRoof()) { domeAzimuthPosition->setText(i18n("Closing")); toggleButtons(domePark, i18n("Parking"), domeUnpark, i18n("Unpark")); appendLogText(i18n("Rolloff roof is closing...")); } else { appendLogText(i18n("Dome is moving counter clockwise...")); } break; case ISD::Dome::DOME_PARKED: setDomeParkStatus(ISD::PARK_PARKED); appendLogText(i18n("%1 is parked.", getDomeModel()->isRolloffRoof() ? i18n("Rolloff roof") : i18n("Dome"))); break; case ISD::Dome::DOME_PARKING: toggleButtons(domePark, i18n("Parking"), domeUnpark, i18n("Unpark")); motionCWButton->setEnabled(true); if (getDomeModel()->isRolloffRoof()) domeAzimuthPosition->setText(i18n("Closing")); else enableMotionControl(false); motionCWButton->setChecked(false); motionCCWButton->setChecked(true); appendLogText(i18n("%1 is parking...", getDomeModel()->isRolloffRoof() ? i18n("Rolloff roof") : i18n("Dome"))); break; case ISD::Dome::DOME_UNPARKING: toggleButtons(domeUnpark, i18n("Unparking"), domePark, i18n("Park")); motionCCWButton->setEnabled(true); if (getDomeModel()->isRolloffRoof()) domeAzimuthPosition->setText(i18n("Opening")); else enableMotionControl(false); motionCWButton->setChecked(true); motionCCWButton->setChecked(false); appendLogText(i18n("%1 is unparking...", getDomeModel()->isRolloffRoof() ? i18n("Rolloff roof") : i18n("Dome"))); break; case ISD::Dome::DOME_TRACKING: enableMotionControl(true); motionCWButton->setEnabled(true); motionCCWButton->setChecked(true); appendLogText(i18n("%1 is tracking.", getDomeModel()->isRolloffRoof() ? i18n("Rolloff roof") : i18n("Dome"))); break; } } void Observatory::setDomeParkStatus(ISD::ParkStatus status) { qCDebug(KSTARS_EKOS_OBSERVATORY) << "Setting dome park status to " << status; switch (status) { case ISD::PARK_UNPARKED: activateButton(domePark, i18n("Park")); buttonPressed(domeUnpark, i18n("Unparked")); motionCWButton->setChecked(false); motionCWButton->setEnabled(true); motionCCWButton->setChecked(false); if (getDomeModel()->isRolloffRoof()) domeAzimuthPosition->setText(i18n("Open")); else enableMotionControl(true); break; case ISD::PARK_PARKED: buttonPressed(domePark, i18n("Parked")); activateButton(domeUnpark, i18n("Unpark")); motionCWButton->setChecked(false); motionCCWButton->setChecked(false); motionCCWButton->setEnabled(false); if (getDomeModel()->isRolloffRoof()) domeAzimuthPosition->setText(i18n("Closed")); else enableMotionControl(false); break; default: break; } } void Observatory::setShutterStatus(ISD::Dome::ShutterStatus status) { qCDebug(KSTARS_EKOS_OBSERVATORY) << "Setting shutter status to " << status; switch (status) { case ISD::Dome::SHUTTER_OPEN: buttonPressed(shutterOpen, i18n("Opened")); activateButton(shutterClosed, i18n("Close")); appendLogText(i18n("Shutter is open.")); break; case ISD::Dome::SHUTTER_OPENING: toggleButtons(shutterOpen, i18n("Opening"), shutterClosed, i18n("Close")); appendLogText(i18n("Shutter is opening...")); break; case ISD::Dome::SHUTTER_CLOSED: buttonPressed(shutterClosed, i18n("Closed")); activateButton(shutterOpen, i18n("Open")); appendLogText(i18n("Shutter is closed.")); break; case ISD::Dome::SHUTTER_CLOSING: toggleButtons(shutterClosed, i18n("Closing"), shutterOpen, i18n("Open")); appendLogText(i18n("Shutter is closing...")); break; default: break; } } +void Observatory::enableWeather(bool enable) +{ + weatherBox->setEnabled(enable); + clearGraphHistory->setVisible(enable); + clearGraphHistory->setEnabled(enable); + sensorGraphs->setVisible(enable); +} +void Observatory::clearSensorDataHistory() +{ + std::map*>::iterator it; + for (it=sensorGraphData.begin(); it != sensorGraphData.end(); ++it) + { + QVector* graphDataVector = it->second; + if (graphDataVector->size() > 0) + { + // we keep only the last one + QCPGraphData last = graphDataVector->last(); + graphDataVector->clear(); + QDateTime when = QDateTime(); + when.setSecsSinceEpoch(static_cast(last.key)); + updateSensorGraph(it->first, when, last.value); + } + } + + // force an update to the current graph + if (selectedSensorID != "") + selectedSensorChanged(selectedSensorID); +} void Observatory::setWeatherModel(ObservatoryWeatherModel *model) { mObservatoryModel->setWeatherModel(model); - if (model != nullptr) - { - connect(weatherWarningBox, &QGroupBox::clicked, model, &ObservatoryWeatherModel::setWarningActionsActive); - connect(weatherAlertBox, &QGroupBox::clicked, model, &ObservatoryWeatherModel::setAlertActionsActive); + // disable the weather UI + enableWeather(false); + if (model != nullptr) connect(model, &Ekos::ObservatoryWeatherModel::ready, this, &Ekos::Observatory::initWeather); - connect(model, &Ekos::ObservatoryWeatherModel::newStatus, this, &Ekos::Observatory::setWeatherStatus); - connect(model, &Ekos::ObservatoryWeatherModel::disconnected, this, &Ekos::Observatory::shutdownWeather); - connect(&weatherStatusTimer, &QTimer::timeout, [this]() - { - weatherWarningStatusLabel->setText(getWeatherModel()->getWarningActionsStatus()); - weatherAlertStatusLabel->setText(getWeatherModel()->getAlertActionsStatus()); - }); - } else shutdownWeather(); + + // make invisible, since not implemented yet + weatherWarningSchedulerCB->setVisible(false); + weatherAlertSchedulerCB->setVisible(false); } void Observatory::enableMotionControl(bool enabled) { MotionBox->setEnabled(enabled); // absolute motion controls if (getDomeModel()->canAbsoluteMove()) { motionMoveAbsButton->setEnabled(enabled); absoluteMotionSB->setEnabled(enabled); } else { motionMoveAbsButton->setEnabled(false); absoluteMotionSB->setEnabled(false); } // relative motion controls if (getDomeModel()->canRelativeMove()) { motionMoveRelButton->setEnabled(enabled); relativeMotionSB->setEnabled(enabled); motionCWButton->setEnabled(enabled); motionCCWButton->setEnabled(enabled); } else { motionMoveRelButton->setEnabled(false); relativeMotionSB->setEnabled(false); motionCWButton->setEnabled(false); motionCCWButton->setEnabled(false); } // special case for rolloff roofs if (getDomeModel()->isRolloffRoof()) { motionCWButton->setText(i18n("Open")); motionCCWButton->setText(i18n("Close")); motionCWButton->setEnabled(enabled); motionCCWButton->setEnabled(enabled); motionMoveAbsButton->setVisible(false); motionMoveRelButton->setVisible(false); absoluteMotionSB->setVisible(false); relativeMotionSB->setVisible(false); } } void Observatory::enableAutoSync(bool enabled) { if (getDomeModel() == nullptr) showAutoSync(false); else { getDomeModel()->setAutoSync(enabled); showAutoSync(enabled); } } void Observatory::showAutoSync(bool enabled) { slavingEnableButton->setChecked(enabled); slavingDisableButton->setChecked(! enabled); } void Observatory::initWeather() { + // initialize the weather sensor data group box + sensorDataBoxLayout = new QGridLayout(); + sensorData->setLayout(sensorDataBoxLayout); + + enableWeather(true); + initSensorGraphs(); + + connect(weatherWarningBox, &QGroupBox::clicked, getWeatherModel(), &ObservatoryWeatherModel::setWarningActionsActive); + connect(weatherAlertBox, &QGroupBox::clicked, getWeatherModel(), &ObservatoryWeatherModel::setAlertActionsActive); + + connect(getWeatherModel(), &Ekos::ObservatoryWeatherModel::newStatus, this, &Ekos::Observatory::setWeatherStatus); + connect(getWeatherModel(), &Ekos::ObservatoryWeatherModel::disconnected, this, &Ekos::Observatory::shutdownWeather); + connect(clearGraphHistory, &QPushButton::clicked, this, &Observatory::clearSensorDataHistory); + connect(&weatherStatusTimer, &QTimer::timeout, [this]() + { + weatherWarningStatusLabel->setText(getWeatherModel()->getWarningActionsStatus()); + weatherAlertStatusLabel->setText(getWeatherModel()->getAlertActionsStatus()); + }); + weatherBox->setEnabled(true); - weatherLabel->setEnabled(true); weatherActionsBox->setVisible(true); weatherActionsBox->setEnabled(true); weatherWarningBox->setChecked(getWeatherModel()->getWarningActionsActive()); weatherAlertBox->setChecked(getWeatherModel()->getAlertActionsActive()); setWeatherStatus(getWeatherModel()->status()); setWarningActions(getWeatherModel()->getWarningActions()); setAlertActions(getWeatherModel()->getAlertActions()); weatherStatusTimer.start(1000); } void Observatory::shutdownWeather() { - weatherBox->setEnabled(false); - weatherLabel->setEnabled(false); - setWeatherStatus(ISD::Weather::WEATHER_IDLE); weatherStatusTimer.stop(); + setWeatherStatus(ISD::Weather::WEATHER_IDLE); + enableWeather(false); } +void Observatory::updateSensorGraph(QString label, QDateTime now, double value) +{ + // we assume that labels are unique and use the full label as identifier + QString id = label; + + // lazy instantiation of the sensor data storage + if (sensorGraphData[id] == nullptr) + { + sensorGraphData[id] = new QVector(); + sensorRanges[id] = value > 0 ? 1 : (value < 0 ? -1 : 0); + } + + // store the data + sensorGraphData[id]->append(QCPGraphData(static_cast(now.toSecsSinceEpoch()), value)); + + // add data for the graphs we display + if (selectedSensorID == id) + { + // display data point + sensorGraphs->graph()->addData(sensorGraphData[id]->last().key, sensorGraphData[id]->last().value); + sensorGraphs->rescaleAxes(); + // ensure that the 0-line is visible + if ((sensorRanges[id] > 0 && value < 0) || (sensorRanges[id] < 0 && value > 0)) + sensorRanges[id] = 0; + + // ensure visibility of the 0-line on the y-axis + if (sensorRanges[id] > 0) + sensorGraphs->yAxis->setRangeLower(0); + else if (sensorRanges[id] < 0) + sensorGraphs->yAxis->setRangeUpper(0); + sensorGraphs->replot(); + } +} + +void Observatory::updateSensorData(std::vector weatherData) +{ + std::vector::iterator it; + QDateTime now = KStarsData::Instance()->lt(); + + for (it=weatherData.begin(); it != weatherData.end(); ++it) + { + QString const id = it->label; + + if (sensorDataWidgets[id] == nullptr) + { + QPushButton* labelWidget = new QPushButton(it->label); + labelWidget->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed)); + labelWidget->setCheckable(true); + labelWidget->setStyleSheet("QPushButton:checked\n{\nbackground-color: maroon;\nborder: 1px outset;\nfont-weight:bold;\n}"); + // we need the object name since the label may contain '&' for keyboard shortcuts + labelWidget->setObjectName(it->label); + + QLineEdit* valueWidget = new QLineEdit(QString().setNum(it->value, 'f', 2)); + // fix width to enable stretching of the graph + valueWidget->setMinimumWidth(96); + valueWidget->setMaximumWidth(96); + valueWidget->setReadOnly(true); + valueWidget->setAlignment(Qt::AlignRight); + + sensorDataWidgets[id] = new QPair(labelWidget, valueWidget); + + sensorDataBoxLayout->addWidget(labelWidget, sensorDataBoxLayout->rowCount(), 0); + sensorDataBoxLayout->addWidget(valueWidget, sensorDataBoxLayout->rowCount()-1, 1); + + // initial graph selection + if (selectedSensorID == "" && id.indexOf('(') > 0 && id.indexOf('(') < id.indexOf(')')) + { + selectedSensorID = id; + labelWidget->setChecked(true); + } + + sensorDataNamesGroup->addButton(labelWidget); + } + else + { + sensorDataWidgets[id]->first->setText(QString(it->label)); + sensorDataWidgets[id]->second->setText(QString().setNum(it->value, 'f', 2)); + } + + // store sensor data unit if necessary + updateSensorGraph(it->label, now, it->value); + } +} + +void Observatory::mouseOverLine(QMouseEvent *event) +{ + double key = sensorGraphs->xAxis->pixelToCoord(event->localPos().x()); + QCPGraph *graph = qobject_cast(sensorGraphs->plottableAt(event->pos(), false)); + + if (graph) + { + int index = sensorGraphs->graph(0)->findBegin(key); + double value = sensorGraphs->graph(0)->dataMainValue(index); + + QToolTip::showText( + event->globalPos(), + i18n("%1 = %2", selectedSensorID, value)); + } + else { + QToolTip::hideText(); + } +} + + +void Observatory::selectedSensorChanged(QString id) +{ + QVector *data = sensorGraphData[id]; + + if (data != nullptr) + { + // copy the graph data to the graph container + QCPGraphDataContainer *container = new QCPGraphDataContainer(); + for (QVector::iterator it = data->begin(); it != data->end(); ++it) + container->add(QCPGraphData(it->key, it->value)); + + sensorGraphs->graph()->setData(QSharedPointer(container)); + sensorGraphs->rescaleAxes(); + sensorGraphs->replot(); + selectedSensorID = id; + } +} + + + void Observatory::setWeatherStatus(ISD::Weather::Status status) { std::string label; switch (status) { case ISD::Weather::WEATHER_OK: label = "security-high"; appendLogText(i18n("Weather is OK")); break; case ISD::Weather::WEATHER_WARNING: label = "security-medium"; appendLogText(i18n("Weather Warning")); break; case ISD::Weather::WEATHER_ALERT: label = "security-low"; appendLogText(i18n("Weather Alert")); break; default: label = ""; break; } - weatherStatusLabel->setPixmap(QIcon::fromTheme(label.c_str()).pixmap(QSize(48, 48))); + weatherStatusLabel->setPixmap(QIcon::fromTheme(label.c_str()).pixmap(QSize(28, 28))); std::vector weatherData = getWeatherModel()->getWeatherData(); // update weather sensor data - int row_nr = 0; - std::vector::iterator it; - for (it=weatherData.begin(); it != weatherData.end(); ++it) - { - if (sensorDataBoxLayout->rowCount() > row_nr) - { - sensorDataWidgets.value(row_nr)->first->setText(QString(it->label)); - sensorDataWidgets.value(row_nr)->second->setText(QString().setNum(it->value, 'f', 2)); - } - else - { - QLabel* labelWidget = new QLabel(it->label); - QLineEdit* valueWidget = new QLineEdit(QString().setNum(it->value, 'f', 2)); - valueWidget->setReadOnly(false); - valueWidget->setAlignment(Qt::AlignRight); + updateSensorData(weatherData); - sensorDataWidgets.append(new QPair(labelWidget, valueWidget)); +} - sensorDataBoxLayout->addRow(labelWidget, valueWidget); - } - row_nr++; - } +void Observatory::initSensorGraphs() +{ + // set some pens, brushes and backgrounds: + sensorGraphs->xAxis->setBasePen(QPen(Qt::white, 1)); + sensorGraphs->yAxis->setBasePen(QPen(Qt::white, 1)); + sensorGraphs->xAxis->setTickPen(QPen(Qt::white, 1)); + sensorGraphs->yAxis->setTickPen(QPen(Qt::white, 1)); + sensorGraphs->xAxis->setSubTickPen(QPen(Qt::white, 1)); + sensorGraphs->yAxis->setSubTickPen(QPen(Qt::white, 1)); + sensorGraphs->xAxis->setTickLabelColor(Qt::white); + sensorGraphs->yAxis->setTickLabelColor(Qt::white); + sensorGraphs->xAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); + sensorGraphs->yAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); + sensorGraphs->xAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); + sensorGraphs->yAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); + sensorGraphs->xAxis->grid()->setSubGridVisible(true); + sensorGraphs->yAxis->grid()->setSubGridVisible(true); + sensorGraphs->xAxis->grid()->setZeroLinePen(Qt::NoPen); + sensorGraphs->yAxis->grid()->setZeroLinePen(Qt::NoPen); + sensorGraphs->xAxis->setUpperEnding(QCPLineEnding::esSpikeArrow); + sensorGraphs->yAxis->setUpperEnding(QCPLineEnding::esSpikeArrow); + QLinearGradient plotGradient; + plotGradient.setStart(0, 0); + plotGradient.setFinalStop(0, 350); + plotGradient.setColorAt(0, QColor(80, 80, 80)); + plotGradient.setColorAt(1, QColor(50, 50, 50)); + sensorGraphs->setBackground(plotGradient); + QLinearGradient axisRectGradient; + axisRectGradient.setStart(0, 0); + axisRectGradient.setFinalStop(0, 350); + axisRectGradient.setColorAt(0, QColor(80, 80, 80)); + axisRectGradient.setColorAt(1, QColor(30, 30, 30)); + sensorGraphs->axisRect()->setBackground(axisRectGradient); + + QSharedPointer dateTicker(new QCPAxisTickerDateTime); + dateTicker->setDateTimeFormat("hh:mm"); + dateTicker->setTickCount(2); + sensorGraphs->xAxis->setTicker(dateTicker); + + // allow dragging in all directions + sensorGraphs->setInteraction(QCP::iRangeDrag, true); + sensorGraphs->setInteraction(QCP::iRangeZoom); + + // create the universal graph + QCPGraph *graph = sensorGraphs->addGraph(); + graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, QPen(Qt::black, 0), QBrush(Qt::green), 5)); + graph->setPen(QPen(Qt::darkGreen)); + graph->setBrush(QColor(10, 100, 50, 70)); + + // ensure that the 0-line is visible + sensorGraphs->yAxis->setRangeLower(0); + + sensorDataNamesGroup = new QButtonGroup(); + // enable changing the displayed sensor + connect(sensorDataNamesGroup, static_cast(&QButtonGroup::buttonClicked), [this](QAbstractButton *button) + { + selectedSensorChanged(button->objectName()); + }); + + // show current temperature below the mouse + connect(sensorGraphs, &QCustomPlot::mouseMove, this, &Ekos::Observatory::mouseOverLine); } void Observatory::weatherWarningSettingsChanged() { struct WeatherActions actions; actions.parkDome = weatherWarningDomeCB->isChecked(); actions.closeShutter = weatherWarningShutterCB->isChecked(); actions.delay = static_cast(weatherWarningDelaySB->value()); getWeatherModel()->setWarningActions(actions); } void Observatory::weatherAlertSettingsChanged() { struct WeatherActions actions; actions.parkDome = weatherAlertDomeCB->isChecked(); actions.closeShutter = weatherAlertShutterCB->isChecked(); actions.delay = static_cast(weatherAlertDelaySB->value()); getWeatherModel()->setAlertActions(actions); } void Observatory::observatoryStatusChanged(bool ready) { // statusReadyButton->setEnabled(!ready); statusReadyButton->setChecked(ready); emit newStatus(ready); } void Observatory::domeAzimuthChanged(double position) { domeAzimuthPosition->setText(QString::number(position, 'f', 2)); } void Observatory::setWarningActions(WeatherActions actions) { weatherWarningDomeCB->setChecked(actions.parkDome); weatherWarningShutterCB->setChecked(actions.closeShutter); weatherWarningDelaySB->setValue(static_cast(actions.delay)); } void Observatory::setAlertActions(WeatherActions actions) { weatherAlertDomeCB->setChecked(actions.parkDome); weatherAlertShutterCB->setChecked(actions.closeShutter); weatherAlertDelaySB->setValue(static_cast(actions.delay)); } void Observatory::toggleButtons(QPushButton *buttonPressed, QString titlePressed, QPushButton *buttonCounterpart, QString titleCounterpart) { buttonPressed->setEnabled(false); buttonPressed->setText(titlePressed); buttonCounterpart->setEnabled(true); buttonCounterpart->setChecked(false); buttonCounterpart->setCheckable(false); buttonCounterpart->setText(titleCounterpart); } void Observatory::activateButton(QPushButton *button, QString title) { button->setEnabled(true); button->setCheckable(false); button->setText(title); } void Observatory::buttonPressed(QPushButton *button, QString title) { button->setEnabled(false); button->setCheckable(true); button->setChecked(true); button->setText(title); } void Observatory::statusControlSettingsChanged() { ObservatoryStatusControl control; control.useDome = useDomeCB->isChecked(); control.useShutter = useShutterCB->isChecked(); control.useWeather = useWeatherCB->isChecked(); mObservatoryModel->setStatusControl(control); } void Observatory::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_OBSERVATORY) << text; emit newLog(text); } void Observatory::clearLog() { m_LogText.clear(); emit newLog(QString()); } } diff --git a/kstars/ekos/observatory/observatory.h b/kstars/ekos/observatory/observatory.h index 51a11d9b8..b87e461f3 100644 --- a/kstars/ekos/observatory/observatory.h +++ b/kstars/ekos/observatory/observatory.h @@ -1,120 +1,143 @@ /* Ekos Observatory Module Copyright (C) Wolfgang Reissenberger 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_observatory.h" #include "observatorymodel.h" #include "observatorydomemodel.h" #include "observatoryweathermodel.h" #include #include #include namespace Ekos { class Observatory : public QWidget, public Ui::Observatory { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.kstars.Ekos.Observatory") Q_PROPERTY(QStringList logText READ logText NOTIFY newLog) public: Observatory(); ObservatoryDomeModel *getDomeModel() { return mObservatoryModel->getDomeModel(); } ObservatoryWeatherModel *getWeatherModel() { return mObservatoryModel->getWeatherModel(); } // Logging QStringList logText() { return m_LogText; } QString getLogText() { return m_LogText.join("\n"); } void clearLog(); signals: Q_SCRIPTABLE void newLog(const QString &text); /** * @brief Signal a new observatory status */ Q_SCRIPTABLE void newStatus(bool isReady); private: ObservatoryModel *mObservatoryModel = nullptr; void setDomeModel(ObservatoryDomeModel *model); void setWeatherModel(ObservatoryWeatherModel *model); // motion control void enableMotionControl(bool enabled); // slaving control void enableAutoSync(bool enabled); void showAutoSync(bool enabled); // Logging QStringList m_LogText; void appendLogText(const QString &); // timer for refreshing the observatory status QTimer weatherStatusTimer; // reacting on weather changes void setWarningActions(WeatherActions actions); void setAlertActions(WeatherActions actions); // button handling void toggleButtons(QPushButton *buttonPressed, QString titlePressed, QPushButton *buttonCounterpart, QString titleCounterpart); void activateButton(QPushButton *button, QString title); void buttonPressed(QPushButton *button, QString title); // weather sensor data - QFormLayout* sensorDataBoxLayout; - QList*> sensorDataWidgets; + QGridLayout* sensorDataBoxLayout; + // map id -> (label, widget) + std::map*> sensorDataWidgets = {}; + // map id -> graph key x value vector + std::map*> sensorGraphData = {}; + + // map id -> range (+1: values only > 0, 0: values > 0 and < 0; -1: values < 0) + std::map sensorRanges = {}; + + // selected sensor for graph display + QString selectedSensorID = ""; + + // button group for sensor names to ensure, that only one button is pressed + QButtonGroup *sensorDataNamesGroup; + + void initSensorGraphs(); + void updateSensorData(std::vector weatherData); + void updateSensorGraph(QString label, QDateTime now, double value); private slots: // observatory status handling void setObseratoryStatusControl(ObservatoryStatusControl control); void statusControlSettingsChanged(); void initWeather(); + void enableWeather(bool enable); + void clearSensorDataHistory(); void shutdownWeather(); void setWeatherStatus(ISD::Weather::Status status); + // sensor data graphs + void mouseOverLine(QMouseEvent *event); // reacting on weather changes void weatherWarningSettingsChanged(); void weatherAlertSettingsChanged(); + // reacting on sensor selection change + void selectedSensorChanged(QString id); + // reacting on observatory status changes void observatoryStatusChanged(bool ready); void domeAzimuthChanged(double position); void initDome(); void shutdownDome(); void setDomeStatus(ISD::Dome::Status status); void setDomeParkStatus(ISD::ParkStatus status); void setShutterStatus(ISD::Dome::ShutterStatus status); }; } diff --git a/kstars/ekos/observatory/observatory.ui b/kstars/ekos/observatory/observatory.ui index c979f5adc..4f43c52c8 100644 --- a/kstars/ekos/observatory/observatory.ui +++ b/kstars/ekos/observatory/observatory.ui @@ -1,1096 +1,1161 @@ Observatory 0 0 800 600 Observatory - Status of the Observatory + 3 3 3 3 3 3 3 false Dome 3 3 3 3 160 0 320 16777215 24 75 true 0 Qt::AlignCenter Position 3 3 3 3 - true + false Motion false Absolute position the dome should move. 999.990000000000009 false 96 36 96 36 Move the dome to the given absolute position. Move (abs) false Relative position the dome should move. -999.990000000000009 999.990000000000009 false 96 36 96 36 Move the dome for the given degrees and direction. QPushButton:checked { background-color: maroon; border: 1px outset; font-weight:bold; } Move (rel) false 96 36 96 36 Rotate clockwise QPushButton:checked { background-color: maroon; border: 1px outset; font-weight:bold; } &CW true false 96 36 96 36 Rotate counter clockwise QPushButton:checked { background-color: maroon; border: 1px outset; font-weight:bold; } CCW true 3 3 3 3 Slaving Qt::Horizontal 40 20 96 36 96 36 <html><head/><body><p>Enable slaving, dome motion <span style=" font-weight:600;">follows telescope motion</span></p></body></html> QPushButton:checked { background-color: maroon; border: 1px outset; font-weight:bold; } Enable true 96 36 96 36 <html><head/><body><p>Disable slaving, dome <span style=" font-weight:600;">does not follow telescope motion</span>.</p></body></html> QPushButton:checked { background-color: maroon; border: 1px outset; font-weight:bold; } Disable true 3 3 3 3 false 96 36 96 36 <html><head/><body><p>Park the dome. For advanced control of the dome please use the INDI tab.</p></body></html> QPushButton:checked { background-color: maroon; border: 1px outset; font-weight:bold; } Park 32 16 false false 96 36 96 36 <html><head/><body><p>Unpark the dome. For advanced control of the dome please use the INDI tab.</p></body></html> QPushButton:checked { background-color: maroon; border: 1px outset; font-weight:bold; } UnPark false false Qt::Horizontal 40 20 false 96 36 96 36 Abort dome motion QPushButton:checked { background-color: maroon; border: 1px outset; font-weight:bold; } Abort false Shutter 3 3 3 3 3 Qt::Horizontal 40 20 false 96 36 72 36 <html><head/><body><p>Close the shutter of the dome. For advanced control of the dome please use the INDI tab.</p></body></html> QPushButton:checked { background-color: maroon; border: 1px outset; font-weight:bold; } Close 32 16 false false 96 36 72 36 <html><head/><body><p>Open the shutter of the dome. For advanced control of the dome please use the INDI tab.</p></body></html> QPushButton:checked { background-color: maroon; border: 1px outset; font-weight:bold; } Open false false false Observatory Status 3 3 3 3 3 <html><head/><body><p>If selected, the dome needs to be unparked for the observatory status being &quot;READY&quot;.</p></body></html> Dome <html><head/><body><p>If selected, the shutter needs to be open for the observatory status being &quot;READY&quot;.</p></body></html> Shutter <html><head/><body><p>If selected, the weather needs to be OK for the observatory status being &quot;READY&quot;.</p></body></html> Weather false 96 36 72 36 <html><head/><body><p>Observatory status. Select the observatory elements that are relevant for the status:</p> <ul> <li><b>Dome</b>: unparked &rarr; ready</li> <li><b>Shutter</b>: open &rarr; ready</li> <li><b>Weather</b>: OK &rarr; ready</li> </ul> </body></html> QPushButton:checked { background-color: maroon; border: 1px outset; font-weight:bold; } Ready 32 16 true false false Weather 3 3 3 3 3 - - - - - 0 - 0 - - - - - 48 - 48 - - - - - 48 - 48 - - - - - - - - - - - false - - - Display the weather status. The warning and alert limits are set in the INDI tab. - - - Weather Status: - - - - + false Actions 3 3 3 3 3 War&ning true 3 0 3 0 3 Park Dome Close Shutter Stop Scheduler <html><head/><body><p><span style=" font-style:italic;">Status: inactive</span></p></body></html> Qt::AutoText Qt::Horizontal 40 20 Delay (sec): Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 9999 Ale&rt true 3 3 3 0 3 Close Shutter Park Dome Stop Scheduler <html><head/><body><p><span style=" font-style:italic;">Status: inactive</span></p></body></html> Qt::AutoText Qt::Horizontal 40 20 Delay (sec): Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 9999 - + + + + + 0 + 0 + + + + + 28 + 28 + + + + + 28 + 28 + + + + + + + + Qt::Horizontal + + QSizePolicy::Fixed + - 40 + 10 20 + + + + + 0 + 0 + + + + + + + - - - Sensor Data + + + + 140 + 0 + + + + <html><head/><body><p>Current data of the weather sensors. Click on the sensor name to display its data over time.</p></body></html> + + + + + + + false + + + + 24 + 24 + + + + + 24 + 24 + + + + <html><head/><body><p>Clear sensor data history</p></body></html> + + + + + + + .. + + + + 24 + 24 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + Qt::Vertical 20 3 + + + QCustomPlot + QWidget +
auxiliary/qcustomplot.h
+ 1 +
+