From 2be14f944a7e401725b09619eb3b3526675751f6 Mon Sep 17 00:00:00 2001 From: srcejon Date: Thu, 28 Mar 2024 15:26:23 +0000 Subject: [PATCH] Add util classes for getting data from GOES, SDO, Solar Orbiter STIX and Fermi satellites. --- sdrbase/CMakeLists.txt | 8 + sdrbase/util/goesxray.cpp | 223 +++++++++++++ sdrbase/util/goesxray.h | 96 ++++++ sdrbase/util/grb.cpp | 198 +++++++++++ sdrbase/util/grb.h | 81 +++++ sdrbase/util/solardynamicsobservatory.cpp | 386 ++++++++++++++++++++++ sdrbase/util/solardynamicsobservatory.h | 81 +++++ sdrbase/util/stix.cpp | 176 ++++++++++ sdrbase/util/stix.h | 83 +++++ 9 files changed, 1332 insertions(+) create mode 100644 sdrbase/util/goesxray.cpp create mode 100644 sdrbase/util/goesxray.h create mode 100644 sdrbase/util/grb.cpp create mode 100644 sdrbase/util/grb.h create mode 100644 sdrbase/util/solardynamicsobservatory.cpp create mode 100644 sdrbase/util/solardynamicsobservatory.h create mode 100644 sdrbase/util/stix.cpp create mode 100644 sdrbase/util/stix.h diff --git a/sdrbase/CMakeLists.txt b/sdrbase/CMakeLists.txt index 391057dc9..4553695a8 100644 --- a/sdrbase/CMakeLists.txt +++ b/sdrbase/CMakeLists.txt @@ -238,7 +238,9 @@ set(sdrbase_SOURCES util/flightinformation.cpp util/ft8message.cpp util/giro.cpp + util/goesxray.cpp util/golay2312.cpp + util/grb.cpp util/httpdownloadmanager.cpp util/interpolation.cpp util/kiwisdrlist.cpp @@ -266,8 +268,10 @@ set(sdrbase_SOURCES util/samplesourceserializer.cpp util/simpleserializer.cpp util/serialutil.cpp + util/solardynamicsobservatory.cpp #util/spinlock.cpp util/spyserverlist.cpp + util/stix.cpp util/rtty.cpp util/uid.cpp util/units.cpp @@ -487,7 +491,9 @@ set(sdrbase_HEADERS util/flightinformation.h util/ft8message.h util/giro.h + util/goesxray.h util/golay2312.h + util/grb.h util/httpdownloadmanager.h util/incrementalarray.h util/incrementalvector.h @@ -520,8 +526,10 @@ set(sdrbase_HEADERS util/samplesourceserializer.h util/simpleserializer.h util/serialutil.h + util/solardynamicsobservatory.h #util/spinlock.h util/spyserverlist.h + util/stix.h util/uid.h util/units.h util/timeutil.h diff --git a/sdrbase/util/goesxray.cpp b/sdrbase/util/goesxray.cpp new file mode 100644 index 000000000..6e976f21c --- /dev/null +++ b/sdrbase/util/goesxray.cpp @@ -0,0 +1,223 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// 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 as 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "goesxray.h" + +#include +#include +#include +#include +#include + +GOESXRay::GOESXRay() +{ + m_networkManager = new QNetworkAccessManager(); + connect(m_networkManager, &QNetworkAccessManager::finished, this, &GOESXRay::handleReply); + connect(&m_dataTimer, &QTimer::timeout, this, &GOESXRay::getData); +} + + +GOESXRay::~GOESXRay() +{ + disconnect(&m_dataTimer, &QTimer::timeout, this, &GOESXRay::getData); + disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &GOESXRay::handleReply); + delete m_networkManager; +} + +GOESXRay* GOESXRay::create(const QString& service) +{ + if (service == "services.swpc.noaa.gov") + { + return new GOESXRay(); + } + else + { + qDebug() << "GOESXRay::create: Unsupported service: " << service; + return nullptr; + } +} + +void GOESXRay::getDataPeriodically(int periodInMins) +{ + if (periodInMins > 0) + { + m_dataTimer.setInterval(periodInMins*60*1000); + m_dataTimer.start(); + getData(); + } + else + { + m_dataTimer.stop(); + } +} + +void GOESXRay::getData() +{ + // Around 160kB per file + QUrl url(QString("https://services.swpc.noaa.gov/json/goes/primary/xrays-6-hour.json")); + m_networkManager->get(QNetworkRequest(url)); + + QUrl secondaryURL(QString("https://services.swpc.noaa.gov/json/goes/secondary/xrays-6-hour.json")); + m_networkManager->get(QNetworkRequest(secondaryURL)); + + QUrl protonPrimaryURL(QString("https://services.swpc.noaa.gov/json/goes/primary/integral-protons-plot-6-hour.json")); + m_networkManager->get(QNetworkRequest(protonPrimaryURL)); +} + +bool GOESXRay::containsNonNull(const QJsonObject& obj, const QString &key) const +{ + if (obj.contains(key)) + { + QJsonValue val = obj.value(key); + return !val.isNull(); + } + return false; +} + +void GOESXRay::handleReply(QNetworkReply* reply) +{ + if (reply) + { + if (!reply->error()) + { + QByteArray bytes = reply->readAll(); + bool primary = reply->url().toString().contains("primary"); + + if (reply->url().fileName() == "xrays-6-hour.json") { + handleXRayJson(bytes, primary); + } else if (reply->url().fileName() == "integral-protons-plot-6-hour.json") { + handleProtonJson(bytes, primary); + } else { + qDebug() << "GOESXRay::handleReply: unexpected filename: " << reply->url().fileName(); + } + } + else + { + qDebug() << "GOESXRay::handleReply: error: " << reply->error(); + } + reply->deleteLater(); + } + else + { + qDebug() << "GOESXRay::handleReply: reply is null"; + } +} + +void GOESXRay::handleXRayJson(const QByteArray& bytes, bool primary) +{ + QJsonDocument document = QJsonDocument::fromJson(bytes); + if (document.isArray()) + { + QJsonArray array = document.array(); + QList data; + for (auto valRef : array) + { + if (valRef.isObject()) + { + QJsonObject obj = valRef.toObject(); + + XRayData measurement; + + if (obj.contains(QStringLiteral("satellite"))) { + measurement.m_satellite = QString("GOES %1").arg(obj.value(QStringLiteral("satellite")).toInt()); + } + if (containsNonNull(obj, QStringLiteral("time_tag"))) { + measurement.m_dateTime = QDateTime::fromString(obj.value(QStringLiteral("time_tag")).toString(), Qt::ISODate); + } + if (containsNonNull(obj, QStringLiteral("flux"))) { + measurement.m_flux = obj.value(QStringLiteral("flux")).toDouble(); + } + if (containsNonNull(obj, QStringLiteral("energy"))) + { + QString energy = obj.value(QStringLiteral("energy")).toString(); + if (energy == "0.05-0.4nm") { + measurement.m_band = XRayData::SHORT; + } else if (energy == "0.1-0.8nm") { + measurement.m_band = XRayData::LONG; + } else { + qDebug() << "GOESXRay::handleXRayJson: Unknown energy: " << energy; + } + } + + data.append(measurement); + } + else + { + qDebug() << "GOESXRay::handleXRayJson: Array element is not an object: " << valRef; + } + } + if (data.size() > 0) { + emit xRayDataUpdated(data, primary); + } else { + qDebug() << "GOESXRay::handleXRayJson: No data in array: " << document; + } + } + else + { + qDebug() << "GOESXRay::handleXRayJson: Document is not an array: " << document; + } +} + +void GOESXRay::handleProtonJson(const QByteArray& bytes, bool primary) +{ + QJsonDocument document = QJsonDocument::fromJson(bytes); + if (document.isArray()) + { + QJsonArray array = document.array(); + QList data; + for (auto valRef : array) + { + if (valRef.isObject()) + { + QJsonObject obj = valRef.toObject(); + + ProtonData measurement; + + if (obj.contains(QStringLiteral("satellite"))) { + measurement.m_satellite = QString("GOES %1").arg(obj.value(QStringLiteral("satellite")).toInt()); + } + if (containsNonNull(obj, QStringLiteral("time_tag"))) { + measurement.m_dateTime = QDateTime::fromString(obj.value(QStringLiteral("time_tag")).toString(), Qt::ISODate); + } + if (containsNonNull(obj, QStringLiteral("flux"))) { + measurement.m_flux = obj.value(QStringLiteral("flux")).toDouble(); + } + if (containsNonNull(obj, QStringLiteral("energy"))) + { + QString energy = obj.value(QStringLiteral("energy")).toString(); + QString value = energy.mid(2).split(' ')[0]; + measurement.m_energy = value.toInt(); // String like: ">=50 MeV" + } + + data.append(measurement); + } + else + { + qDebug() << "GOESXRay::handleProtonJson: Array element is not an object: " << valRef; + } + } + if (data.size() > 0) { + emit protonDataUpdated(data, primary); + } else { + qDebug() << "GOESXRay::handleProtonJson: No data in array: " << document; + } + } + else + { + qDebug() << "GOESXRay::handleProtonJson: Document is not an array: " << document; + } +} diff --git a/sdrbase/util/goesxray.h b/sdrbase/util/goesxray.h new file mode 100644 index 000000000..c7e6d0fb7 --- /dev/null +++ b/sdrbase/util/goesxray.h @@ -0,0 +1,96 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// 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 as 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_GOESXRAY_H +#define INCLUDE_GOESXRAY_H + +#include +#include +#include + +#include "export.h" + +class QNetworkAccessManager; +class QNetworkReply; + +// GOES X-Ray data +// This gets 1-minute averages of solar X-rays the 1-8 Angstrom (0.1-0.8 nm) and 0.5-4.0 Angstrom (0.05-0.4 nm) passbands from the GOES satellites +// https://www.swpc.noaa.gov/products/goes-x-ray-flux +// There are primary and secondary data sources, from different satellites, as sometimes they can be in eclipse +// Also gets Proton flux (Which may be observed on Earth a couple of days after a large flare/CME) +class SDRBASE_API GOESXRay : public QObject +{ + Q_OBJECT +protected: + GOESXRay(); + +public: + struct XRayData { + QDateTime m_dateTime; + QString m_satellite; + double m_flux; + enum Band { + UNKNOWN, + SHORT, // 0.05-0.4nm + LONG // 0.1-0.8nm + } m_band; + XRayData() : + m_flux(NAN), + m_band(UNKNOWN) + { + } + }; + + struct ProtonData { + QDateTime m_dateTime; + QString m_satellite; + double m_flux; + int m_energy; // 10=10MeV, 50MeV, 100MeV, 500MeV + ProtonData() : + m_flux(NAN), + m_energy(0) + { + } + }; + + static GOESXRay* create(const QString& service="services.swpc.noaa.gov"); + + ~GOESXRay(); + void getDataPeriodically(int periodInMins=10); + +public slots: + void getData(); + +private slots: + void handleReply(QNetworkReply* reply); + +signals: + void xRayDataUpdated(const QList& data, bool primary); // Called when new data available. + void protonDataUpdated(const QList &data, bool primary); + +private: + bool containsNonNull(const QJsonObject& obj, const QString &key) const; + void handleXRayJson(const QByteArray& bytes, bool primary); + void handleProtonJson(const QByteArray& bytes, bool primary); + + QTimer m_dataTimer; // Timer for periodic updates + QNetworkAccessManager *m_networkManager; + +}; + +#endif /* INCLUDE_GOESXRAY_H */ + diff --git a/sdrbase/util/grb.cpp b/sdrbase/util/grb.cpp new file mode 100644 index 000000000..e42593b68 --- /dev/null +++ b/sdrbase/util/grb.cpp @@ -0,0 +1,198 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Jon Beniston, M7RCE // +// // +// 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 as 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "grb.h" +#include "util/csv.h" + +#include +#include +#include +#include +#include + +GRB::GRB() +{ + connect(&m_dataTimer, &QTimer::timeout, this,&GRB::getData); + m_networkManager = new QNetworkAccessManager(); + connect(m_networkManager, &QNetworkAccessManager::finished, this, &GRB::handleReply); + + QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation); + QDir writeableDir(locations[0]); + if (!writeableDir.mkpath(QStringLiteral("cache") + QDir::separator() + QStringLiteral("grb"))) { + qDebug() << "Failed to create cache/grb"; + } + + m_cache = new QNetworkDiskCache(); + m_cache->setCacheDirectory(locations[0] + QDir::separator() + QStringLiteral("cache") + QDir::separator() + QStringLiteral("grb")); + m_cache->setMaximumCacheSize(100000000); + m_networkManager->setCache(m_cache); +} + +GRB::~GRB() +{ + disconnect(&m_dataTimer, &QTimer::timeout, this, &GRB::getData); + disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &GRB::handleReply); + delete m_networkManager; +} + +GRB* GRB::create() +{ + return new GRB(); +} + +void GRB::getDataPeriodically(int periodInMins) +{ + if (periodInMins > 0) + { + m_dataTimer.setInterval(periodInMins*60*1000); + m_dataTimer.start(); + getData(); + } + else + { + m_dataTimer.stop(); + } +} + +void GRB::getData() +{ + QUrl url("https://user-web.icecube.wisc.edu/~grbweb_public/Summary_table.txt"); + + m_networkManager->get(QNetworkRequest(url)); +} + +void GRB::handleReply(QNetworkReply* reply) +{ + if (reply) + { + if (!reply->error()) + { + if (reply->url().fileName().endsWith(".txt")) + { + QByteArray bytes = reply->readAll(); + handleText(bytes); + } + else + { + qDebug() << "GRB::handleReply: Unexpected file" << reply->url().fileName(); + } + } + else + { + qDebug() << "GRB::handleReply: Error: " << reply->error(); + } + reply->deleteLater(); + } + else + { + qDebug() << "GRB::handleReply: Reply is null"; + } +} + +void GRB::handleText(QByteArray& bytes) +{ + // Convert to CSV + QString s(bytes); + QStringList l = s.split("\n"); + for (int i = 0; i < l.size(); i++) { + l[i] = l[i].simplified().replace(" ", ","); + } + s = l.join("\n"); + + QTextStream in(&s); + + // Skip header + for (int i = 0; i < 4; i++) { + in.readLine(); + } + + QList grbs; + QStringList cols; + while(CSV::readRow(in, &cols)) + { + Data grb; + + if (cols.length() >= 10) + { + grb.m_name = cols[0]; + grb.m_fermiName = cols[1]; + int year = grb.m_name.mid(3, 2).toInt(); + if (year >= 90) { + year += 1900; + } else { + year += 2000; + } + QDate date(year, grb.m_name.mid(5, 2).toInt(), grb.m_name.mid(7, 2).toInt()); + QTime time = QTime::fromString(cols[2]); + grb.m_dateTime = QDateTime(date, time); + grb.m_ra = cols[3].toFloat(); + grb.m_dec = cols[4].toFloat(); + grb.m_fluence = cols[9].toFloat(); + + //qDebug() << grb.m_name << grb.m_dateTime.toString() << grb.m_ra << grb.m_dec << grb.m_fluence ; + + if (grb.m_dateTime.isValid()) { + grbs.append(grb); + } + } + } + + emit dataUpdated(grbs); +} + + QString GRB::Data::getFermiURL() const +{ + if (m_fermiName.isEmpty() || (m_fermiName == "None")) { + return ""; + } + QString base = "https://heasarc.gsfc.nasa.gov/FTP/fermi/data/gbm/bursts/"; + QString yearDir = "20" + m_fermiName.mid(3, 2); + QString dataDir = m_fermiName; + dataDir.replace("GRB", "bn"); + return base + yearDir + "/" + dataDir + "/current/"; +} + +QString GRB::Data::getFermiPlotURL() const +{ + QString base = getFermiURL(); + if (base.isEmpty()) { + return ""; + } + + QString name = m_fermiName; + name.replace("GRB", "bn"); + return getFermiURL() + "glg_lc_all_" + name + "_v00.gif"; // Could be v01.gif? How to know without fetching index? +} + +QString GRB::Data::getFermiSkyMapURL() const +{ + QString base = getFermiURL(); + if (base.isEmpty()) { + return ""; + } + + QString name = m_fermiName; + name.replace("GRB", "bn"); + return getFermiURL() + "glg_skymap_all_" + name + "_v00.png"; +} + +QString GRB::Data::getSwiftURL() const +{ + QString name = m_name; + name.replace("GRB", ""); + return "https://swift.gsfc.nasa.gov/archive/grb_table/" + name; +} diff --git a/sdrbase/util/grb.h b/sdrbase/util/grb.h new file mode 100644 index 000000000..a0075de19 --- /dev/null +++ b/sdrbase/util/grb.h @@ -0,0 +1,81 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Jon Beniston, M7RCE // +// // +// 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 as 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_GRB_H +#define INCLUDE_GRB_H + +#include +#include + +#include "export.h" + +class QNetworkAccessManager; +class QNetworkReply; +class QNetworkDiskCache; + +// GRB (Gamma Ray Burst) database +// Gets GRB database from GRBweb https://user-web.icecube.wisc.edu/~grbweb_public/ +// Uses summary .txt file so only contains last 1000 GRBs +class SDRBASE_API GRB : public QObject +{ + Q_OBJECT +protected: + GRB(); + +public: + + struct SDRBASE_API Data { + + QString m_name; // E.g: GRB240310A + QString m_fermiName; // Name used by Fermi telescope. E.g. GRB240310236. Can be None if not detected by Fermi + QDateTime m_dateTime; + float m_ra; // Right Ascension + float m_dec; // Declination + float m_fluence; // erg/cm^2 + + QString getFermiURL() const; // Get URL where Fermi data is stored + QString getFermiPlotURL() const; + QString getFermiSkyMapURL() const; + QString getSwiftURL() const; + + }; + + static GRB* create(); + + ~GRB(); + void getDataPeriodically(int periodInMins=1440); // GRBweb is updated every 24 hours, usually just after 9am UTC + +public slots: + void getData(); + +private slots: + void handleReply(QNetworkReply* reply); + +signals: + void dataUpdated(const QListdata); // Called when new data is available. + +private: + + QTimer m_dataTimer; // Timer for periodic updates + QNetworkAccessManager *m_networkManager; + QNetworkDiskCache *m_cache; + + void handleText(QByteArray& bytes); + +}; + +#endif /* INCLUDE_GRB_H */ diff --git a/sdrbase/util/solardynamicsobservatory.cpp b/sdrbase/util/solardynamicsobservatory.cpp new file mode 100644 index 000000000..11c609852 --- /dev/null +++ b/sdrbase/util/solardynamicsobservatory.cpp @@ -0,0 +1,386 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Jon Beniston, M7RCE // +// // +// 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 as 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "solardynamicsobservatory.h" + +#include +#include +#include +#include + +SolarDynamicsObservatory::SolarDynamicsObservatory() : + m_size(512) +{ + connect(&m_dataTimer, &QTimer::timeout, this, qOverload<>(&SolarDynamicsObservatory::getImage)); + m_networkManager = new QNetworkAccessManager(); + connect(m_networkManager, &QNetworkAccessManager::finished, this, &SolarDynamicsObservatory::handleReply); + + QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation); + QDir writeableDir(locations[0]); + if (!writeableDir.mkpath(QStringLiteral("cache") + QDir::separator() + QStringLiteral("solardynamicsobservatory"))) { + qDebug() << "SolarDynamicsObservatory::SolarDynamicsObservatory: Failed to create cache/solardynamicsobservatory"; + } + + m_cache = new QNetworkDiskCache(); + m_cache->setCacheDirectory(locations[0] + QDir::separator() + QStringLiteral("cache") + QDir::separator() + QStringLiteral("solardynamicsobservatory")); + m_cache->setMaximumCacheSize(100000000); + m_networkManager->setCache(m_cache); +} + +SolarDynamicsObservatory::~SolarDynamicsObservatory() +{ + disconnect(&m_dataTimer, &QTimer::timeout, this, qOverload<>(&SolarDynamicsObservatory::getImage)); + disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &SolarDynamicsObservatory::handleReply); + delete m_networkManager; +} + +SolarDynamicsObservatory* SolarDynamicsObservatory::create() +{ + return new SolarDynamicsObservatory(); +} + +QList SolarDynamicsObservatory::getImageSizes() +{ + return {512, 1024, 2048, 4096}; +} + +QList SolarDynamicsObservatory::getVideoSizes() +{ + return {512, 1024}; +} + +const QStringList SolarDynamicsObservatory::getImageNames() +{ + QChar angstronm(0x212B); + QStringList names; + + // SDO + names.append(QString("AIA 094 %1").arg(angstronm)); + names.append(QString("AIA 131 %1").arg(angstronm)); + names.append(QString("AIA 171 %1").arg(angstronm)); + names.append(QString("AIA 193 %1").arg(angstronm)); + names.append(QString("AIA 211 %1").arg(angstronm)); + names.append(QString("AIA 304 %1").arg(angstronm)); + names.append(QString("AIA 335 %1").arg(angstronm)); + names.append(QString("AIA 1600 %1").arg(angstronm)); + names.append(QString("AIA 1700 %1").arg(angstronm)); + names.append(QString("AIA 211 %1, 193 %1, 171 %1").arg(angstronm)); + names.append(QString("AIA 304 %1, 211 %1, 171 %1").arg(angstronm)); + names.append(QString("AIA 094 %1, 335 %1, 193 %1").arg(angstronm)); + names.append(QString("AIA 171 %1, HMIB").arg(angstronm)); + names.append("HMI Magneotgram"); + names.append("HMI Colorized Magneotgram"); + names.append("HMI Intensitygram - Colored"); + names.append("HMI Intensitygram - Flattened"); + names.append("HMI Intensitygram"); + names.append("HMI Dopplergram"); + + // SOHO + names.append("LASCO C2"); + names.append("LASCO C3"); + + return names; +} + +const QStringList SolarDynamicsObservatory::getChannelNames() +{ + QStringList channelNames = { + "0094", + "0131", + "0171", + "0193", + "0211", + "0304", + "0335", + "1600", + "1700", + "211193171", + "304211171", + "094335193", + "HMImag", + "HMIB", + "HMIBC", + "HMIIC", + "HMIIF", + "HMII", + "HMID", + "c2", + "c3" + }; + + return channelNames; +} + +const QStringList SolarDynamicsObservatory::getImageFileNames() +{ + // Ordering needs to match getImageNames() + // %1 replaced with size + QStringList filenames = { + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_0094.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_0131.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_0171.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_0193.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_0211.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_0304.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_0335.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_1600.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_1700.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_211193171.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/f_304_211_171_%1.jpg", + //"https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_304211171.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/f_094_335_193_%1.jpg", + //"https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_094335193.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/f_HMImag_171_%1.jpg", + //"https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_HMImag.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_HMIB.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_HMIBC.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_HMIIC.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_HMIIF.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_HMII.jpg", + "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_%1_HMID.jpg", + "https://soho.nascom.nasa.gov/data/realtime/c2/512/latest.jpg", + "https://soho.nascom.nasa.gov/data/realtime/c3/512/latest.jpg" + }; + + return filenames; +} + +const QStringList SolarDynamicsObservatory::getVideoNames() +{ + QChar angstronm(0x212B); + QStringList names; + + // SDO + names.append(QString("AIA 094 %1").arg(angstronm)); + names.append(QString("AIA 131 %1").arg(angstronm)); + names.append(QString("AIA 171 %1").arg(angstronm)); + names.append(QString("AIA 193 %1").arg(angstronm)); + names.append(QString("AIA 211 %1").arg(angstronm)); + names.append(QString("AIA 304 %1").arg(angstronm)); + names.append(QString("AIA 335 %1").arg(angstronm)); + names.append(QString("AIA 1600 %1").arg(angstronm)); + names.append(QString("AIA 1700 %1").arg(angstronm)); + + // SOHO + names.append("LASCO C2"); + names.append("LASCO C3"); + + return names; +} + +const QStringList SolarDynamicsObservatory::getVideoFileNames() +{ + const QStringList filenames = { + "http://sdo.gsfc.nasa.gov/assets/img/latest/mpeg/latest_%1_0094.mp4", // Videos sometimes fail to load on Windows if https used + "http://sdo.gsfc.nasa.gov/assets/img/latest/mpeg/latest_%1_0131.mp4", + "http://sdo.gsfc.nasa.gov/assets/img/latest/mpeg/latest_%1_0171.mp4", + "http://sdo.gsfc.nasa.gov/assets/img/latest/mpeg/latest_%1_0193.mp4", + "http://sdo.gsfc.nasa.gov/assets/img/latest/mpeg/latest_%1_0211.mp4", + "http://sdo.gsfc.nasa.gov/assets/img/latest/mpeg/latest_%1_0304.mp4", + "http://sdo.gsfc.nasa.gov/assets/img/latest/mpeg/latest_%1_0335.mp4", + "http://sdo.gsfc.nasa.gov/assets/img/latest/mpeg/latest_%1_1600.mp4", + "http://sdo.gsfc.nasa.gov/assets/img/latest/mpeg/latest_%1_1700.mp4", + "http://soho.nascom.nasa.gov/data/LATEST/current_c2.mp4", + "http://soho.nascom.nasa.gov/data/LATEST/current_c3.mp4", + }; + + return filenames; +} + +QString SolarDynamicsObservatory::getImageURL(const QString& image, int size) +{ + const QStringList names = SolarDynamicsObservatory::getImageNames(); + const QStringList filenames = SolarDynamicsObservatory::getImageFileNames(); + int idx = names.indexOf(image); + + if (idx != -1) { + return QString(filenames[idx]).arg(size); + } else { + return ""; + } +} + +QString SolarDynamicsObservatory::getVideoURL(const QString& video, int size) +{ + const QStringList names = SolarDynamicsObservatory::getVideoNames(); + const QStringList filenames = SolarDynamicsObservatory::getVideoFileNames(); + int idx = names.indexOf(video); + + if (idx != -1) { + return QString(filenames[idx]).arg(size); + } else { + return ""; + } +} + +void SolarDynamicsObservatory::getImagePeriodically(const QString& image, int size, int periodInMins) +{ + m_image = image; + m_size = size; + if (periodInMins > 0) + { + m_dataTimer.setInterval(periodInMins*60*1000); + m_dataTimer.start(); + getImage(); + } + else + { + m_dataTimer.stop(); + } +} + +void SolarDynamicsObservatory::getImage() +{ + getImage(m_image, m_size); +} + +void SolarDynamicsObservatory::getImage(const QString& imageName, int size) +{ + QString urlString = getImageURL(imageName, size); + if (!urlString.isEmpty()) + { + QUrl url(urlString); + + m_networkManager->get(QNetworkRequest(url)); + } +} + +void SolarDynamicsObservatory::getImage(const QString& imageName, QDateTime dateTime, int size) +{ + // Stop periodic updates, if not after latest data + m_dataTimer.stop(); + + // Get file index, as we don't know what time will be used in the file + QDate date = dateTime.date(); + QString urlString = QString("https://sdo.gsfc.nasa.gov/assets/img/browse/%1/%2/%3/") + .arg(date.year()) + .arg(date.month(), 2, 10, QLatin1Char('0')) + .arg(date.day(), 2, 10, QLatin1Char('0')); + QUrl url(urlString); + + // Save details of image we are after + m_dateTime = dateTime; + m_size = size; + m_image = imageName; + + m_networkManager->get(QNetworkRequest(url)); +} + +void SolarDynamicsObservatory::handleReply(QNetworkReply* reply) +{ + if (reply) + { + if (!reply->error()) + { + if (reply->url().fileName().endsWith(".jpg")) + { + handleJpeg(reply->readAll()); + } + else + { + handleIndex(reply->readAll()); + } + + } + else + { + qDebug() << "SolarDynamicsObservatory::handleReply: Error: " << reply->error(); + } + reply->deleteLater(); + } + else + { + qDebug() << "SolarDynamicsObservatory::handleReply: Reply is null"; + } +} + +void SolarDynamicsObservatory::handleJpeg(const QByteArray& bytes) +{ + QImage image; + + if (image.loadFromData(bytes)) { + emit imageUpdated(image); + } else { + qWarning() << "SolarDynamicsObservatory::handleJpeg: Failed to load image"; + } +} + +void SolarDynamicsObservatory::handleIndex(const QByteArray& bytes) +{ + const QStringList names = SolarDynamicsObservatory::getImageNames(); + const QStringList channelNames = SolarDynamicsObservatory::getChannelNames(); + int idx = names.indexOf(m_image); + if (idx < 0) { + return; + } + QString channel = channelNames[idx]; + + QString file(bytes); + QStringList lines = file.split("\n"); + + QString date = m_dateTime.date().toString("yyyyMMdd"); + QString pattern = QString("\"%1_([0-9]{6})_%2_%3.jpg\"").arg(date).arg(m_size).arg(channel); + QRegularExpression re(pattern); + + // Get all times the image is available + QList times; + for (const auto& line : lines) + { + QRegularExpressionMatch match = re.match(line); + if (match.hasMatch()) + { + QString t = match.capturedTexts()[1]; + int h = t.left(2).toInt(); + int m = t.mid(2, 2).toInt(); + int s = t.right(2).toInt(); + times.append(QTime(h, m, s)); + } + } + + if (times.length() > 0) + { + QTime target = m_dateTime.time(); + QTime current = times[0]; + for (int i = 1; i < times.size(); i++) + { + if (target < times[i]) { + break; + } + current = times[i]; + } + + // Get image + QDate date = m_dateTime.date(); + QString urlString = QString("https://sdo.gsfc.nasa.gov/assets/img/browse/%1/%2/%3/%1%2%3_%4%5%6_%7_%8.jpg") + .arg(date.year()) + .arg(date.month(), 2, 10, QLatin1Char('0')) + .arg(date.day(), 2, 10, QLatin1Char('0')) + .arg(current.hour(), 2, 10, QLatin1Char('0')) + .arg(current.minute(), 2, 10, QLatin1Char('0')) + .arg(current.second(), 2, 10, QLatin1Char('0')) + .arg(m_size) + .arg(channel); + + QUrl url(urlString); + + m_networkManager->get(QNetworkRequest(url)); + } + else + { + qDebug() << "SolarDynamicsObservatory: No image available"; + } +} diff --git a/sdrbase/util/solardynamicsobservatory.h b/sdrbase/util/solardynamicsobservatory.h new file mode 100644 index 000000000..f0fd5605d --- /dev/null +++ b/sdrbase/util/solardynamicsobservatory.h @@ -0,0 +1,81 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Jon Beniston, M7RCE // +// // +// 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 as 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_SOLARDYNAMICSOBSERVATORY_H +#define INCLUDE_SOLARDYNAMICSOBSERVATORY_H + +#include +#include +#include + +#include "export.h" + +class QNetworkAccessManager; +class QNetworkReply; +class QNetworkDiskCache; + +// This gets solar imagery from SDO (Solar Dynamics Observatory) - https://sdo.gsfc.nasa.gov/ +// and LASCO images from SOHO - https://soho.nascom.nasa.gov/ +class SDRBASE_API SolarDynamicsObservatory : public QObject +{ + Q_OBJECT +protected: + SolarDynamicsObservatory(); + +public: + + static SolarDynamicsObservatory* create(); + + ~SolarDynamicsObservatory(); + void getImagePeriodically(const QString& image, int size=512, int periodInMins=15); + void getImage(const QString& m_image, int size); + void getImage(const QString& m_image, QDateTime dateTime, int size=512); + + static QString getImageURL(const QString& image, int size); + static QString getVideoURL(const QString& video, int size=512); + + static QList getImageSizes(); + static const QStringList getChannelNames(); + static const QStringList getImageNames(); + static QList getVideoSizes(); + static const QStringList getVideoNames(); + +private slots: + void getImage(); + void handleReply(QNetworkReply* reply); + +signals: + void imageUpdated(const QImage& image); // Called when new image is available. + +private: + + QTimer m_dataTimer; // Timer for periodic updates + QNetworkAccessManager *m_networkManager; + QNetworkDiskCache *m_cache; + + QString m_image; + int m_size; + QDateTime m_dateTime; + + void handleJpeg(const QByteArray& bytes); + void handleIndex(const QByteArray& bytes); + static const QStringList getImageFileNames(); + static const QStringList getVideoFileNames(); + +}; + +#endif /* INCLUDE_SOLARDYNAMICSOBSERVATORY_H */ diff --git a/sdrbase/util/stix.cpp b/sdrbase/util/stix.cpp new file mode 100644 index 000000000..9230141da --- /dev/null +++ b/sdrbase/util/stix.cpp @@ -0,0 +1,176 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Jon Beniston, M7RCE // +// // +// 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 as 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "stix.h" + +#include +#include +#include +#include +#include + +STIX::STIX() +{ + m_networkManager = new QNetworkAccessManager(); + connect(m_networkManager, &QNetworkAccessManager::finished, this, &STIX::handleReply); + connect(&m_dataTimer, &QTimer::timeout, this, &STIX::getData); +} + + +STIX::~STIX() +{ + disconnect(&m_dataTimer, &QTimer::timeout, this, &STIX::getData); + disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &STIX::handleReply); + delete m_networkManager; +} + +STIX* STIX::create() +{ + return new STIX(); +} + +void STIX::getDataPeriodically(int periodInMins) +{ + if (periodInMins > 0) + { + m_dataTimer.setInterval(periodInMins*60*1000); + m_dataTimer.start(); + getData(); + } + else + { + m_dataTimer.stop(); + } +} + +void STIX::getData() +{ + QUrlQuery data(QString("https://datacenter.stix.i4ds.net/api/request/flare-list")); + QDateTime start; + + if (m_mostRecent.isValid()) { + start = m_mostRecent; + } else { + start = QDateTime::currentDateTime().addDays(-5); + } + + data.addQueryItem("start_utc", start.toString(Qt::ISODate)); + data.addQueryItem("end_utc", QDateTime::currentDateTime().toString(Qt::ISODate)); + data.addQueryItem("sort", "time"); + + QUrl url("https://datacenter.stix.i4ds.net/api/request/flare-list"); + QNetworkRequest request(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + m_networkManager->post(request, data.toString(QUrl::FullyEncoded).toUtf8()); +} + +bool STIX::containsNonNull(const QJsonObject& obj, const QString &key) const +{ + if (obj.contains(key)) + { + QJsonValue val = obj.value(key); + return !val.isNull(); + } + return false; +} + +void STIX::handleReply(QNetworkReply* reply) +{ + if (reply) + { + if (!reply->error()) + { + QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); + + if (document.isArray()) + { + QJsonArray array = document.array(); + QList data; + for (auto valRef : array) + { + if (valRef.isObject()) + { + QJsonObject obj = valRef.toObject(); + + FlareData measurement; + + if (obj.contains(QStringLiteral("flare_id"))) { + measurement.m_id = obj.value(QStringLiteral("flare_id")).toString(); + } + if (obj.contains(QStringLiteral("start_UTC"))) + { + measurement.m_startDateTime = QDateTime::fromString(obj.value(QStringLiteral("start_UTC")).toString(), Qt::ISODate); + if (!m_mostRecent.isValid() || (measurement.m_startDateTime > m_mostRecent)) { + m_mostRecent = measurement.m_startDateTime; + } + } + if (obj.contains(QStringLiteral("end_UTC"))) { + measurement.m_endDateTime = QDateTime::fromString(obj.value(QStringLiteral("end_UTC")).toString(), Qt::ISODate); + } + if (obj.contains(QStringLiteral("peak_UTC"))) { + measurement.m_peakDateTime = QDateTime::fromString(obj.value(QStringLiteral("peak_UTC")).toString(), Qt::ISODate); + } + if (obj.contains(QStringLiteral("duration"))) { + measurement.m_duration = obj.value(QStringLiteral("duration")).toInt(); + } + if (obj.contains(QStringLiteral("GOES_flux"))) { + measurement.m_flux = obj.value(QStringLiteral("GOES_flux")).toDouble(); + } + + data.append(measurement); + } + else + { + qDebug() << "STIX::handleReply: Array element is not an object: " << valRef; + } + } + if (data.size() > 0) + { + m_data.append(data); + emit dataUpdated(m_data); + } + else + { + qDebug() << "STIX::handleReply: No data in array: " << document; + } + } + else + { + qDebug() << "STIX::handleReply: Document is not an array: " << document; + } + } + else + { + qDebug() << "STIX::handleReply: error: " << reply->error(); + } + reply->deleteLater(); + } + else + { + qDebug() << "STIX::handleReply: reply is null"; + } +} + + QString STIX::FlareData::getLightCurvesURL() const + { + return QString("https://datacenter.stix.i4ds.net/view/plot/lightcurves?start=%1&span=%2").arg(m_startDateTime.toSecsSinceEpoch()).arg(m_duration); + } + + QString STIX::FlareData::getDataURL() const + { + return QString("https://datacenter.stix.i4ds.net/view/list/fits/%1/%2").arg(m_startDateTime.toSecsSinceEpoch()).arg(m_duration); + } diff --git a/sdrbase/util/stix.h b/sdrbase/util/stix.h new file mode 100644 index 000000000..924b8e7ea --- /dev/null +++ b/sdrbase/util/stix.h @@ -0,0 +1,83 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Jon Beniston, M7RCE // +// // +// 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 as 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_STIX_H +#define INCLUDE_STIX_H + +#include +#include +#include +#include + +#include "export.h" + +class QNetworkAccessManager; +class QNetworkReply; + +// Solar Orbiter STIX (Spectrometer/Telescope for Imaging X-rays) instrument +// Gets solar flare data - Newest data is often about 24 hours old +class SDRBASE_API STIX : public QObject +{ + Q_OBJECT +protected: + STIX(); + +public: + struct SDRBASE_API FlareData { + QString m_id; + QDateTime m_startDateTime; + QDateTime m_endDateTime; + QDateTime m_peakDateTime; + int m_duration; // In seconds + double m_flux; + FlareData() : + m_duration(0), + m_flux(NAN) + { + } + + QString getLightCurvesURL() const; + QString getDataURL() const; + }; + + static STIX* create(); + + ~STIX(); + void getDataPeriodically(int periodInMins=60); + +public slots: + void getData(); + +private slots: + void handleReply(QNetworkReply* reply); + +signals: + void dataUpdated(const QList& data); // Called when new data available. + +private: + bool containsNonNull(const QJsonObject& obj, const QString &key) const; + + QTimer m_dataTimer; // Timer for periodic updates + QNetworkAccessManager *m_networkManager; + + QDateTime m_mostRecent; + QList m_data; + +}; + +#endif /* INCLUDE_STIX_H */ +