diff --git a/CMakeLists.txt b/CMakeLists.txt index 98a73cb08..d20a95429 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -141,6 +141,7 @@ option(ENABLE_FEATURE_RIGCTLSERVER "Enable feature rigctlserver plugin" ON) option(ENABLE_FEATURE_PERTESTER "Enable feature pertester plugin" ON) option(ENABLE_FEATURE_GS232CONTROLLER "Enable feature gs232controller plugin" ON) option(ENABLE_FEATURE_REMOTECONTROL "Enable feature remote control plugin" ON) +option(ENABLE_FEATURE_SKYMAP "Enable feature sky map plugin" ON) # on windows always build external libraries if(WIN32) diff --git a/doc/img/SkyMap_ESASky.png b/doc/img/SkyMap_ESASky.png new file mode 100644 index 000000000..1fcedf063 Binary files /dev/null and b/doc/img/SkyMap_ESASky.png differ diff --git a/doc/img/SkyMap_settings.png b/doc/img/SkyMap_settings.png new file mode 100644 index 000000000..ff005222e Binary files /dev/null and b/doc/img/SkyMap_settings.png differ diff --git a/doc/img/SkyMap_wwt.png b/doc/img/SkyMap_wwt.png new file mode 100644 index 000000000..d52549a3f Binary files /dev/null and b/doc/img/SkyMap_wwt.png differ diff --git a/plugins/feature/CMakeLists.txt b/plugins/feature/CMakeLists.txt index 8c1f12ccb..1be829154 100644 --- a/plugins/feature/CMakeLists.txt +++ b/plugins/feature/CMakeLists.txt @@ -12,6 +12,11 @@ else() message(STATUS "Not building map (ENABLE_FEATURE_MAP=${ENABLE_FEATURE_MAP} Qt${QT_DEFAULT_MAJOR_VERSION}Quick_FOUND=${Qt${QT_DEFAULT_MAJOR_VERSION}Quick_FOUND} Qt${QT_DEFAULT_MAJOR_VERSION}QuickWidgets_FOUND=${Qt${QT_DEFAULT_MAJOR_VERSION}QuickWidgets_FOUND} Qt${QT_DEFAULT_MAJOR_VERSION}Positioning_FOUND=${Qt${QT_DEFAULT_MAJOR_VERSION}Positioning_FOUND} Qt${QT_DEFAULT_MAJOR_VERSION}Location_FOUND=${Qt${QT_DEFAULT_MAJOR_VERSION}Location_FOUND})") endif() +# WebEngine on Qt5, WebEngineCore on Qt6 +if(ENABLE_FEATURE_SKYMAP AND Qt${QT_DEFAULT_MAJOR_VERSION}WebEngine_FOUND OR Qt${QT_DEFAULT_MAJOR_VERSION}WebEngineCore_FOUND) + add_subdirectory(skymap) +endif() + if (ENABLE_FEATURE_VORLOCALIZER AND Qt${QT_DEFAULT_MAJOR_VERSION}Quick_FOUND AND Qt${QT_DEFAULT_MAJOR_VERSION}QuickWidgets_FOUND AND Qt${QT_DEFAULT_MAJOR_VERSION}Positioning_FOUND) add_subdirectory(vorlocalizer) else() diff --git a/plugins/feature/skymap/CMakeLists.txt b/plugins/feature/skymap/CMakeLists.txt new file mode 100644 index 000000000..5d83d879f --- /dev/null +++ b/plugins/feature/skymap/CMakeLists.txt @@ -0,0 +1,92 @@ +project(skymap) + +set(skymap_SOURCES + skymap.cpp + skymapsettings.cpp + skymapplugin.cpp + skymapwebapiadapter.cpp + webserver.cpp +) + +set(skymap_HEADERS + skymap.h + skymapsettings.h + skymapplugin.h + skymapreport.h + skymapwebapiadapter.h + webserver.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client + ${Qt${QT_DEFAULT_MAJOR_VERSION}Gui_PRIVATE_INCLUDE_DIRS} +) + +if(NOT SERVER_MODE) + set(skymap_SOURCES + ${skymap_SOURCES} + skymapgui.cpp + skymapgui.ui + skymapsettingsdialog.cpp + skymapsettingsdialog.ui + websocketserver.cpp + skymap.qrc + webinterface.cpp + wtml.cpp + webview.cpp + icons.qrc + ) + set(skymap_HEADERS + ${skymap_HEADERS} + skymapgui.h + skymapsettingsdialog.h + skymapmodel.h + skymapitem.h + websocketserver.h + skymaptileserver.h + webinterface.h + wtml.h + webview.h + ) + + set(TARGET_NAME featureskymap) + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) + + if(Qt${QT_DEFAULT_MAJOR_VERSION}WebEngineCore_FOUND) + set(TARGET_LIB "Qt::Widgets" Qt::Positioning Qt::Location Qt::WebEngineCore Qt::WebEngineWidgets) + elseif(Qt${QT_DEFAULT_MAJOR_VERSION}WebEngine_FOUND) + set(TARGET_LIB "Qt::Widgets" Qt::Positioning Qt::Location Qt::WebEngine Qt::WebEngineCore Qt::WebEngineWidgets) + else() + set(TARGET_LIB "Qt::Widgets" Qt::Positioning Qt::Location) + endif() +else() + set(TARGET_NAME featureskymapsrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + +add_library(${TARGET_NAME} SHARED + ${skymap_SOURCES} +) + +target_link_libraries(${TARGET_NAME} + Qt::Core + ${TARGET_LIB} + sdrbase + ${TARGET_LIB_GUI} +) + +install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER}) + +if(WIN32) + # Run deployqt for QtQuick etc + include(DeployQt) + windeployqt(${TARGET_NAME} ${SDRANGEL_BINARY_BIN_DIR} ${PROJECT_SOURCE_DIR}/skymap) +endif() + +# Install debug symbols +if (WIN32) + install(FILES $ CONFIGURATIONS Debug RelWithDebInfo DESTINATION ${INSTALL_FOLDER} ) +endif() diff --git a/plugins/feature/skymap/html/aladin.html b/plugins/feature/skymap/html/aladin.html new file mode 100644 index 000000000..dbba8975b --- /dev/null +++ b/plugins/feature/skymap/html/aladin.html @@ -0,0 +1,166 @@ + + + + SDRangel Aladin + + + + + +
+ + + + + + diff --git a/plugins/feature/skymap/html/esasky.html b/plugins/feature/skymap/html/esasky.html new file mode 100644 index 000000000..8362ed7d1 --- /dev/null +++ b/plugins/feature/skymap/html/esasky.html @@ -0,0 +1,181 @@ + + + + SDRangel ESASky + + + + + + +
+ +
+ + + diff --git a/plugins/feature/skymap/html/wwt.html b/plugins/feature/skymap/html/wwt.html new file mode 100644 index 000000000..fe1738cfa --- /dev/null +++ b/plugins/feature/skymap/html/wwt.html @@ -0,0 +1,828 @@ + + + + SDRangel WWT + + + + + + + + + + + +
+ + + + diff --git a/plugins/feature/skymap/icons.qrc b/plugins/feature/skymap/icons.qrc new file mode 100644 index 000000000..4ef775a72 --- /dev/null +++ b/plugins/feature/skymap/icons.qrc @@ -0,0 +1,6 @@ + + + icons/constellation.png + icons/reticle.png + + diff --git a/plugins/feature/skymap/icons/constellation.png b/plugins/feature/skymap/icons/constellation.png new file mode 100644 index 000000000..b9a693ce5 Binary files /dev/null and b/plugins/feature/skymap/icons/constellation.png differ diff --git a/plugins/feature/skymap/icons/reticle.png b/plugins/feature/skymap/icons/reticle.png new file mode 100644 index 000000000..6bd900403 Binary files /dev/null and b/plugins/feature/skymap/icons/reticle.png differ diff --git a/plugins/feature/skymap/readme.md b/plugins/feature/skymap/readme.md new file mode 100644 index 000000000..c567afe08 --- /dev/null +++ b/plugins/feature/skymap/readme.md @@ -0,0 +1,141 @@ +

Sky Map Feature Plugin

+ +

Introduction

+ +The Sky Map Feature provides visualization of the sky in multiple wavelengths (radio, IR, optical, UV, X-Ray, gamma). +Multiple Sky Maps are supported, including the [World Wide Telescope](https://www.worldwidetelescope.org/), [ESASky](https://www.esa.int/About_Us/ESAC/Explore_the_cosmos_with_ESASky) and [Aladin Lite](https://aladin.cds.unistra.fr/). + +The Rotator Controller and Star Tracker plugins can track the position viewed in the Sky Map. The Sky Map can also be set to track coordinates from the Star Tracker, Satellite Tracker, Rotator Controller or Map. + +![Sky Map feature WWT](../../../doc/img/SkyMap_WWT.png) + +With the ESASky map, a host of astronomical data is available from JWST, Hubble, Gaia, Herschel and others, including images, spectra and publications: + +![Sky Map feature ESASky](../../../doc/img/SkyMap_ESASky.png) + +

Interface

+ +![Sky Map feature plugin GUI](../../../doc/img/SkyMap_settings.png) + +

1: Find

+ +Enter an astronomical object name and press enter to centre the sky map on that object. + +You can also enter J2000 RA and Dec coordinates, in either HMS/DMS or decimal: + +* 12 05 12.23 +17 06 21.0 +* 12 05 12 17 06 21 +* 12:05:12.23 -17:06:21.20 +* 12h05m12.23s +17d06m21.10s +* 107.1324 -34.233 + +Note that the supported object names depend on the chosen map (2). + +

2: Map Type

+ +Allows you to select the sky map: + +* WWT - World Wide Telescope +* ESASky +* Aladin + +Each map provides different features, image and data sets. + +

3: Background Image Set

+ +For WWT and Aladin, this sets the background image set the sky map will display. For ESASky, this must be set within the ESASky GUI (using the Manage Skies button), as this can use multiple image sets. + +Image sets are available for a variety of wavelengths (radio, IR, optical, UV, X-Ray, gamma) from a number of different surveys. + +

4: Projection

+ +For World Wide Telescope: + +* Sky - Views the sky from a position on Earth, +* Solar System - Views the Solar System, with the ability to show orbits, +* Planet - Views one of the planets or major moons. + +For Aladin: + +* SIN - [orthographic](https://en.wikipedia.org/wiki/Orthographic_map_projection), +* AIT - [Hammer-Aitoff](https://en.wikipedia.org/wiki/Hammer_projection), +* MOL - [Mollweide](https://en.wikipedia.org/wiki/Mollweide_projection), +* MER - [Mercator](https://en.wikipedia.org/wiki/Mercator_projection), +* ARC - zenithal/azimuthal equidistant, +* TAN - [gnomonic](https://en.wikipedia.org/wiki/Gnomonic_projection), +* HPX - [HEALPix](https://en.wikipedia.org/wiki/HEALPix). + +This option is not available for ESASky, which is currently fixed to orthographic. + +See [List of map projections](https://en.wikipedia.org/wiki/List_of_map_projections). + +

5: Display Names

+ +For WWT, when checked, displays names of constellations and Ecliptic text (when the constellations and Ecliptic are visible). + +

6: Display Constellations

+ +For WWT, this option enables the display of constellations. How the constellations are drawn can be customised in the Display Settings dialog (12). + +

7: Display gird

+ +When checked, displays a coordinate grid. + +For WWT, Ecliptic, Alt/Az, Equatorial and Galactic grids can be displayed. These can be selected in the Display Settings dialog (12). + +For ESASky and Aladin, the grid will be Equatorial or Galactic, depending on the coordinate mode selected. + +

8: Display reticle

+ +When checked, displays a reticle (cross hair) at the centre of the view. Coordinates for the reticle are displayed in the status bar in the bottom of the window. + +

9: Display antenna field-of-view

+ +When checked, displays the antenna field-of-view. The antenna beamwidth can be set in the Display Settings dialog (12) or from a Star Tracker source (11). + +

10: Track

+ +When checked, the centre of view will track the coordinates received from the selected Source plugin (11). + +

11: Source

+ +Select a Star Tracker, Rotator Controller, Satellite Tracker or Map plugin to read viewing coordinates from. + +* When a Star Tracker is selected, target RA/Dec, observation position, antenna beamwith and date/time will be read from the selected plugin. +* For other plugin types, Alt/El will be read and other parameters are taken from the the Display Settings dialog (12). + +

12: Display settings

+ +When clicked, opens the Display Settings dialog. The Display Settings dialog allows the user to set: + +* Observation location (latitude and longitude in degreees). +* Antenna beamwidth in degrees. +* Settings for WWT, such as how the constellations are drawn, what grids are displayed and how the Solar System view appears. + +

Sky Map Controls

+ +* Click and drag using the left mouse button to rotate through right ascension or declination. +* Use the mouse wheel to zoom in and out. +* In WWT, right click to display information about the nearest astronomical object. The object will be circled and a popup information window will appear with links to relevant astronomical databases (ADS, SIMBAD, NED, VizieR). +The popup window can be moved by left clicking and dragging it. +* In ESASky, right click to display a popup window, with links to various astronomical databases (SIMBAD, NED, VizieR) for the corresponding coordinates. +* In Aladin, right click to display a popup menu. Use the "What is this?" menu to display a window with a link to the CDS astronomical database. + +ESASky and Aladin are able to overlay catalog data: + +* In ESASky, zoom in to the area of interest, then press the "Explore catalog data for this region" button. A window will appear showing available catalogs. Left click and boxes will be overlaid on the map for each catalog entry. The data is also displayed in tabular form. You can left click on a box to view its data in the table. +* In Aladin, zoom in to the areae of interest, then press the "Manage layers" button. In the popup window, select one or more of the available catalogs, or press Add catalogue to add a new one. Boxes will be overlaid on the map for each catalog entry. Left click on a box to display the data for it in tabular form. + +

Attribution

+ +Constellation icons created by Freepik - https://www.flaticon.com + +

API

+ +Full details of the API can be found in the Swagger documentation. Here is a quick example of how to centre the sky map on an object from the command line: + + curl -X POST "http://127.0.0.1:8091/sdrangel/featureset/feature/0/actions" -d '{"featureType": "SkyMap", "SkyMapActions": { "find": "polaris" }}' + +And to centre the sky map at a particular RA and dec (Not for WWT): + + curl -X POST "http://127.0.0.1:8091/sdrangel/featureset/feature/0/actions" -d '{"featureType": "SkyMap", "SkyMapActions": { "find": "18 36 56 +38 47 01" }}' diff --git a/plugins/feature/skymap/skymap.cpp b/plugins/feature/skymap/skymap.cpp new file mode 100644 index 000000000..a2bba969e --- /dev/null +++ b/plugins/feature/skymap/skymap.cpp @@ -0,0 +1,510 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021-2024 Jon Beniston, M7RCE // +// Copyright (C) 2021-2023 Edouard Griffiths, F4EXB // +// Copyright (C) 2022 Jiří Pinkava // +// // +// 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 +#include +#include +#include +#include + +#include "SWGFeatureSettings.h" +#include "SWGFeatureReport.h" +#include "SWGFeatureActions.h" +#include "SWGDeviceState.h" + +#include "dsp/dspengine.h" + +#include "device/deviceset.h" +#include "channel/channelapi.h" +#include "feature/featureset.h" +#include "settings/serializable.h" +#include "maincore.h" +#include "skymap.h" + +MESSAGE_CLASS_DEFINITION(SkyMap::MsgConfigureSkyMap, Message) +MESSAGE_CLASS_DEFINITION(SkyMap::MsgFind, Message) +MESSAGE_CLASS_DEFINITION(SkyMap::MsgSetDateTime, Message) +MESSAGE_CLASS_DEFINITION(SkyMap::MsgReportViewDetails, Message) + +const char* const SkyMap::m_featureIdURI = "sdrangel.feature.skymap"; +const char* const SkyMap::m_featureId = "SkyMap"; + +SkyMap::SkyMap(WebAPIAdapterInterface *webAPIAdapterInterface) : + Feature(m_featureIdURI, webAPIAdapterInterface), + m_multiplier(0.0) +{ + qDebug("SkyMap::SkyMap: webAPIAdapterInterface: %p", webAPIAdapterInterface); + setObjectName(m_featureId); + m_state = StIdle; + m_errorMessage = "SkyMap error"; + m_networkManager = new QNetworkAccessManager(); + QObject::connect( + m_networkManager, + &QNetworkAccessManager::finished, + this, + &SkyMap::networkManagerFinished + ); +} + +SkyMap::~SkyMap() +{ + QObject::disconnect( + m_networkManager, + &QNetworkAccessManager::finished, + this, + &SkyMap::networkManagerFinished + ); + delete m_networkManager; +} + +bool SkyMap::handleMessage(const Message& cmd) +{ + if (MsgConfigureSkyMap::match(cmd)) + { + MsgConfigureSkyMap& cfg = (MsgConfigureSkyMap&) cmd; + qDebug() << "SkyMap::handleMessage: MsgConfigureSkyMap"; + applySettings(cfg.getSettings(), cfg.getSettingsKeys(), cfg.getForce()); + + return true; + } + else if (MsgReportViewDetails::match(cmd)) + { + MsgReportViewDetails& report = (MsgReportViewDetails&) cmd; + m_viewDetails = report.getViewDetails(); + return true; + } + else + { + return false; + } +} + +QByteArray SkyMap::serialize() const +{ + return m_settings.serialize(); +} + +bool SkyMap::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + MsgConfigureSkyMap *msg = MsgConfigureSkyMap::create(m_settings, QList(), true); + m_inputMessageQueue.push(msg); + return true; + } + else + { + m_settings.resetToDefaults(); + MsgConfigureSkyMap *msg = MsgConfigureSkyMap::create(m_settings, QList(), true); + m_inputMessageQueue.push(msg); + return false; + } +} + +void SkyMap::applySettings(const SkyMapSettings& settings, const QList& settingsKeys, bool force) +{ + qDebug() << "SkyMap::applySettings:" << settings.getDebugString(settingsKeys, force) << " force: " << force; + + if (settingsKeys.contains("useReverseAPI")) + { + bool fullUpdate = (settingsKeys.contains("useReverseAPI") && settings.m_useReverseAPI) || + settingsKeys.contains("reverseAPIAddress") || + settingsKeys.contains("reverseAPIPort") || + settingsKeys.contains("reverseAPIFeatureSetIndex") || + settingsKeys.contains("m_reverseAPIFeatureIndex"); + webapiReverseSendSettings(settingsKeys, settings, fullUpdate || force); + } + + if (force) { + m_settings = settings; + } else { + m_settings.applySettings(settingsKeys, settings); + } +} + +int SkyMap::webapiRun(bool run, + SWGSDRangel::SWGDeviceState& response, + QString& errorMessage) +{ + (void) run; + (void) errorMessage; + getFeatureStateStr(*response.getState()); + return 202; +} + +int SkyMap::webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setSkyMapSettings(new SWGSDRangel::SWGSkyMapSettings()); + response.getSkyMapSettings()->init(); + webapiFormatFeatureSettings(response, m_settings); + return 200; +} + +int SkyMap::webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + SkyMapSettings settings = m_settings; + webapiUpdateFeatureSettings(settings, featureSettingsKeys, response); + + MsgConfigureSkyMap *msg = MsgConfigureSkyMap::create(settings, featureSettingsKeys, force); + m_inputMessageQueue.push(msg); + + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureSkyMap *msgToGUI = MsgConfigureSkyMap::create(settings, featureSettingsKeys, force); + m_guiMessageQueue->push(msgToGUI); + } + + webapiFormatFeatureSettings(response, settings); + + return 200; +} + +int SkyMap::webapiReportGet( + SWGSDRangel::SWGFeatureReport& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setSkyMapReport(new SWGSDRangel::SWGSkyMapReport()); + response.getSkyMapReport()->init(); + webapiFormatFeatureReport(response); + return 200; +} + +int SkyMap::webapiActionsPost( + const QStringList& featureActionsKeys, + SWGSDRangel::SWGFeatureActions& query, + QString& errorMessage) +{ + SWGSDRangel::SWGSkyMapActions *swgSkyMapActions = query.getSkyMapActions(); + + if (swgSkyMapActions) + { + if (featureActionsKeys.contains("find")) + { + QString id = *swgSkyMapActions->getFind(); + + if (getMessageQueueToGUI()) { + getMessageQueueToGUI()->push(MsgFind::create(id)); + } + } + /*if (featureActionsKeys.contains("setDateTime")) + { + QString dateTimeString = *swgSkyMapActions->getSetDateTime(); + QDateTime dateTime = QDateTime::fromString(dateTimeString, Qt::ISODateWithMs); + + if (getMessageQueueToGUI()) { + getMessageQueueToGUI()->push(MsgSetDateTime::create(dateTime)); + } + }*/ + return 202; + } + else + { + errorMessage = "Missing SkyMapActions in query"; + return 400; + } + return 400; +} + +void SkyMap::webapiFormatFeatureSettings( + SWGSDRangel::SWGFeatureSettings& response, + const SkyMapSettings& settings) +{ + response.getSkyMapSettings()->setDisplayNames(settings.m_displayNames ? 1 : 0); + response.getSkyMapSettings()->setDisplayConstellations(settings.m_displayConstellations ? 1 : 0); + response.getSkyMapSettings()->setDisplayReticle(settings.m_displayReticle ? 1 : 0); + response.getSkyMapSettings()->setDisplayGrid(settings.m_displayGrid ? 1 : 0); + response.getSkyMapSettings()->setDisplayAntennaFoV(settings.m_displayAntennaFoV ? 1 : 0); + response.getSkyMapSettings()->setMap(new QString(settings.m_map)); + response.getSkyMapSettings()->setBackground(new QString(settings.m_background)); + response.getSkyMapSettings()->setProjection(new QString(settings.m_projection)); + response.getSkyMapSettings()->setSource(new QString(settings.m_source)); + response.getSkyMapSettings()->setTrack(settings.m_track ? 1 : 0); + response.getSkyMapSettings()->setLatitude(settings.m_latitude); + response.getSkyMapSettings()->setLongitude(settings.m_longitude); + response.getSkyMapSettings()->setAltitude(settings.m_altitude); + response.getSkyMapSettings()->setHpbw(settings.m_hpbw); + response.getSkyMapSettings()->setUseMyPosition(settings.m_useMyPosition); + + if (response.getSkyMapSettings()->getTitle()) { + *response.getSkyMapSettings()->getTitle() = settings.m_title; + } else { + response.getSkyMapSettings()->setTitle(new QString(settings.m_title)); + } + + response.getSkyMapSettings()->setRgbColor(settings.m_rgbColor); + response.getSkyMapSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getSkyMapSettings()->getReverseApiAddress()) { + *response.getSkyMapSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getSkyMapSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getSkyMapSettings()->setReverseApiPort(settings.m_reverseAPIPort); + response.getSkyMapSettings()->setReverseApiFeatureSetIndex(settings.m_reverseAPIFeatureSetIndex); + response.getSkyMapSettings()->setReverseApiFeatureIndex(settings.m_reverseAPIFeatureIndex); + + if (settings.m_rollupState) + { + if (response.getSkyMapSettings()->getRollupState()) + { + settings.m_rollupState->formatTo(response.getSkyMapSettings()->getRollupState()); + } + else + { + SWGSDRangel::SWGRollupState *swgRollupState = new SWGSDRangel::SWGRollupState(); + settings.m_rollupState->formatTo(swgRollupState); + response.getSkyMapSettings()->setRollupState(swgRollupState); + } + } +} + +void SkyMap::webapiUpdateFeatureSettings( + SkyMapSettings& settings, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response) +{ + if (featureSettingsKeys.contains("displayNames")) { + settings.m_displayNames = response.getSkyMapSettings()->getDisplayNames(); + } + if (featureSettingsKeys.contains("displayConstellations")) { + settings.m_displayConstellations = response.getSkyMapSettings()->getDisplayConstellations(); + } + if (featureSettingsKeys.contains("displayReticle")) { + settings.m_displayReticle = response.getSkyMapSettings()->getDisplayReticle(); + } + if (featureSettingsKeys.contains("displayGrid")) { + settings.m_displayGrid = response.getSkyMapSettings()->getDisplayGrid(); + } + if (featureSettingsKeys.contains("displayAntennaFoV")) { + settings.m_displayAntennaFoV = response.getSkyMapSettings()->getDisplayAntennaFoV(); + } + if (featureSettingsKeys.contains("map")) { + settings.m_map = *response.getSkyMapSettings()->getMap(); + } + if (featureSettingsKeys.contains("background")) { + settings.m_background = *response.getSkyMapSettings()->getBackground(); + } + if (featureSettingsKeys.contains("projection")) { + settings.m_projection = *response.getSkyMapSettings()->getProjection(); + } + if (featureSettingsKeys.contains("source")) { + settings.m_source = *response.getSkyMapSettings()->getSource(); + } + if (featureSettingsKeys.contains("track")) { + settings.m_track = response.getSkyMapSettings()->getTrack(); + } + if (featureSettingsKeys.contains("latitude")) { + settings.m_latitude = response.getSkyMapSettings()->getLatitude(); + } + if (featureSettingsKeys.contains("longitude")) { + settings.m_longitude = response.getSkyMapSettings()->getLongitude(); + } + if (featureSettingsKeys.contains("altitude")) { + settings.m_altitude = response.getSkyMapSettings()->getAltitude(); + } + if (featureSettingsKeys.contains("hpbw")) { + settings.m_hpbw = response.getSkyMapSettings()->getHpbw(); + } + if (featureSettingsKeys.contains("useMyPosition")) { + settings.m_useMyPosition = response.getSkyMapSettings()->getUseMyPosition(); + } + if (featureSettingsKeys.contains("title")) { + settings.m_title = *response.getSkyMapSettings()->getTitle(); + } + if (featureSettingsKeys.contains("rgbColor")) { + settings.m_rgbColor = response.getSkyMapSettings()->getRgbColor(); + } + if (featureSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getSkyMapSettings()->getUseReverseApi() != 0; + } + if (featureSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getSkyMapSettings()->getReverseApiAddress(); + } + if (featureSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getSkyMapSettings()->getReverseApiPort(); + } + if (featureSettingsKeys.contains("reverseAPIFeatureSetIndex")) { + settings.m_reverseAPIFeatureSetIndex = response.getSkyMapSettings()->getReverseApiFeatureSetIndex(); + } + if (featureSettingsKeys.contains("reverseAPIFeatureIndex")) { + settings.m_reverseAPIFeatureIndex = response.getSkyMapSettings()->getReverseApiFeatureIndex(); + } + if (settings.m_rollupState && featureSettingsKeys.contains("rollupState")) { + settings.m_rollupState->updateFrom(featureSettingsKeys, response.getSkyMapSettings()->getRollupState()); + } +} + +void SkyMap::webapiReverseSendSettings(const QList& featureSettingsKeys, const SkyMapSettings& settings, bool force) +{ + SWGSDRangel::SWGFeatureSettings *swgFeatureSettings = new SWGSDRangel::SWGFeatureSettings(); + // swgFeatureSettings->setOriginatorFeatureIndex(getIndexInDeviceSet()); + // swgFeatureSettings->setOriginatorFeatureSetIndex(getDeviceSetIndex()); + swgFeatureSettings->setFeatureType(new QString("SkyMap")); + swgFeatureSettings->setSkyMapSettings(new SWGSDRangel::SWGSkyMapSettings()); + SWGSDRangel::SWGSkyMapSettings *swgSkyMapSettings = swgFeatureSettings->getSkyMapSettings(); + + // transfer data that has been modified. When force is on transfer all data except reverse API data + + if (featureSettingsKeys.contains("displayNames") || force) { + swgSkyMapSettings->setDisplayNames(settings.m_displayNames); + } + if (featureSettingsKeys.contains("displayConstellations") || force) { + swgSkyMapSettings->setDisplayConstellations(settings.m_displayConstellations); + } + if (featureSettingsKeys.contains("displayReticle") || force) { + swgSkyMapSettings->setDisplayReticle(settings.m_displayReticle); + } + if (featureSettingsKeys.contains("displayGrid") || force) { + swgSkyMapSettings->setDisplayGrid(settings.m_displayGrid); + } + if (featureSettingsKeys.contains("displayAntennaFoV") || force) { + swgSkyMapSettings->setDisplayAntennaFoV(settings.m_displayAntennaFoV); + } + if (featureSettingsKeys.contains("map") || force) { + swgSkyMapSettings->setMap(new QString(settings.m_map)); + } + if (featureSettingsKeys.contains("background") || force) { + swgSkyMapSettings->setBackground(new QString(settings.m_background)); + } + if (featureSettingsKeys.contains("projection") || force) { + swgSkyMapSettings->setProjection(new QString(settings.m_projection)); + } + if (featureSettingsKeys.contains("source") || force) { + swgSkyMapSettings->setSource(new QString(settings.m_source)); + } + if (featureSettingsKeys.contains("track") || force) { + swgSkyMapSettings->setTrack(settings.m_track); + } + if (featureSettingsKeys.contains("latitude") || force) { + swgSkyMapSettings->setLatitude(settings.m_latitude); + } + if (featureSettingsKeys.contains("longitude") || force) { + swgSkyMapSettings->setLongitude(settings.m_longitude); + } + if (featureSettingsKeys.contains("altitude") || force) { + swgSkyMapSettings->setAltitude(settings.m_altitude); + } + if (featureSettingsKeys.contains("hpbw") || force) { + swgSkyMapSettings->setHpbw(settings.m_hpbw); + } + if (featureSettingsKeys.contains("useMyPosition") || force) { + swgSkyMapSettings->setTrack(settings.m_useMyPosition); + } + if (featureSettingsKeys.contains("title") || force) { + swgSkyMapSettings->setTitle(new QString(settings.m_title)); + } + if (featureSettingsKeys.contains("rgbColor") || force) { + swgSkyMapSettings->setRgbColor(settings.m_rgbColor); + } + + QString channelSettingsURL = QString("http://%1:%2/sdrangel/featureset/%3/feature/%4/settings") + .arg(settings.m_reverseAPIAddress) + .arg(settings.m_reverseAPIPort) + .arg(settings.m_reverseAPIFeatureSetIndex) + .arg(settings.m_reverseAPIFeatureIndex); + m_networkRequest.setUrl(QUrl(channelSettingsURL)); + m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QBuffer *buffer = new QBuffer(); + buffer->open((QBuffer::ReadWrite)); + buffer->write(swgFeatureSettings->asJson().toUtf8()); + buffer->seek(0); + + // Always use PATCH to avoid passing reverse API settings + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); + + delete swgFeatureSettings; +} + +void SkyMap::webapiFormatFeatureReport(SWGSDRangel::SWGFeatureReport& response) +{ + QString skymapDateTime = getSkyMapDateTime().toString(Qt::ISODateWithMs); + if (response.getSkyMapReport()->getDateTime()) { + //*response.getSkyMapReport()->getDateTime() = skymapDateTime; + *response.getSkyMapReport()->getDateTime() = m_viewDetails.m_dateTime.toString(Qt::ISODateWithMs); + } else { + //response.getSkyMapReport()->setDateTime(new QString(skymapDateTime)); + response.getSkyMapReport()->setDateTime(new QString(m_viewDetails.m_dateTime.toString(Qt::ISODateWithMs))); + } + response.getSkyMapReport()->setRa(m_viewDetails.m_ra); + response.getSkyMapReport()->setDec(m_viewDetails.m_dec); + response.getSkyMapReport()->setAzimuth(m_viewDetails.m_azimuth); + response.getSkyMapReport()->setElevation(m_viewDetails.m_elevation); + response.getSkyMapReport()->setFov(m_viewDetails.m_fov); + response.getSkyMapReport()->setLatitude(m_viewDetails.m_latitude); + response.getSkyMapReport()->setLongitude(m_viewDetails.m_longitude); +} + +void SkyMap::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "SkyMap::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("SkyMap::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } + + reply->deleteLater(); +} + +void SkyMap::setSkyMapDateTime(QDateTime skymapDateTime, QDateTime systemDateTime, double multiplier) +{ + QMutexLocker mutexLocker(&m_dateTimeMutex); + m_skymapDateTime = skymapDateTime; + m_systemDateTime = systemDateTime; + m_multiplier = multiplier; +} + +QDateTime SkyMap::getSkyMapDateTime() +{ + QMutexLocker mutexLocker(&m_dateTimeMutex); + if (m_multiplier == 0.0) + { + return m_skymapDateTime; + } + else + { + // It's not possible to synchronously get the time from Cesium + // so we calculate it based on the system clock difference from + // when changes were made to the clock GUI elements + // Should be accurate enough for satellite tracker + qint64 diffMsecs = m_systemDateTime.msecsTo(QDateTime::currentDateTime()); + return m_skymapDateTime.addMSecs(diffMsecs * m_multiplier); + } +} diff --git a/plugins/feature/skymap/skymap.h b/plugins/feature/skymap/skymap.h new file mode 100644 index 000000000..cc9d794fb --- /dev/null +++ b/plugins/feature/skymap/skymap.h @@ -0,0 +1,221 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020, 2022 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Kacper Michajłow // +// Copyright (C) 2021-2024 Jon Beniston, M7RCE // +// Copyright (C) 2022 Jiří Pinkava // +// // +// 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_FEATURE_SKYMAP_H_ +#define INCLUDE_FEATURE_SKYMAP_H_ + +#include +#include +#include +#include + +#include "feature/feature.h" +#include "util/message.h" + +#include "skymapsettings.h" + +class WebAPIAdapterInterface; +class QNetworkAccessManager; +class QNetworkReply; + +namespace SWGSDRangel { + class SWGDeviceState; +} + +class SkyMap : public Feature +{ + Q_OBJECT +public: + + class MsgConfigureSkyMap : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const SkyMapSettings& getSettings() const { return m_settings; } + const QList& getSettingsKeys() const { return m_settingsKeys; } + bool getForce() const { return m_force; } + + static MsgConfigureSkyMap* create(const SkyMapSettings& settings, const QList& settingsKeys, bool force) { + return new MsgConfigureSkyMap(settings, settingsKeys, force); + } + + private: + SkyMapSettings m_settings; + QList m_settingsKeys; + bool m_force; + + MsgConfigureSkyMap(const SkyMapSettings& settings, const QList& settingsKeys, bool force) : + Message(), + m_settings(settings), + m_settingsKeys(settingsKeys), + m_force(force) + { } + }; + + class MsgFind : public Message { + MESSAGE_CLASS_DECLARATION + + public: + QString getTarget() const { return m_target; } + + static MsgFind* create(const QString& target) { + return new MsgFind(target); + } + + private: + QString m_target; + + MsgFind(const QString& target) : + Message(), + m_target(target) + {} + }; + + class MsgSetDateTime : public Message { + MESSAGE_CLASS_DECLARATION + + public: + QDateTime getDateTime() const { return m_dateTime; } + + static MsgSetDateTime* create(const QDateTime& dateTime) { + return new MsgSetDateTime(dateTime); + } + + private: + QDateTime m_dateTime; + + MsgSetDateTime(const QDateTime& dateTime) : + Message(), + m_dateTime(dateTime) + {} + }; + + struct ViewDetails { + double m_ra; + double m_dec; + float m_azimuth; + float m_elevation; + float m_fov; + float m_latitude; + float m_longitude; + QDateTime m_dateTime; + + ViewDetails() : + m_ra(0.0), + m_dec(0.0), + m_azimuth(0.0f), + m_elevation(0.0f), + m_fov(0.0f), + m_latitude(0.0f), + m_longitude(0.0f) + { + } + }; + + class MsgReportViewDetails : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const ViewDetails& getViewDetails() { return m_viewDetails; } + + static MsgReportViewDetails* create(const ViewDetails& viewDetails) { + return new MsgReportViewDetails(viewDetails); + } + + private: + ViewDetails m_viewDetails; + + MsgReportViewDetails(const ViewDetails& viewDetails) : + Message(), + m_viewDetails(viewDetails) + {} + }; + + SkyMap(WebAPIAdapterInterface *webAPIAdapterInterface); + virtual ~SkyMap(); + virtual void destroy() { delete this; } + virtual bool handleMessage(const Message& cmd); + + virtual void getIdentifier(QString& id) const { id = objectName(); } + virtual QString getIdentifier() const { return objectName(); } + virtual void getTitle(QString& title) const { title = m_settings.m_title; } + + virtual QByteArray serialize() const; + virtual bool deserialize(const QByteArray& data); + + virtual int webapiRun(bool run, + SWGSDRangel::SWGDeviceState& response, + QString& errorMessage); + + virtual int webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage); + + virtual int webapiReportGet( + SWGSDRangel::SWGFeatureReport& response, + QString& errorMessage); + + virtual int webapiActionsPost( + const QStringList& featureActionsKeys, + SWGSDRangel::SWGFeatureActions& query, + QString& errorMessage); + + static void webapiFormatFeatureSettings( + SWGSDRangel::SWGFeatureSettings& response, + const SkyMapSettings& settings); + + static void webapiUpdateFeatureSettings( + SkyMapSettings& settings, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response); + + void setSkyMapDateTime(QDateTime skymapDateTime, QDateTime systemDateTime, double multiplier); + QDateTime getSkyMapDateTime(); + + static const char* const m_featureIdURI; + static const char* const m_featureId; + +private: + SkyMapSettings m_settings; + ViewDetails m_viewDetails; + + QNetworkAccessManager *m_networkManager; + QNetworkRequest m_networkRequest; + + void applySettings(const SkyMapSettings& settings, const QList& settingsKeys, bool force = false); + void webapiFormatFeatureReport(SWGSDRangel::SWGFeatureReport& response); + void webapiReverseSendSettings(const QList& featureSettingsKeys, const SkyMapSettings& settings, bool force); + + QDateTime m_skymapDateTime; + QDateTime m_systemDateTime; + double m_multiplier; + QRecursiveMutex m_dateTimeMutex; + +private slots: + void networkManagerFinished(QNetworkReply *reply); +}; + +#endif // INCLUDE_FEATURE_SKYMAP_H_ diff --git a/plugins/feature/skymap/skymap.qrc b/plugins/feature/skymap/skymap.qrc new file mode 100644 index 000000000..a68c6bb2e --- /dev/null +++ b/plugins/feature/skymap/skymap.qrc @@ -0,0 +1,7 @@ + + + html/wwt.html + html/esasky.html + html/aladin.html + + diff --git a/plugins/feature/skymap/skymapgui.cpp b/plugins/feature/skymap/skymapgui.cpp new file mode 100644 index 000000000..ff771e91c --- /dev/null +++ b/plugins/feature/skymap/skymapgui.cpp @@ -0,0 +1,1102 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021-2024 Jon Beniston, M7RCE // +// Copyright (C) 2021-2022 Edouard Griffiths, F4EXB // +// // +// 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 + +#include + +#include +#include +#include + +#include "feature/featureuiset.h" +#include "gui/basicfeaturesettingsdialog.h" +#include "gui/dialogpositioner.h" +#include "util/astronomy.h" +#include "util/units.h" +#include "maincore.h" + +#include "ui_skymapgui.h" +#include "skymap.h" +#include "skymapgui.h" +#include "skymapsettingsdialog.h" + +#include "SWGTargetAzimuthElevation.h" +#include "SWGSkyMapTarget.h" + +SkyMapGUI* SkyMapGUI::create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature) +{ + SkyMapGUI* gui = new SkyMapGUI(pluginAPI, featureUISet, feature); + return gui; +} + +void SkyMapGUI::destroy() +{ + delete this; +} + +void SkyMapGUI::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + applyAllSettings(); +} + +QByteArray SkyMapGUI::serialize() const +{ + return m_settings.serialize(); +} + +bool SkyMapGUI::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + m_feature->setWorkspaceIndex(m_settings.m_workspaceIndex); + displaySettings(); + applyAllSettings(); + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +bool SkyMapGUI::handleMessage(const Message& message) +{ + if (SkyMap::MsgConfigureSkyMap::match(message)) + { + qDebug("SkyMapGUI::handleMessage: SkyMap::MsgConfigureSkyMap"); + const SkyMap::MsgConfigureSkyMap& cfg = (SkyMap::MsgConfigureSkyMap&) message; + + if (cfg.getForce()) { + m_settings = cfg.getSettings(); + } else { + m_settings.applySettings(cfg.getSettingsKeys(), cfg.getSettings()); + } + + blockApplySettings(true); + displaySettings(); + blockApplySettings(false); + + return true; + } + else if (SkyMap::MsgFind::match(message)) + { + SkyMap::MsgFind& msgFind = (SkyMap::MsgFind&) message; + find(msgFind.getTarget()); + return true; + } + else if (SkyMap::MsgSetDateTime::match(message)) + { + SkyMap::MsgSetDateTime& msgSetDateTime = (SkyMap::MsgSetDateTime&) message; + return true; + } + else if (MainCore::MsgTargetAzimuthElevation::match(message)) + { + MainCore::MsgTargetAzimuthElevation& msg = (MainCore::MsgTargetAzimuthElevation&) message; + + // Ignore messages from StarTracker, as we use MsgSkyMapTarget instead, as more accurate and no refraction correction + if ((msg.getPipeSource() == m_source) && !m_settings.m_source.contains("StarTracker")) + { + // Convert from Az/Alt to RA/Dec + SWGSDRangel::SWGTargetAzimuthElevation *swgTarget = msg.getSWGTargetAzimuthElevation(); + AzAlt aa; + aa.az = swgTarget->getAzimuth(); + aa.alt = swgTarget->getElevation(); + + QGeoCoordinate position = getPosition(); + RADec rd; + QDateTime dateTime = QDateTime::currentDateTime(); + + rd = Astronomy::azAltToRaDec(aa, position.latitude(), position.longitude(), dateTime); + + // Convert from JNOW to J2000 + double jd = Astronomy::julianDate(dateTime); + rd = Astronomy::precess(rd, jd, Astronomy::jd_j2000()); + + m_ra = rd.ra; + m_dec = rd.dec; + if (m_settings.m_track) { + m_webInterface->setView(rd.ra, rd.dec); + } + + m_webInterface->setAntennaFoV(m_settings.m_hpbw); + } + return true; + } + else if (MainCore::MsgSkyMapTarget::match(message)) + { + MainCore::MsgSkyMapTarget& msg = (MainCore::MsgSkyMapTarget&) message; + if (msg.getPipeSource() == m_source) + { + SWGSDRangel::SWGSkyMapTarget *swgTarget = msg.getSWGSkyMapTarget(); + m_ra = swgTarget->getRa(); + m_dec = swgTarget->getDec(); + if (m_settings.m_track) { + m_webInterface->setView(m_ra, m_dec); + } + setPosition(swgTarget->getLatitude(), swgTarget->getLongitude(), swgTarget->getAltitude()); + QDateTime dateTime = QDateTime::currentDateTime(); + if (swgTarget->getDateTime()) + { + QString dateTimeString = *swgTarget->getDateTime(); + if (!dateTimeString.isEmpty()) { + dateTime = QDateTime::fromString(*swgTarget->getDateTime(), Qt::ISODateWithMs); + } + } + setDateTime(dateTime); + + m_webInterface->setAntennaFoV(swgTarget->getHpbw()); + } + return true; + } + + return false; +} + +void SkyMapGUI::handleInputMessages() +{ + Message* message; + + while ((message = getInputMessageQueue()->pop())) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +void SkyMapGUI::onWidgetRolled(QWidget* widget, bool rollDown) +{ + (void) widget; + (void) rollDown; + + getRollupContents()->saveState(m_rollupState); +} + +SkyMapGUI::SkyMapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent) : + FeatureGUI(parent), + ui(new Ui::SkyMapGUI), + m_pluginAPI(pluginAPI), + m_featureUISet(featureUISet), + m_doApplySettings(true), + m_source(nullptr) +{ + m_feature = feature; + setAttribute(Qt::WA_DeleteOnClose, true); + m_helpURL = "plugins/feature/skymap/readme.md"; + RollupContents *rollupContents = getRollupContents(); + ui->setupUi(rollupContents); + setSizePolicy(rollupContents->sizePolicy()); + rollupContents->arrangeRollups(); + connect(rollupContents, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); + + m_webInterface = new WebInterface(); + connect(m_webInterface, &WebInterface::received, this, &SkyMapGUI::receivedEvent); + + // Web server to serve dynamic files from QResources + m_webPort = 0; + m_webServer = new WebServer(m_webPort); + + m_webServer->addSubstitution("/skymap/html/wwt.html", "$WS_PORT$", QString::number(m_webInterface->serverPort())); + m_webServer->addSubstitution("/skymap/html/esasky.html", "$WS_PORT$", QString::number(m_webInterface->serverPort())); + m_webServer->addSubstitution("/skymap/html/aladin.html", "$WS_PORT$", QString::number(m_webInterface->serverPort())); + + ui->tabs->tabBar()->tabButton(0, QTabBar::RightSide)->resize(0, 0); // Hide close button + ui->web->setTabs(ui->tabs); + ui->web->load(QUrl(QString("http://127.0.0.1:%1/skymap/html/wwt.html").arg(m_webPort))); + ui->web->show(); + + m_skymap = reinterpret_cast(feature); + m_skymap->setMessageQueueToGUI(&m_inputMessageQueue); + + m_settings.setRollupState(&m_rollupState); + + connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &))); + connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + + QWebEngineSettings *settings = ui->web->settings(); + settings->setAttribute(QWebEngineSettings::FullScreenSupportEnabled, true); + connect(ui->web->page(), &QWebEnginePage::fullScreenRequested, this, &SkyMapGUI::fullScreenRequested); +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + connect(ui->web->page(), &QWebEnginePage::loadingChanged, this, &SkyMapGUI::loadingChanged); + connect(ui->web, &QWebEngineView::renderProcessTerminated, this, &SkyMapGUI::renderProcessTerminated); +#endif + + // Default to My Position + m_settings.m_latitude = MainCore::instance()->getSettings().getLatitude(); + m_settings.m_longitude = MainCore::instance()->getSettings().getLongitude(); + m_settings.m_altitude = MainCore::instance()->getSettings().getAltitude(); + + // Get updated when position changes + connect(&MainCore::instance()->getSettings(), &MainSettings::preferenceChanged, this, &SkyMapGUI::preferenceChanged); + + displaySettings(); + applyAllSettings(); + + makeUIConnections(); + m_resizer.enableChildMouseTracking(); + + QObject::connect(MainCore::instance(), &MainCore::featureAdded, this, &SkyMapGUI::handleFeatureAdded); + QObject::connect(MainCore::instance(), &MainCore::channelAdded, this, &SkyMapGUI::handleChannelAdded); + QObject::connect(MainCore::instance(), &MainCore::featureRemoved, this, &SkyMapGUI::handleFeatureRemoved); + QObject::connect(MainCore::instance(), &MainCore::channelRemoved, this, &SkyMapGUI::handleChannelRemoved); + updateSourceList(); + + connect(&m_wtml, &WTML::dataUpdated, this, &SkyMapGUI::wtmlUpdated); + m_wtml.getData(); +} + +SkyMapGUI::~SkyMapGUI() +{ + QObject::disconnect(MainCore::instance(), &MainCore::featureAdded, this, &SkyMapGUI::handleFeatureAdded); + QObject::disconnect(MainCore::instance(), &MainCore::channelAdded, this, &SkyMapGUI::handleChannelAdded); + QObject::disconnect(MainCore::instance(), &MainCore::featureRemoved, this, &SkyMapGUI::handleFeatureRemoved); + QObject::disconnect(MainCore::instance(), &MainCore::channelRemoved, this, &SkyMapGUI::handleChannelRemoved); + + if (m_webServer) + { + m_webServer->close(); + delete m_webServer; + } + delete ui; +} + +void SkyMapGUI::setWorkspaceIndex(int index) +{ + m_settings.m_workspaceIndex = index; + m_feature->setWorkspaceIndex(index); +} + +void SkyMapGUI::blockApplySettings(bool block) +{ + m_doApplySettings = !block; +} + +void SkyMapGUI::on_map_currentIndexChanged(int index) +{ + m_settings.m_map = ui->map->currentText(); + applySetting("map"); + + if (m_settings.m_map == "WWT") { + ui->web->load(QUrl(QString("http://127.0.0.1:%1/skymap/html/wwt.html").arg(m_webPort))); + } else if (m_settings.m_map == "ESASky") { + ui->web->load(QUrl(QString("http://127.0.0.1:%1/skymap/html/esasky.html").arg(m_webPort))); + } else if (m_settings.m_map == "Aladin") { + ui->web->load(QUrl(QString("http://127.0.0.1:%1/skymap/html/aladin.html").arg(m_webPort))); + } + updateToolbar(); + updateBackgrounds(); +} + +void SkyMapGUI::on_background_currentIndexChanged(int index) +{ + if (index >= 0) + { + m_settings.m_background = ui->background->currentText(); + applySetting("background"); + m_webInterface->setBackground(backgroundID(m_settings.m_background)); + } +} + +void SkyMapGUI::on_projection_currentIndexChanged(int index) +{ + if (index >= 0) + { + m_settings.m_projection = ui->projection->currentText(); + applySetting("projection"); + if (m_settings.m_map == "WWT") { + updateBackgrounds(); + } + m_webInterface->setProjection(m_settings.m_projection); + if (m_settings.m_map == "WWT") { + m_webInterface->setBackground(backgroundID(m_settings.m_background)); + } + } +} + +void SkyMapGUI::on_source_currentIndexChanged(int index) +{ + if (index >= 0) + { + m_settings.m_source = ui->source->currentText(); + applySetting("source"); + } +} + + +void SkyMapGUI::renderProcessTerminated(QWebEnginePage::RenderProcessTerminationStatus terminationStatus, int exitCode) +{ + qDebug() << "SkyMapGUI::renderProcessTerminated: " << terminationStatus << "exitCode" << exitCode; +} + +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + +void SkyMapGUI::loadingChanged(const QWebEngineLoadingInfo &loadingInfo) +{ + if (loadingInfo.status() == QWebEngineLoadingInfo::LoadFailedStatus) + { + qDebug() << "SkyMapGUI::loadingChanged: Failed to load " << loadingInfo.url().toString() + << "errorString: " << loadingInfo.errorString() << " " + << "errorDomain:" << loadingInfo.errorDomain() + << "errorCode:" << loadingInfo.errorCode() + ; + } +} +#endif + +void SkyMapGUI::initSkyMap() +{ + QGeoCoordinate position = getPosition(); + updateToolbar(); + m_webInterface->setWWTSettings(m_settings.m_wwtSettings); + m_webInterface->setPosition(position); + m_webInterface->showNames(m_settings.m_displayNames); + m_webInterface->showConstellations(m_settings.m_displayConstellations); + m_webInterface->showReticle(m_settings.m_displayReticle); + m_webInterface->showGrid(m_settings.m_displayGrid); + m_webInterface->showAntennaFoV(m_settings.m_displayAntennaFoV); + m_webInterface->setProjection(m_settings.m_projection); + m_webInterface->setBackground(backgroundID(m_settings.m_background)); +} + +void SkyMapGUI::displaySettings() +{ + setTitleColor(m_settings.m_rgbColor); + setWindowTitle(m_settings.m_title); + setTitle(m_settings.m_title); + blockApplySettings(true); + ui->displayNames->setChecked(m_settings.m_displayNames); + int idx = ui->map->findText(m_settings.m_map); + if (idx >= 0) { + ui->map->setCurrentIndex(idx); + } + ui->displayConstellations->setChecked(m_settings.m_displayConstellations); + ui->displayReticle->setChecked(m_settings.m_displayReticle); + ui->displayGrid->setChecked(m_settings.m_displayGrid); + ui->displayAntennaFoV->setChecked(m_settings.m_displayAntennaFoV); + idx = ui->background->findText(m_settings.m_background); + if (idx >= 0) { + ui->background->setCurrentIndex(idx); + } + ui->track->setChecked(m_settings.m_track); + idx = ui->source->findText(m_settings.m_source); + if (idx >= 0) { + ui->source->setCurrentIndex(idx); + } + initSkyMap(); + getRollupContents()->restoreState(m_rollupState); + blockApplySettings(false); +} + +void SkyMapGUI::onMenuDialogCalled(const QPoint &p) +{ + if (m_contextMenuType == ContextMenuChannelSettings) + { + BasicFeatureSettingsDialog dialog(this); + dialog.setTitle(m_settings.m_title); + dialog.setUseReverseAPI(m_settings.m_useReverseAPI); + dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress); + dialog.setReverseAPIPort(m_settings.m_reverseAPIPort); + dialog.setReverseAPIFeatureSetIndex(m_settings.m_reverseAPIFeatureSetIndex); + dialog.setReverseAPIFeatureIndex(m_settings.m_reverseAPIFeatureIndex); + dialog.setDefaultTitle(m_displayedName); + + dialog.move(p); + new DialogPositioner(&dialog, false); + dialog.exec(); + + m_settings.m_title = dialog.getTitle(); + m_settings.m_useReverseAPI = dialog.useReverseAPI(); + m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress(); + m_settings.m_reverseAPIPort = dialog.getReverseAPIPort(); + m_settings.m_reverseAPIFeatureSetIndex = dialog.getReverseAPIFeatureSetIndex(); + m_settings.m_reverseAPIFeatureIndex = dialog.getReverseAPIFeatureIndex(); + + setTitle(m_settings.m_title); + setTitleColor(m_settings.m_rgbColor); + + QList settingsKeys({ + "rgbColor", + "title", + "useReverseAPI", + "reverseAPIAddress", + "reverseAPIPort", + "reverseAPIDeviceIndex", + "reverseAPIChannelIndex" + }); + + applySettings(settingsKeys); + } + + resetContextMenuType(); +} + +void SkyMapGUI::applySetting(const QString& settingsKey) +{ + applySettings({settingsKey}); +} + +void SkyMapGUI::applySettings(const QStringList& settingsKeys, bool force) +{ + if (m_doApplySettings) + { + SkyMap::MsgConfigureSkyMap* message = SkyMap::MsgConfigureSkyMap::create(m_settings, m_settingsKeys, force); + m_skymap->getInputMessageQueue()->push(message); + + QObject *oldSource = m_source; + QObject *source = MainCore::getAvailableChannelOrFeatureByLongId(m_settings.m_source, m_availableChannelOrFeatures); + if (source) + { + registerPipe(source); + m_source = source; + } + else + { + m_source = nullptr; + } + + // When we change plugins, default to current date and time and My Position, until we get something different + if (oldSource && !m_source) + { + setDateTime(QDateTime::currentDateTime()); + setPosition(MainCore::instance()->getSettings().getLatitude(), + MainCore::instance()->getSettings().getLongitude(), + MainCore::instance()->getSettings().getAltitude()); + } + } + + m_settingsKeys.clear(); +} + +void SkyMapGUI::applyAllSettings() +{ + applySettings(QStringList(), true); +} + +void SkyMapGUI::find(const QString& text) +{ + float ra, dec; + + // WWT's find doesn't support coordinates, so we check here + if (Units::stringToRADec(text, ra, dec)) { + m_webInterface->setView(ra, dec); + } else { + m_webInterface->track(text); + } +} + +void SkyMapGUI::on_find_returnPressed() +{ + find(ui->find->text().trimmed()); +} + +void SkyMapGUI::on_displaySettings_clicked() +{ + SkyMapSettingsDialog dialog(&m_settings); + new DialogPositioner(&dialog, true); + if (dialog.exec() == QDialog::Accepted) + { + applySettings(dialog.m_settingsKeysChanged); + + if (dialog.m_settingsKeysChanged.contains("latitude") + || dialog.m_settingsKeysChanged.contains("longitude") + || dialog.m_settingsKeysChanged.contains("altitude") + || dialog.m_settingsKeysChanged.contains("useMyPosition")) { + m_webInterface->setPosition(getPosition()); + } + if (dialog.m_settingsKeysChanged.contains("hpbw")) { + m_webInterface->setAntennaFoV(m_settings.m_hpbw); + } + if (dialog.m_settingsKeysChanged.contains("wwtSettings")) { + m_webInterface->setWWTSettings(m_settings.m_wwtSettings); + } + } +} + +void SkyMapGUI::on_displayNames_clicked(bool checked) +{ + m_settings.m_displayNames = checked; + m_webInterface->showNames(checked); + applySetting("displayNames"); +} + +void SkyMapGUI::on_displayConstellations_clicked(bool checked) +{ + m_settings.m_displayConstellations = checked; + m_webInterface->showConstellations(checked); + applySetting("displayConstellations"); +} + +void SkyMapGUI::on_displayReticle_clicked(bool checked) +{ + m_settings.m_displayReticle = checked; + m_webInterface->showReticle(checked); + applySetting("displayReticle"); +} + +void SkyMapGUI::on_displayGrid_clicked(bool checked) +{ + m_settings.m_displayGrid = checked; + m_webInterface->showGrid(checked); + applySetting("displayGrid"); +} + +void SkyMapGUI::on_displayAntennaFoV_clicked(bool checked) +{ + m_settings.m_displayAntennaFoV = checked; + m_webInterface->showAntennaFoV(checked); + applySetting("displayAntennaFoV"); +} + +void SkyMapGUI::on_track_clicked(bool checked) +{ + m_settings.m_track = checked; + applySetting("track"); + if (m_settings.m_track) { + m_webInterface->setView(m_ra, m_dec); + } +} + +void SkyMapGUI::fullScreenRequested(QWebEngineFullScreenRequest fullScreenRequest) +{ + fullScreenRequest.accept(); + if (fullScreenRequest.toggleOn()) + { + ui->web->setParent(nullptr); + ui->web->showFullScreen(); + } + else + { + ui->tab0->layout()->addWidget(ui->web); + } +} + +void SkyMapGUI::setDateTime(QDateTime dateTime) +{ + m_dateTime = dateTime; + m_webInterface->setDateTime(m_dateTime); +} + +QDateTime SkyMapGUI::getDateTime() const +{ + if (!m_source || !m_dateTime.isValid()) { + return QDateTime::currentDateTime(); + } else { + return m_dateTime; + } +} + +void SkyMapGUI::setPosition(float latitude, float longitude, float altitude) +{ + m_settings.m_latitude = latitude; + m_settings.m_longitude = longitude; + m_settings.m_altitude = altitude; + m_webInterface->setPosition(getPosition()); +} + +QGeoCoordinate SkyMapGUI::getPosition() const +{ + float latitude, longitude, altitude; + + if (m_settings.m_useMyPosition) + { + latitude = MainCore::instance()->getSettings().getLatitude(); + longitude = MainCore::instance()->getSettings().getLongitude(); + altitude = MainCore::instance()->getSettings().getAltitude(); + } + else + { + latitude = m_settings.m_latitude; + longitude = m_settings.m_longitude; + altitude = m_settings.m_altitude; + } + + QGeoCoordinate position(latitude, longitude, altitude); + return position; +} + +void SkyMapGUI::preferenceChanged(int elementType) +{ + Preferences::ElementType pref = (Preferences::ElementType)elementType; + if ((pref == Preferences::Latitude) || (pref == Preferences::Longitude) || (pref == Preferences::Altitude)) + { + QGeoCoordinate position = getPosition(); + m_webInterface->setPosition(position); + } +} + +void SkyMapGUI::makeUIConnections() +{ + QObject::connect(ui->find, &QLineEdit::returnPressed, this, &SkyMapGUI::on_find_returnPressed); + QObject::connect(ui->displaySettings, &QToolButton::clicked, this, &SkyMapGUI::on_displaySettings_clicked); + QObject::connect(ui->displayNames, &QToolButton::clicked, this, &SkyMapGUI::on_displayNames_clicked); + QObject::connect(ui->displayConstellations, &QToolButton::clicked, this, &SkyMapGUI::on_displayConstellations_clicked); + QObject::connect(ui->displayReticle, &QToolButton::clicked, this, &SkyMapGUI::on_displayReticle_clicked); + QObject::connect(ui->displayGrid, &QToolButton::clicked, this, &SkyMapGUI::on_displayGrid_clicked); + QObject::connect(ui->displayAntennaFoV, &QToolButton::clicked, this, &SkyMapGUI::on_displayAntennaFoV_clicked); + QObject::connect(ui->background, qOverload(&QComboBox::currentIndexChanged), this, &SkyMapGUI::on_background_currentIndexChanged); + QObject::connect(ui->map, qOverload(&QComboBox::currentIndexChanged), this, &SkyMapGUI::on_map_currentIndexChanged); + QObject::connect(ui->projection, qOverload(&QComboBox::currentIndexChanged), this, &SkyMapGUI::on_projection_currentIndexChanged); + QObject::connect(ui->source, qOverload(&QComboBox::currentIndexChanged), this, &SkyMapGUI::on_source_currentIndexChanged); + QObject::connect(ui->track, &QToolButton::clicked, this, &SkyMapGUI::on_track_clicked); + QObject::connect(ui->tabs, &QTabWidget::tabCloseRequested, this, &SkyMapGUI::on_tabs_tabCloseRequested); +} + +void SkyMapGUI::receivedEvent(const QJsonObject &obj) +{ + if (obj.contains("event")) + { + QString event = obj.value("event").toString(); + if (event == "view") + { + QStringList status; + + double ra = 0.0, dec = 0.0; + double latitude = 0.0, longitude = 0.0; + double fov = 0.0; + AzAlt aa = {0.0, 0.0}; + QDateTime dateTime; + + if (obj.contains("dateTime")) + { + dateTime = QDateTime::fromString(obj.value("dateTime").toString(), Qt::ISODateWithMs).toLocalTime(); + } + else + { + dateTime = getDateTime(); + } + + if (obj.contains("ra") && obj.contains("dec")) + { + ra = obj.value("ra").toDouble(); + dec = obj.value("dec").toDouble(); + + // Convert from decimal to DMS + QString raDMS = Units::decimalHoursToHoursMinutesAndSeconds(ra); + QString decDMS = Units::decimalDegreesToDegreeMinutesAndSeconds(dec); + + status.append(QString("J2000 RA: %1 Dec: %2") + .arg(raDMS).arg(decDMS)); + } + + if (obj.contains("fov")) + { + fov = obj.value("fov").toDouble(); + + status.append(QString("FoV: %1%2").arg(fov, 0, 'f', 2).arg(QChar(0xb0))); + } + + if (obj.contains("latitude") && obj.contains("longitude")) + { + latitude = obj.value("latitude").toDouble(); + longitude = obj.value("longitude").toDouble(); + + status.append(QString("Lat: %1%3 Long: %2%3") + .arg(latitude).arg(longitude) + .arg(QChar(0xb0))); + } + + if (obj.contains("dateTime")) + { + status.append(QString("Date: %1").arg(dateTime.date().toString())); + status.append(QString("Time: %1").arg(dateTime.time().toString())); + } + + if (obj.contains("ra") && obj.contains("dec") && obj.contains("latitude") && obj.contains("longitude") && obj.contains("dateTime")) + { + // Convert from RA/Dec to Az/Alt + RADec rd = {ra, dec}; + aa = Astronomy::raDecToAzAlt(rd, latitude, longitude, dateTime); + QString az = QString::number(aa.az, 'f', 3); + QString alt = QString::number(aa.alt, 'f', 3); + + status.insert(1, QString("Az: %1%3 Alt: %2%3") + .arg(az).arg(alt) + .arg(QChar(0xb0))); + + sendToRotator(status[0], aa.az, aa.alt); + } + else if (obj.contains("ra") && obj.contains("dec")) + { + RADec rd = {ra, dec}; + QGeoCoordinate position = getPosition(); + + AzAlt aa = Astronomy::raDecToAzAlt(rd, position.latitude(), position.longitude(), dateTime); + sendToRotator(status[0], aa.az, aa.alt); + } + + setStatusText(status.join(" - ")); + + // Send details to SkyMap for use in Web report + SkyMap::ViewDetails viewDetails; + viewDetails.m_ra = ra; + viewDetails.m_dec = dec; + viewDetails.m_azimuth = aa.az; + viewDetails.m_elevation = aa.alt; + viewDetails.m_latitude = latitude; + viewDetails.m_longitude = longitude; + viewDetails.m_fov = fov; + viewDetails.m_dateTime = dateTime; + SkyMap::MsgReportViewDetails* message = SkyMap::MsgReportViewDetails::create(viewDetails); + m_skymap->getInputMessageQueue()->push(message); + + } + else if (event == "ready") + { + initSkyMap(); + } + } + else + { + qDebug() << "SkyMapGUI::receivedEvent - Unexpected event: " << obj; + } +} + +// Loaded via WTML +QStringList wwtBackgrounds; + +const QStringList wwtPlanets = { + "Sun", + "Mercury", + "Venus", + "Earth", + "Moon", + "Mars", + "Jupiter", + "Saturn", + "Uranus", + "Neptune", + "Pluto", + "Io", + "Europa", + "Ganymede", + "Callisto" +}; + +const QStringList wwtPlanetIDs = { + "Sun", + "Mercury", + "Venus", + "Bing Maps Aerial", //"Earth", + "Moon", + "Visible Imagery", // Mars + "Jupiter", + "Saturn", + "Uranus", + "Neptune", + "Pluto (New Horizons)", + "Io", + "Europa", + "Ganymede", + "Callisto" +}; + +// From https://github.com/cds-astro/aladin-lite/blob/master/src/js/ImageLayer.js +const QStringList aladinBackgrounds = { + "DSS colored", + "DSS2 Red (F+R)", + "2MASS colored", + "Density map for Gaia EDR3 (I/350/gaiaedr3)", + "PanSTARRS DR1 g", + "PanSTARRS DR1 color", + "DECaPS DR1 color", + "Fermi color", + "Halpha", + "GALEXGR6_7 NUV", + "IRIS colored", + "Mellinger colored", + "SDSS9 colored", + "SDSS9 band-g", + "IRAC color I1,I2,I4 - (GLIMPSE, SAGE, SAGE-SMC, SINGS)", + "VTSS-Ha", + "XMM PN colored", + "AllWISE color", + "GLIMPSE360" +}; + +const QStringList aladinBackgroundIDs = { + "P/DSS2/color", + "P/DSS2/red", + "P/2MASS/color", + "P/DM/I/350/gaiaedr3", + "P/PanSTARRS/DR1/g", + "P/PanSTARRS/DR1/color-z-zg-g", + "P/DECaPS/DR1/color", + "P/Fermi/color", + "P/Finkbeiner", + "P/GALEXGR6_7/NUV", + "P/IRIS/color", + "P/Mellinger/color", + "P/SDSS9/color", + "P/SDSS9/g", + "P/SPITZER/color", + "P/VTSS/Ha", + "xcatdb/P/XMM/PN/color", + "P/allWISE/color", + "P/GLIMPSE360" +}; + +const QStringList esaSkyBackgrounds = { +}; + +QString SkyMapGUI::backgroundID(const QString& name) +{ + QString id = name; + + if (m_settings.m_map == "Aladin") + { + int idx = aladinBackgrounds.indexOf(name); + if (idx >= 0) { + id = aladinBackgroundIDs[idx]; + } + } + else if (m_settings.m_map == "WWT") + { + if (m_settings.m_projection == "Solar system") + { + m_webInterface->track(m_settings.m_background); + id = "Solar system"; + } + else + { + int idx = wwtPlanets.indexOf(name); + if (idx >= 0) { + id = wwtPlanetIDs[idx]; + } + } + } + return id; +} + +void SkyMapGUI::updateBackgrounds() +{ + QStringList backgrounds; + + if (m_settings.m_map == "WWT") { + if (m_settings.m_projection == "Sky") { + backgrounds = wwtBackgrounds; + } else if (m_settings.m_projection == "Solar system") { + backgrounds = wwtPlanets; + } else { + backgrounds = wwtPlanets; + } + } else if (m_settings.m_map == "ESASky") { + backgrounds = QStringList(); + } else if (m_settings.m_map == "Aladin") { + backgrounds = aladinBackgrounds; + } + + ui->background->blockSignals(true); + ui->background->clear(); + for (int i = 0; i < backgrounds.size(); i++) { + ui->background->addItem(backgrounds[i]); + } + ui->background->blockSignals(false); + int idx = ui->background->findText(m_settings.m_background); + if (idx >= 0) { + ui->background->setCurrentIndex(idx); + } else { + ui->background->setCurrentIndex(0); + } + + on_background_currentIndexChanged(ui->projection->currentIndex()); +} + +void SkyMapGUI::wtmlUpdated(const QList& dataSets) +{ + wwtBackgrounds.clear(); + for (int i = 0; i < dataSets.size(); i++) + { + if (dataSets[i].m_dataSetType == "Sky") { + wwtBackgrounds.append(dataSets[i].m_name); + } + } + updateBackgrounds(); +} + +void SkyMapGUI::updateProjection() +{ + QStringList projections; + + if (m_settings.m_map == "WWT") { + projections = QStringList{"Sky", "Solar system", "Planet"}; + } else if (m_settings.m_map == "ESASky") { + projections = QStringList(); + } else if (m_settings.m_map == "Aladin") { + projections = QStringList{"SIN", "TAN", "STG", "ZEA", "FEYE", "AIR", "ARC", "NCP", "MER", "CAR", "CEA", "CYP", "AIT", "MOL", "PAR", "SFL", "COD", "HPX"}; + } + + ui->projection->blockSignals(true); + ui->projection->clear(); + for (int i = 0; i < projections.size(); i++){ + ui->projection->addItem(projections[i]); + } + ui->projection->blockSignals(false); + int idx = ui->projection->findText(m_settings.m_projection); + if (idx >= 0) { + ui->projection->setCurrentIndex(idx); + } else { + ui->projection->setCurrentIndex(0); + } + + on_projection_currentIndexChanged(ui->projection->currentIndex()); +} + +void SkyMapGUI::updateToolbar() +{ + bool constellationsVisible = false; + bool reticleVisible = true; + bool namesVisible = false; + bool projectionVisible = true; + bool backgroundVisible = true; + + if (m_settings.m_map == "WWT") + { + constellationsVisible = true; + namesVisible = true; + } + else if (m_settings.m_map == "ESASky") + { + projectionVisible = false; + backgroundVisible = false; + reticleVisible = false; + } + + ui->background->setVisible(backgroundVisible); + ui->projection->setVisible(projectionVisible); + + ui->displayNames->setVisible(namesVisible); + ui->displayConstellations->setVisible(constellationsVisible); + ui->displayReticle->setVisible(reticleVisible); + + updateProjection(); +} + +void SkyMapGUI::on_tabs_tabCloseRequested(int index) +{ + QWidget *widget = ui->tabs->widget(index); + ui->tabs->removeTab(index); + delete widget; +} + +void SkyMapGUI::sendToRotator(const QString& name, double az, double alt) +{ + QList rotatorPipes; + MainCore::instance()->getMessagePipes().getMessagePipes(m_skymap, "target", rotatorPipes); + + for (const auto& pipe : rotatorPipes) + { + MessageQueue *messageQueue = qobject_cast(pipe->m_element); + SWGSDRangel::SWGTargetAzimuthElevation *swgTarget = new SWGSDRangel::SWGTargetAzimuthElevation(); + swgTarget->setName(new QString(name)); + swgTarget->setAzimuth(az); + swgTarget->setElevation(alt); + messageQueue->push(MainCore::MsgTargetAzimuthElevation::create(m_skymap, swgTarget)); + } +} + +void SkyMapGUI::updateSourceList() +{ + m_availableChannelOrFeatures = MainCore::instance()->getAvailableChannelsAndFeatures(SkyMapSettings::m_pipeURIs); + + ui->source->blockSignals(true); + ui->source->clear(); + + for (const auto& item : m_availableChannelOrFeatures) { + ui->source->addItem(item.getLongId()); + } + + // Select current setting, if exists + // If not, make sure nothing selected, as channel may be created later on + int idx = ui->source->findText(m_settings.m_source); + if (idx >= 0) { + ui->source->setCurrentIndex(idx); + } else { + ui->source->setCurrentIndex(-1); + } + + ui->source->blockSignals(false); + + // If no current settting, select first available + if (m_settings.m_source.isEmpty()) + { + ui->source->setCurrentIndex(0); + on_source_currentIndexChanged(0); + } +} + +void SkyMapGUI::handleFeatureAdded(int featureSetIndex, Feature *feature) +{ + updateSourceList(); +} + +void SkyMapGUI::handleFeatureRemoved(int featureSetIndex, Feature *oldFeature) +{ + updateSourceList(); +} + +void SkyMapGUI::handleChannelAdded(int deviceSetIndex, ChannelAPI *channel) +{ + updateSourceList(); +} + +void SkyMapGUI::handleChannelRemoved(int deviceSetIndex, ChannelAPI *channel) +{ + updateSourceList(); +} + +void SkyMapGUI::registerPipe(QObject *object) +{ + qDebug("SkyMapGUI::registerPipe: register %s (%p)", qPrintable(object->objectName()), object); + MessagePipes& messagePipes = MainCore::instance()->getMessagePipes(); + + ObjectPipe *pipe = messagePipes.registerProducerToConsumer(object, this, "target"); + MessageQueue *messageQueue = qobject_cast(pipe->m_element); + QObject::connect( + messageQueue, + &MessageQueue::messageEnqueued, + this, + [=](){ this->handlePipeMessageQueue(messageQueue); }, + Qt::QueuedConnection + ); + + pipe = messagePipes.registerProducerToConsumer(object, this, "skymap.target"); + messageQueue = qobject_cast(pipe->m_element); + QObject::connect( + messageQueue, + &MessageQueue::messageEnqueued, + this, + [=](){ this->handlePipeMessageQueue(messageQueue); }, + Qt::QueuedConnection + ); +} + +void SkyMapGUI::handlePipeMessageQueue(MessageQueue* messageQueue) +{ + Message* message; + + while ((message = messageQueue->pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} \ No newline at end of file diff --git a/plugins/feature/skymap/skymapgui.h b/plugins/feature/skymap/skymapgui.h new file mode 100644 index 000000000..ba7b6b090 --- /dev/null +++ b/plugins/feature/skymap/skymapgui.h @@ -0,0 +1,153 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021-2024 Jon Beniston, M7RCE // +// Copyright (C) 2022 Edouard Griffiths, F4EXB // +// // +// 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_FEATURE_SKYMAPGUI_H_ +#define INCLUDE_FEATURE_SKYMAPGUI_H_ + +#include +#include +#include +#include +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) +#include +#endif +#include + +#include +#include + +#include "feature/featuregui.h" +#include "util/messagequeue.h" +#include "settings/rollupstate.h" +#include "maincore.h" + +#include "skymapsettings.h" +#include "webinterface.h" +#include "webserver.h" +#include "wtml.h" + +class PluginAPI; +class FeatureUISet; +class SkyMap; + +namespace Ui { + class SkyMapGUI; +} + +class SkyMapGUI; + +class SkyMapGUI : public FeatureGUI { + Q_OBJECT +public: + static SkyMapGUI* create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature); + virtual void destroy(); + + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + virtual MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + virtual void setWorkspaceIndex(int index); + virtual int getWorkspaceIndex() const { return m_settings.m_workspaceIndex; } + virtual void setGeometryBytes(const QByteArray& blob) { m_settings.m_geometryBytes = blob; } + virtual QByteArray getGeometryBytes() const { return m_settings.m_geometryBytes; } + SkyMap *getSkyMap() { return m_skymap; } + +private: + Ui::SkyMapGUI* ui; + PluginAPI* m_pluginAPI; + FeatureUISet* m_featureUISet; + SkyMapSettings m_settings; + QList m_settingsKeys; + RollupState m_rollupState; + bool m_doApplySettings; + QList m_availableChannelOrFeatures; + QObject *m_source; + + SkyMap* m_skymap; + MessageQueue m_inputMessageQueue; + quint16 m_skymapTileServerPort; + + WebServer *m_webServer; + quint16 m_webPort; + WTML m_wtml; + WebInterface *m_webInterface; + + double m_ra; //!< Target from source plugin + double m_dec; + QDateTime m_dateTime; //!< Date time from source plugin + + explicit SkyMapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent = nullptr); + virtual ~SkyMapGUI(); + + void blockApplySettings(bool block); + void applySetting(const QString& settingsKey); + void applySettings(const QStringList& settingsKeys, bool force = false); + void applyAllSettings(); + void displaySettings(); + bool handleMessage(const Message& message); + void makeUIConnections(); + + void setDateTime(QDateTime dateTime); + QDateTime getDateTime() const; + void setPosition(float latitude, float longitude, float altitude); + QGeoCoordinate getPosition() const; + void initSkyMap(); + QString backgroundID(const QString& name); + void updateBackgrounds(); + void updateToolbar(); + void updateProjection(); + void find(const QString& text); + void sendToRotator(const QString& name, double az, double alt); + void updateSourceList(); + void registerPipe(QObject *object); + +private slots: + void onMenuDialogCalled(const QPoint &p); + void onWidgetRolled(QWidget* widget, bool rollDown); + void handleInputMessages(); + void on_tabs_tabCloseRequested(int index); + void on_find_returnPressed(); + void on_displaySettings_clicked(); + void on_displayNames_clicked(bool checked); + void on_displayConstellations_clicked(bool checked); + void on_displayReticle_clicked(bool checked); + void on_displayGrid_clicked(bool checked); + void on_displayAntennaFoV_clicked(bool checked); + void on_map_currentIndexChanged(int index); + void on_background_currentIndexChanged(int index); + void on_projection_currentIndexChanged(int index); + void on_source_currentIndexChanged(int index); + void on_track_clicked(bool checked); + void fullScreenRequested(QWebEngineFullScreenRequest fullScreenRequest); + void renderProcessTerminated(QWebEnginePage::RenderProcessTerminationStatus terminationStatus, int exitCode); +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + void loadingChanged(const QWebEngineLoadingInfo &loadingInfo); +#endif + void preferenceChanged(int elementType); + void receivedEvent(const QJsonObject &obj); + void wtmlUpdated(const QList& dataSets); + + void handleFeatureAdded(int featureSetIndex, Feature *feature); + void handleFeatureRemoved(int featureSetIndex, Feature *oldFeature); + void handleChannelAdded(int deviceSetIndex, ChannelAPI *channel); + void handleChannelRemoved(int deviceSetIndex, ChannelAPI *channel); + void handlePipeMessageQueue(MessageQueue* messageQueue); + +}; + +#endif // INCLUDE_FEATURE_SKYMAPGUI_H_ diff --git a/plugins/feature/skymap/skymapgui.ui b/plugins/feature/skymap/skymapgui.ui new file mode 100644 index 000000000..2adaffcf5 --- /dev/null +++ b/plugins/feature/skymap/skymapgui.ui @@ -0,0 +1,425 @@ + + + SkyMapGUI + + + + 0 + 0 + 881 + 293 + + + + + 0 + 0 + + + + + 0 + 0 + + + + + Liberation Sans + 9 + + + + Qt::StrongFocus + + + SkyMap + + + + + 0 + 0 + 871 + 41 + + + + + 300 + 0 + + + + Settings + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + + Find + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + Enter name of object to find + + + + + + + Sky map + + + + WWT + + + + + ESASky + + + + + Aladin + + + + + + + + + 180 + 0 + + + + Select background image set + + + + + + + Projection + + + + + + + Display names + + + ^ + + + + :/info.png:/info.png + + + true + + + true + + + + + + + Display constellations + + + + + + + :/skymap/icons/constellation.png:/skymap/icons/constellation.png + + + true + + + true + + + + + + + Display grid + + + ^ + + + + :/gridrect.png:/gridrect.png + + + true + + + true + + + + + + + Display reticle + + + ^ + + + + :/skymap/icons/reticle.png:/skymap/icons/reticle.png + + + true + + + true + + + + + + + Display antenna field-of-view + + + ^ + + + + :/antenna.png:/antenna.png + + + true + + + true + + + + + + + Track source + + + + + + + :/gridpolar.png:/gridpolar.png + + + true + + + true + + + + + + + Source feature to get target from + + + + None + + + + + + + + Show settings dialog + + + + + + + :/listing.png:/listing.png + + + + + + + + + + + 10 + 60 + 591 + 211 + + + + + 0 + 0 + + + + SkyMap + + + + 3 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + true + + + + Map + + + + 3 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 100 + 100 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + + + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
+ + RollupContents + QWidget +
gui/rollupcontents.h
+ 1 +
+ + QWebEngineView + QWidget +
QtWebEngineWidgets/QWebEngineView
+ 1 +
+ + WebView + QWebEngineView +
webview.h
+ 1 +
+
+ + find + background + displayNames + + + + + + +
diff --git a/plugins/feature/skymap/skymapplugin.cpp b/plugins/feature/skymap/skymapplugin.cpp new file mode 100644 index 000000000..c35b7ebf4 --- /dev/null +++ b/plugins/feature/skymap/skymapplugin.cpp @@ -0,0 +1,84 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2015-2023 Edouard Griffiths, F4EXB // +// Copyright (C) 2019 Davide Gerhard // +// Copyright (C) 2020-2023 Jon Beniston, M7RCE // +// Copyright (C) 2020 Kacper Michajłow // +// // +// 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 +#include "plugin/pluginapi.h" + +#ifndef SERVER_MODE +#include "skymapgui.h" +#endif +#include "skymap.h" +#include "skymapplugin.h" +#include "skymapwebapiadapter.h" + +const PluginDescriptor SkyMapPlugin::m_pluginDescriptor = { + SkyMap::m_featureId, + QStringLiteral("Sky Map"), + QStringLiteral("7.18.0"), + QStringLiteral("(c) Jon Beniston, M7RCE"), + QStringLiteral("https://github.com/f4exb/sdrangel"), + true, + QStringLiteral("https://github.com/f4exb/sdrangel") +}; + +SkyMapPlugin::SkyMapPlugin(QObject* parent) : + QObject(parent), + m_pluginAPI(nullptr) +{ +} + +const PluginDescriptor& SkyMapPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void SkyMapPlugin::initPlugin(PluginAPI* pluginAPI) +{ + m_pluginAPI = pluginAPI; + + m_pluginAPI->registerFeature(SkyMap::m_featureIdURI, SkyMap::m_featureId, this); +} + +#ifdef SERVER_MODE +FeatureGUI* SkyMapPlugin::createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const +{ + (void) featureUISet; + (void) feature; + return nullptr; +} +#else +FeatureGUI* SkyMapPlugin::createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const +{ + return SkyMapGUI::create(m_pluginAPI, featureUISet, feature); +} +#endif + +Feature* SkyMapPlugin::createFeature(WebAPIAdapterInterface* webAPIAdapterInterface) const +{ + return new SkyMap(webAPIAdapterInterface); +} + +FeatureWebAPIAdapter* SkyMapPlugin::createFeatureWebAPIAdapter() const +{ + return new SkyMapWebAPIAdapter(); +} diff --git a/plugins/feature/skymap/skymapplugin.h b/plugins/feature/skymap/skymapplugin.h new file mode 100644 index 000000000..8c8ea3bf4 --- /dev/null +++ b/plugins/feature/skymap/skymapplugin.h @@ -0,0 +1,52 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2015-2020 Edouard Griffiths, F4EXB // +// Copyright (C) 2015 John Greb // +// Copyright (C) 2020-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_FEATURE_SKYMAPPLUGIN_H +#define INCLUDE_FEATURE_SKYMAPPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +class FeatureGUI; +class WebAPIAdapterInterface; + +class SkyMapPlugin : public QObject, PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID "sdrangel.feature.skymap") + +public: + explicit SkyMapPlugin(QObject* parent = nullptr); + + const PluginDescriptor& getPluginDescriptor() const; + void initPlugin(PluginAPI* pluginAPI); + + virtual FeatureGUI* createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const; + virtual Feature* createFeature(WebAPIAdapterInterface *webAPIAdapterInterface) const; + virtual FeatureWebAPIAdapter* createFeatureWebAPIAdapter() const; + +private: + static const PluginDescriptor m_pluginDescriptor; + + PluginAPI* m_pluginAPI; +}; + +#endif // INCLUDE_FEATURE_SKYMAPPLUGIN_H diff --git a/plugins/feature/skymap/skymapsettings.cpp b/plugins/feature/skymap/skymapsettings.cpp new file mode 100644 index 000000000..c01243bbc --- /dev/null +++ b/plugins/feature/skymap/skymapsettings.cpp @@ -0,0 +1,361 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2015-2017, 2019-2020, 2022 Edouard Griffiths, F4EXB // +// Copyright (C) 2020-2024 Jon Beniston, M7RCE // +// Copyright (C) 2022 CRD716 // +// // +// 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 +#include + +#include "util/simpleserializer.h" +#include "util/httpdownloadmanager.h" +#include "settings/serializable.h" + +#include "skymapsettings.h" + +const QStringList SkyMapSettings::m_pipeTypes = { + QStringLiteral("StarTracker"), + QStringLiteral("SatelliteTracker"), + QStringLiteral("GS232Controller"), + QStringLiteral("Map") +}; + +const QStringList SkyMapSettings::m_pipeURIs = { + QStringLiteral("sdrangel.feature.startracker"), + QStringLiteral("sdrangel.feature.satellitetracker"), + QStringLiteral("sdrangel.feature.gs232controller"), + QStringLiteral("sdrangel.feature.map") +}; + +SkyMapSettings::SkyMapSettings() : + m_rollupState(nullptr) +{ + resetToDefaults(); +} + +SkyMapSettings::~SkyMapSettings() +{ +} + +void SkyMapSettings::resetToDefaults() +{ + m_map = "WWT"; + m_displayNames = true; + m_displayConstellations = true; + m_displayReticle = true; + m_displayGrid = false; + m_displayAntennaFoV = false; + m_projection = ""; + m_source = ""; + m_track = false; + m_hpbw = 10.0f; + m_latitude = 0.0f; + m_longitude = 0.0f; + m_altitude = 0.0f; + m_useMyPosition = false; + m_wwtSettings = QHash({ + {"constellationBoundaries", false}, + {"constellationFigures", true}, + {"constellationLabels", true}, + {"constellationPictures", false}, + {"constellationSelection", false}, + {"ecliptic", false}, + {"eclipticOverviewText", false}, + {"eclipticGrid", false}, + {"eclipticGridText", false}, + {"altAzGrid", false}, + {"altAzGridText", false}, + {"galacticGrid", false}, + {"galacticGridText", false}, + {"elevationModel", false}, + {"earthSky", false}, + {"horizon", false}, + {"iss", false}, + {"precessionChart", false}, + {"skyGrids", false}, + {"skyNode", false}, + {"skyOverlays", false}, + {"solarSystemCosmos", false}, + {"solarSystemLighting", true}, + {"solarSystemMilkyWay", true}, + {"solarSystemMinorOrbits", false}, + {"solarSystemMinorPlanets", false}, + {"solarSystemMultiRes", true}, + {"solarSystemOrbits", true}, + {"solarSystemOverlays", false}, + {"solarSystemPlanets", true}, + {"solarSystemStars", true}, + + }); + m_title = "Sky Map"; + m_useReverseAPI = false; + m_reverseAPIAddress = "127.0.0.1"; + m_reverseAPIPort = 8888; + m_reverseAPIFeatureSetIndex = 0; + m_reverseAPIFeatureIndex = 0; + m_workspaceIndex = 0; +} + +QByteArray SkyMapSettings::serialize() const +{ + SimpleSerializer s(1); + + s.writeString(2, m_map); + s.writeBool(1, m_displayNames); + s.writeBool(15, m_displayConstellations); + s.writeBool(17, m_displayReticle); + s.writeBool(18, m_displayGrid); + s.writeBool(21, m_displayAntennaFoV); + s.writeString(3, m_projection); + s.writeString(4, m_source); + s.writeBool(20, m_track); + s.writeFloat(22, m_hpbw); + s.writeFloat(23, m_latitude); + s.writeFloat(24, m_longitude); + s.writeFloat(25, m_altitude); + s.writeBool(26, m_useMyPosition); + s.writeHash(27, m_wwtSettings); + + s.writeString(8, m_title); + s.writeU32(9, m_rgbColor); + s.writeBool(10, m_useReverseAPI); + s.writeString(11, m_reverseAPIAddress); + s.writeU32(12, m_reverseAPIPort); + s.writeU32(13, m_reverseAPIFeatureSetIndex); + s.writeU32(14, m_reverseAPIFeatureIndex); + + if (m_rollupState) { + s.writeBlob(19, m_rollupState->serialize()); + } + + s.writeS32(33, m_workspaceIndex); + s.writeBlob(34, m_geometryBytes); + + return s.final(); +} + +bool SkyMapSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if (!d.isValid()) + { + resetToDefaults(); + return false; + } + + if (d.getVersion() == 1) + { + QByteArray bytetmp; + uint32_t utmp; + QString strtmp; + QByteArray blob; + QString string; + + d.readString(2, &m_map, "WWT"); + d.readBool(1, &m_displayNames, true); + d.readBool(15, &m_displayConstellations, true); + d.readBool(17, &m_displayReticle, true); + d.readBool(18, &m_displayGrid, true); + d.readBool(21, &m_displayAntennaFoV, true); + d.readString(3, &m_projection, ""); + d.readString(4, &m_source, ""); + d.readBool(20, &m_track, false); + d.readFloat(22, &m_hpbw, 10.0f); + d.readFloat(23, &m_latitude, 0.0f); + d.readFloat(24, &m_longitude, 0.0f); + d.readFloat(25, &m_altitude, 0.0f); + d.readBool(26, &m_useMyPosition, false); + d.readHash(27, &m_wwtSettings); + + d.readString(8, &m_title, "Sky Map"); + d.readU32(9, &m_rgbColor, QColor(225, 25, 99).rgba()); + d.readBool(10, &m_useReverseAPI, false); + d.readString(11, &m_reverseAPIAddress, "127.0.0.1"); + d.readU32(12, &utmp, 0); + + if ((utmp > 1023) && (utmp < 65535)) { + m_reverseAPIPort = utmp; + } else { + m_reverseAPIPort = 8888; + } + + d.readU32(13, &utmp, 0); + m_reverseAPIFeatureSetIndex = utmp > 99 ? 99 : utmp; + d.readU32(14, &utmp, 0); + m_reverseAPIFeatureIndex = utmp > 99 ? 99 : utmp; + + if (m_rollupState) + { + d.readBlob(19, &bytetmp); + m_rollupState->deserialize(bytetmp); + } + + d.readS32(33, &m_workspaceIndex, 0); + d.readBlob(34, &m_geometryBytes); + + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +void SkyMapSettings::applySettings(const QStringList& settingsKeys, const SkyMapSettings& settings) +{ + if (settingsKeys.contains("map")) { + m_map = settings.m_map; + } + if (settingsKeys.contains("displayNames")) { + m_displayNames = settings.m_displayNames; + } + if (settingsKeys.contains("displayConstellations")) { + m_displayConstellations = settings.m_displayConstellations; + } + if (settingsKeys.contains("displayReticle")) { + m_displayReticle = settings.m_displayReticle; + } + if (settingsKeys.contains("displayGrid")) { + m_displayGrid = settings.m_displayGrid; + } + if (settingsKeys.contains("displayAntennaFoV")) { + m_displayAntennaFoV = settings.m_displayAntennaFoV; + } + if (settingsKeys.contains("background")) { + m_background = settings.m_background; + } + if (settingsKeys.contains("projection")) { + m_projection = settings.m_projection; + } + if (settingsKeys.contains("source")) { + m_source = settings.m_source; + } + if (settingsKeys.contains("track")) { + m_track = settings.m_track; + } + if (settingsKeys.contains("hpbw")) { + m_hpbw = settings.m_hpbw; + } + if (settingsKeys.contains("latitude")) { + m_latitude = settings.m_latitude; + } + if (settingsKeys.contains("longitude")) { + m_longitude = settings.m_longitude; + } + if (settingsKeys.contains("altitude")) { + m_altitude = settings.m_altitude; + } + if (settingsKeys.contains("useMyPosition")) { + m_useMyPosition = settings.m_useMyPosition; + } + if (settingsKeys.contains("title")) { + m_title = settings.m_title; + } + if (settingsKeys.contains("useReverseAPI")) { + m_useReverseAPI = settings.m_useReverseAPI; + } + if (settingsKeys.contains("reverseAPIAddress")) { + m_reverseAPIAddress = settings.m_reverseAPIAddress; + } + if (settingsKeys.contains("reverseAPIPort")) { + m_reverseAPIPort = settings.m_reverseAPIPort; + } + if (settingsKeys.contains("reverseAPIFeatureSetIndex")) { + m_reverseAPIFeatureSetIndex = settings.m_reverseAPIFeatureSetIndex; + } + if (settingsKeys.contains("reverseAPIFeatureIndex")) { + m_reverseAPIFeatureIndex = settings.m_reverseAPIFeatureIndex; + } + if (settingsKeys.contains("workspaceIndex")) { + m_workspaceIndex = settings.m_workspaceIndex; + } +} + +QString SkyMapSettings::getDebugString(const QStringList& settingsKeys, bool force) const +{ + std::ostringstream ostr; + + if (settingsKeys.contains("map") || force) { + ostr << " m_map: " << m_map.toStdString(); + } + if (settingsKeys.contains("displayNames") || force) { + ostr << " m_displayNames: " << m_displayNames; + } + if (settingsKeys.contains("displayConstellations") || force) { + ostr << " m_displayConstellations: " << m_displayConstellations; + } + if (settingsKeys.contains("displayReticle") || force) { + ostr << " m_displayReticle: " << m_displayReticle; + } + if (settingsKeys.contains("displayAntennaFoV") || force) { + ostr << " m_displayAntennaFoV: " << m_displayAntennaFoV; + } + if (settingsKeys.contains("background") || force) { + ostr << " m_background: " << m_background.toStdString(); + } + if (settingsKeys.contains("projection") || force) { + ostr << " m_projection: " << m_projection.toStdString(); + } + if (settingsKeys.contains("source") || force) { + ostr << " m_source: " << m_source.toStdString(); + } + if (settingsKeys.contains("track") || force) { + ostr << " m_track: " << m_track; + } + if (settingsKeys.contains("hpbw") || force) { + ostr << " m_hpbw: " << m_hpbw; + } + if (settingsKeys.contains("latitude") || force) { + ostr << " m_latitude: " << m_latitude; + } + if (settingsKeys.contains("longitude") || force) { + ostr << " m_longitude: " << m_longitude; + } + if (settingsKeys.contains("altitude") || force) { + ostr << " m_altitude: " << m_altitude; + } + if (settingsKeys.contains("useMyPosition") || force) { + ostr << " m_useMyPosition: " << m_useMyPosition; + } + if (settingsKeys.contains("title") || force) { + ostr << " m_title: " << m_title.toStdString(); + } + if (settingsKeys.contains("useReverseAPI") || force) { + ostr << " m_useReverseAPI: " << m_useReverseAPI; + } + if (settingsKeys.contains("reverseAPIAddress") || force) { + ostr << " m_reverseAPIAddress: " << m_reverseAPIAddress.toStdString(); + } + if (settingsKeys.contains("reverseAPIPort") || force) { + ostr << " m_reverseAPIPort: " << m_reverseAPIPort; + } + if (settingsKeys.contains("reverseAPIFeatureSetIndex") || force) { + ostr << " m_reverseAPIFeatureSetIndex: " << m_reverseAPIFeatureSetIndex; + } + if (settingsKeys.contains("reverseAPIFeatureIndex") || force) { + ostr << " m_reverseAPIFeatureIndex: " << m_reverseAPIFeatureIndex; + } + if (settingsKeys.contains("workspaceIndex") || force) { + ostr << " m_workspaceIndex: " << m_workspaceIndex; + } + + return QString(ostr.str().c_str()); +} + diff --git a/plugins/feature/skymap/skymapsettings.h b/plugins/feature/skymap/skymapsettings.h new file mode 100644 index 000000000..5731fde16 --- /dev/null +++ b/plugins/feature/skymap/skymapsettings.h @@ -0,0 +1,75 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2015-2020, 2022 Edouard Griffiths, F4EXB // +// Copyright (C) 2020-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_FEATURE_SKYMAPSETTINGS_H_ +#define INCLUDE_FEATURE_SKYMAPSETTINGS_H_ + +#include +#include +#include + +class Serializable; + +struct SkyMapSettings +{ + QString m_map; //!< "WWT", "ESASky" or "Aladin" + bool m_displayNames; + bool m_displayConstellations; + bool m_displayReticle; + bool m_displayGrid; + bool m_displayAntennaFoV; + QString m_background; + QString m_projection; + QString m_source; //!< Plugin to get target coords from + bool m_track; //!< Track source + float m_hpbw; //!< Antenna HPBW + float m_latitude; //!< Antenna position + float m_longitude; + float m_altitude; + bool m_useMyPosition; + QHash m_wwtSettings; //!< WWT specific settings + + QString m_title; + quint32 m_rgbColor; + bool m_useReverseAPI; + QString m_reverseAPIAddress; + uint16_t m_reverseAPIPort; + uint16_t m_reverseAPIFeatureSetIndex; + uint16_t m_reverseAPIFeatureIndex; + Serializable *m_rollupState; + int m_workspaceIndex; + QByteArray m_geometryBytes; + + SkyMapSettings(); + ~SkyMapSettings(); + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + void setRollupState(Serializable *rollupState) { m_rollupState = rollupState; } + void applySettings(const QStringList& settingsKeys, const SkyMapSettings& settings); + QString getDebugString(const QStringList& settingsKeys, bool force=false) const; + + static const QStringList m_pipeTypes; + static const QStringList m_pipeURIs; + +}; + +#endif // INCLUDE_FEATURE_SKYMAPSETTINGS_H_ + diff --git a/plugins/feature/skymap/skymapsettingsdialog.cpp b/plugins/feature/skymap/skymapsettingsdialog.cpp new file mode 100644 index 000000000..2ed2e4296 --- /dev/null +++ b/plugins/feature/skymap/skymapsettingsdialog.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 +#include + +#include "skymapsettingsdialog.h" + +SkyMapSettingsDialog::SkyMapSettingsDialog(SkyMapSettings *settings, QWidget* parent) : + QDialog(parent), + m_settings(settings), + ui(new Ui::SkyMapSettingsDialog) +{ + ui->setupUi(this); + + ui->hpbw->setValue(m_settings->m_hpbw); + ui->latitude->setText(QString::number(m_settings->m_latitude, 'f', 6)); + ui->longitude->setText(QString::number(m_settings->m_longitude, 'f', 6)); + ui->altitude->setValue(m_settings->m_altitude); + ui->useMyPosition->setChecked(m_settings->m_useMyPosition); + + // WWT + ui->constellationBoundaries->setChecked(m_settings->m_wwtSettings.value("constellationBoundaries").toBool()); + ui->constellationFigures->setChecked(m_settings->m_wwtSettings.value("constellationFigures").toBool()); + ui->constellationLabels->setChecked(m_settings->m_wwtSettings.value("constellationLabels").toBool()); + ui->constellationPictures->setChecked(m_settings->m_wwtSettings.value("constellationPictures").toBool()); + ui->constellationSelection->setChecked(m_settings->m_wwtSettings.value("constellationSelection").toBool()); + + ui->ecliptic->setChecked(m_settings->m_wwtSettings.value("ecliptic").toBool()); + ui->eclipticOverviewText->setChecked(m_settings->m_wwtSettings.value("eclipticOverviewText").toBool()); + ui->eclipticGrid->setChecked(m_settings->m_wwtSettings.value("eclipticGrid").toBool()); + ui->eclipticGridText->setChecked(m_settings->m_wwtSettings.value("eclipticGridText").toBool()); + ui->altAzGrid->setChecked(m_settings->m_wwtSettings.value("altAzGrid").toBool()); + ui->altAzGridText->setChecked(m_settings->m_wwtSettings.value("altAzGridText").toBool()); + ui->equatorialGrid->setChecked(m_settings->m_wwtSettings.value("equatorialGrid").toBool()); + ui->equatorialGridText->setChecked(m_settings->m_wwtSettings.value("equatorialGridText").toBool()); + ui->galacticGrid->setChecked(m_settings->m_wwtSettings.value("galacticGrid").toBool()); + ui->galacticGridText->setChecked(m_settings->m_wwtSettings.value("galacticGridText").toBool()); + ui->precessionChart->setChecked(m_settings->m_wwtSettings.value("precessionChart").toBool()); + + ui->solarSystemCosmos->setChecked(m_settings->m_wwtSettings.value("solarSystemCosmos").toBool()); + ui->solarSystemLighting->setChecked(m_settings->m_wwtSettings.value("solarSystemLighting").toBool()); + ui->solarSystemMilkyWay->setChecked(m_settings->m_wwtSettings.value("solarSystemMilkyWay").toBool()); + ui->solarSystemMinorOrbits->setChecked(m_settings->m_wwtSettings.value("solarSystemMinorOrbits").toBool()); + ui->solarSystemMinorPlanets->setChecked(m_settings->m_wwtSettings.value("solarSystemMinorPlanets").toBool()); + ui->solarSystemMultiRes->setChecked(m_settings->m_wwtSettings.value("solarSystemMultiRes").toBool()); + ui->solarSystemOrbits->setChecked(m_settings->m_wwtSettings.value("solarSystemOrbits").toBool()); + //ui->solarSystemOverlays->setChecked(m_settings->m_wwtSettings.value("solarSystemOverlays").toBool()); + ui->solarSystemPlanets->setChecked(m_settings->m_wwtSettings.value("solarSystemPlanets").toBool()); + ui->solarSystemStars->setChecked(m_settings->m_wwtSettings.value("solarSystemStars").toBool()); + + ui->iss->setChecked(m_settings->m_wwtSettings.value("iss").toBool()); +} + +SkyMapSettingsDialog::~SkyMapSettingsDialog() +{ + delete ui; +} + +void SkyMapSettingsDialog::accept() +{ + QDialog::accept(); + + if (m_settings->m_hpbw != ui->hpbw->value()) + { + m_settings->m_hpbw = ui->hpbw->value(); + m_settingsKeysChanged.append("hpbw"); + } + if (m_settings->m_latitude != ui->latitude->text().toFloat()) + { + m_settings->m_latitude = ui->latitude->text().toFloat(); + m_settingsKeysChanged.append("latitude"); + } + if (m_settings->m_longitude != ui->longitude->text().toFloat()) + { + m_settings->m_longitude = ui->longitude->text().toFloat(); + m_settingsKeysChanged.append("longitude"); + } + if (m_settings->m_altitude != ui->altitude->value()) + { + m_settings->m_altitude = ui->altitude->value(); + m_settingsKeysChanged.append("altitude"); + } + if (m_settings->m_useMyPosition != ui->useMyPosition->isChecked()) + { + m_settings->m_useMyPosition = ui->useMyPosition->isChecked(); + m_settingsKeysChanged.append("useMyPosition"); + } + + m_settings->m_wwtSettings.insert("constellationBoundaries", ui->constellationBoundaries->isChecked()); + m_settings->m_wwtSettings.insert("constellationFigures", ui->constellationFigures->isChecked()); + m_settings->m_wwtSettings.insert("constellationLabels", ui->constellationLabels->isChecked()); + m_settings->m_wwtSettings.insert("constellationPictures", ui->constellationPictures->isChecked()); + m_settings->m_wwtSettings.insert("constellationSelection", ui->constellationSelection->isChecked()); + + m_settings->m_wwtSettings.insert("ecliptic", ui->ecliptic->isChecked()); + m_settings->m_wwtSettings.insert("eclipticOverviewText", ui->eclipticOverviewText->isChecked()); + m_settings->m_wwtSettings.insert("eclipticGrid", ui->eclipticGrid->isChecked()); + m_settings->m_wwtSettings.insert("eclipticGridText", ui->eclipticGridText->isChecked()); + m_settings->m_wwtSettings.insert("altAzGrid", ui->altAzGrid->isChecked()); + m_settings->m_wwtSettings.insert("altAzGridText", ui->altAzGridText->isChecked()); + m_settings->m_wwtSettings.insert("equatorialGrid", ui->equatorialGrid->isChecked()); + m_settings->m_wwtSettings.insert("equatorialGridText", ui->equatorialGridText->isChecked()); + m_settings->m_wwtSettings.insert("galacticGrid", ui->galacticGrid->isChecked()); + m_settings->m_wwtSettings.insert("galacticGridText", ui->galacticGridText->isChecked()); + m_settings->m_wwtSettings.insert("precessionChart", ui->precessionChart->isChecked()); + + m_settings->m_wwtSettings.insert("solarSystemCosmos", ui->solarSystemCosmos->isChecked()); + m_settings->m_wwtSettings.insert("solarSystemLighting", ui->solarSystemLighting->isChecked()); + m_settings->m_wwtSettings.insert("solarSystemMilkyWay", ui->solarSystemMilkyWay->isChecked()); + m_settings->m_wwtSettings.insert("solarSystemMinorOrbits", ui->solarSystemMinorOrbits->isChecked()); + m_settings->m_wwtSettings.insert("solarSystemMinorPlanets", ui->solarSystemMinorPlanets->isChecked()); + m_settings->m_wwtSettings.insert("solarSystemMultiRes", ui->solarSystemMultiRes->isChecked()); + m_settings->m_wwtSettings.insert("solarSystemOrbits", ui->solarSystemOrbits->isChecked()); + //m_settings->m_wwtSettings.insert("solarSystemOverlays", ui->solarSystemOverlays->isChecked()); + m_settings->m_wwtSettings.insert("solarSystemPlanets", ui->solarSystemPlanets->isChecked()); + m_settings->m_wwtSettings.insert("solarSystemStars", ui->solarSystemStars->isChecked()); + + m_settings->m_wwtSettings.insert("iss", ui->iss->isChecked()); + + m_settingsKeysChanged.append("wwtSettings"); // Being lazy here +} + +void SkyMapSettingsDialog::on_constellationBoundaries_toggled(bool checked) +{ + ui->constellationSelection->setEnabled(checked); +} + +void SkyMapSettingsDialog::on_ecliptic_toggled(bool checked) +{ + ui->eclipticOverviewText->setEnabled(checked); +} + +void SkyMapSettingsDialog::on_eclipticGrid_toggled(bool checked) +{ + ui->eclipticGridText->setEnabled(checked); +} + +void SkyMapSettingsDialog::on_altAzGrid_toggled(bool checked) +{ + ui->altAzGridText->setEnabled(checked); +} + +void SkyMapSettingsDialog::on_equatorialGrid_toggled(bool checked) +{ + ui->equatorialGridText->setEnabled(checked); +} + +void SkyMapSettingsDialog::on_galacticGrid_toggled(bool checked) +{ + ui->galacticGridText->setEnabled(checked); +} + +void SkyMapSettingsDialog::on_useMyPosition_toggled(bool checked) +{ + ui->latitude->setEnabled(!checked); + ui->latitudeLabel->setEnabled(!checked); + ui->longitude->setEnabled(!checked); + ui->longitudeLabel->setEnabled(!checked); + ui->altitude->setEnabled(!checked); + ui->altitudeLabel->setEnabled(!checked); +} diff --git a/plugins/feature/skymap/skymapsettingsdialog.h b/plugins/feature/skymap/skymapsettingsdialog.h new file mode 100644 index 000000000..710520082 --- /dev/null +++ b/plugins/feature/skymap/skymapsettingsdialog.h @@ -0,0 +1,56 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_FEATURE_SKYMAPSETTINGSDIALOG_H +#define INCLUDE_FEATURE_SKYMAPSETTINGSDIALOG_H + +#include +#include +#include + +#include "ui_skymapsettingsdialog.h" +#include "skymapsettings.h" + +class SkyMapSettingsDialog : public QDialog { + Q_OBJECT + +public: + explicit SkyMapSettingsDialog(SkyMapSettings *settings, QWidget* parent = 0); + ~SkyMapSettingsDialog(); + +public: + QStringList m_settingsKeysChanged; // List of setting keys that have been changed + +private: + SkyMapSettings *m_settings; + +private slots: + void accept(); + void on_constellationBoundaries_toggled(bool checked); + void on_ecliptic_toggled(bool checked); + void on_eclipticGrid_toggled(bool checked); + void on_altAzGrid_toggled(bool checked); + void on_equatorialGrid_toggled(bool checked); + void on_galacticGrid_toggled(bool checked); + void on_useMyPosition_toggled(bool checked); + +private: + Ui::SkyMapSettingsDialog* ui; + +}; + +#endif // INCLUDE_FEATURE_SKYMAPSETTINGSDIALOG_H diff --git a/plugins/feature/skymap/skymapsettingsdialog.ui b/plugins/feature/skymap/skymapsettingsdialog.ui new file mode 100644 index 000000000..6e5a92269 --- /dev/null +++ b/plugins/feature/skymap/skymapsettingsdialog.ui @@ -0,0 +1,480 @@ + + + SkyMapSettingsDialog + + + + 0 + 0 + 441 + 671 + + + + + 9 + + + + SkyMap Display Settings + + + + + + + 0 + + + + + 0 + + + + Settings + + + + + + Use observation location from Preferences > My Position + + + + + + + Latitude (°) + + + + + + + Longitude (°) + + + + + + + Altitude (m) + + + + + + + Antenna/observation altitude in metres + + + -100 + + + 10000 + + + + + + + HPBW (°) + + + + + + + Antenna half-power beamwidth in degrees + + + 0.010000000000000 + + + 360.000000000000000 + + + + + + + Latitude of antenna/observation location in degrees. North positive + + + + + + + Longitude of antenna/observation location in degrees. East positive + + + + + + + + WWT + + + + + + Grids + + + + + + + 160 + 0 + + + + Ecliptic + + + + + + + Ecliptic Grid + + + + + + + false + + + Ecliptic Overview Text + + + + + + + false + + + Ecliptic Grid Text + + + + + + + Alt Az Grid + + + + + + + false + + + Alt Az Grid Text + + + + + + + Equatorial Grid + + + + + + + false + + + Equatorial Grid Text + + + + + + + Galactic Grid + + + + + + + false + + + Galactic Grid Text + + + + + + + Precession Chart + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Constellations + + + + + + Display constellation stick figures + + + Figures + + + + + + + + 160 + 0 + + + + Display boundaries between constellations + + + Boundaries + + + + + + + Displays boundary only for constellation at the center of view + + + Centre boundary only + + + + + + + Display constellation names + + + Labels + + + + + + + Display constellation pictures + + + Pictures + + + + + + + + + + Display ISS model + + + ISS + + + + + + + Settings that apply only to the Solar System view + + + Solar System View + + + + + + + 160 + 0 + + + + Displays cosmos (when zoomed far out) + + + Cosmos + + + + + + + Displays Milky Way + + + Milky Way + + + + + + + Displays stars + + + Stars + + + + + + + Displays planets + + + Planets + + + + + + + Displays asteroids + + + Asteroids + + + + + + + Displays planetary orbits + + + Planetary Orbits + + + + + + + Displays orbits of moons and minor satellites + + + Moon and Satellite Orbits + + + + + + + Applies lighting and shadows to planets + + + Lighting and Shadows + + + + + + + Uses higher resolution planetary imagery when zooming in + + + Mulit Res Planets + + + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + + buttonBox + accepted() + SkyMapSettingsDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + SkyMapSettingsDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/plugins/feature/skymap/skymapwebapiadapter.cpp b/plugins/feature/skymap/skymapwebapiadapter.cpp new file mode 100644 index 000000000..15b9bb537 --- /dev/null +++ b/plugins/feature/skymap/skymapwebapiadapter.cpp @@ -0,0 +1,54 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2015-2020 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 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 "SWGFeatureSettings.h" +#include "skymap.h" +#include "skymapwebapiadapter.h" + +SkyMapWebAPIAdapter::SkyMapWebAPIAdapter() +{} + +SkyMapWebAPIAdapter::~SkyMapWebAPIAdapter() +{} + +int SkyMapWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setSkyMapSettings(new SWGSDRangel::SWGSkyMapSettings()); + response.getSkyMapSettings()->init(); + SkyMap::webapiFormatFeatureSettings(response, m_settings); + + return 200; +} + +int SkyMapWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) force; // no action + (void) errorMessage; + SkyMap::webapiUpdateFeatureSettings(m_settings, featureSettingsKeys, response); + + return 200; +} diff --git a/plugins/feature/skymap/skymapwebapiadapter.h b/plugins/feature/skymap/skymapwebapiadapter.h new file mode 100644 index 000000000..fae3e597c --- /dev/null +++ b/plugins/feature/skymap/skymapwebapiadapter.h @@ -0,0 +1,50 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019-2020 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 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_SKYMAP_WEBAPIADAPTER_H +#define INCLUDE_SKYMAP_WEBAPIADAPTER_H + +#include "feature/featurewebapiadapter.h" +#include "skymapsettings.h" + +/** + * Standalone API adapter only for the settings + */ +class SkyMapWebAPIAdapter : public FeatureWebAPIAdapter { +public: + SkyMapWebAPIAdapter(); + virtual ~SkyMapWebAPIAdapter(); + + virtual QByteArray serialize() const { return m_settings.serialize(); } + virtual bool deserialize(const QByteArray& data) { return m_settings.deserialize(data); } + + virtual int webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage); + +private: + SkyMapSettings m_settings; +}; + +#endif // INCLUDE_SKYMAP_WEBAPIADAPTER_H diff --git a/plugins/feature/skymap/webinterface.cpp b/plugins/feature/skymap/webinterface.cpp new file mode 100644 index 000000000..e929849e6 --- /dev/null +++ b/plugins/feature/skymap/webinterface.cpp @@ -0,0 +1,160 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 +#include + +#include "webinterface.h" + +WebInterface::WebInterface(QObject *parent) : + WebSocketServer(parent) +{ +} + +// Set the current camera view to a given coordinates +void WebInterface::setView(double ra, double dec, float zoom) +{ + QJsonObject obj { + {"command", "setView"}, + {"ra", ra}, + {"dec", dec}, + {"zoom", zoom} + }; + send(obj); +} + +// Set observation position +void WebInterface::setPosition(const QGeoCoordinate& position) +{ + QJsonObject obj { + {"command", "setPosition"}, + {"latitude", position.latitude()}, + {"longitude", position.longitude()}, + {"altitude", position.altitude()} + }; + send(obj); +} + +// Set date and time +void WebInterface::setDateTime(QDateTime dateTime) +{ + QJsonObject obj { + {"command", "setDateTime"}, + {"dateTime", dateTime.toUTC().toString(Qt::ISODateWithMs)} + }; + send(obj); +} + +// Set the camera to track the item with the specified name +void WebInterface::track(const QString& name) +{ + QJsonObject obj { + {"command", "track"}, + {"name", name} + }; + send(obj); +} + +void WebInterface::setBackground(const QString &background) +{ + QJsonObject obj { + {"command", "setBackground"}, + {"background", background}, + }; + send(obj); +} + +void WebInterface::setProjection(const QString &projection) +{ + QJsonObject obj { + {"command", "setProjection"}, + {"projection", projection}, + }; + send(obj); +} + +void WebInterface::showConstellations(bool show) +{ + QJsonObject obj { + {"command", "showConstellations"}, + {"show", show} + }; + send(obj); +} + +void WebInterface::showReticle(bool show) +{ + QJsonObject obj { + {"command", "showReticle"}, + {"show", show} + }; + send(obj); +} + +void WebInterface::showGrid(bool show) +{ + QJsonObject obj { + {"command", "showGrid"}, + {"show", show} + }; + send(obj); +} + +void WebInterface::showNames(bool show) +{ + QJsonObject obj { + {"command", "showNames"}, + {"show", show} + }; + send(obj); +} + +void WebInterface::showAntennaFoV(bool show) +{ + QJsonObject obj { + {"command", "showAntennaFoV"}, + {"show", show} + }; + send(obj); +} + +void WebInterface::setAntennaFoV(float hpbw) +{ + QJsonObject obj { + {"command", "setAntennaFoV"}, + {"hpbw", hpbw} + }; + send(obj); +} + +void WebInterface::setWWTSettings(const QHash& settings) +{ + QJsonObject obj { + {"command", "setWWTSettings"}, + }; + + QHashIterator itr(settings); + while (itr.hasNext()) + { + itr.next(); + QString key = itr.key(); + QVariant value = itr.value(); + obj.insert(key, value.toString()); + } + + send(obj); +} diff --git a/plugins/feature/skymap/webinterface.h b/plugins/feature/skymap/webinterface.h new file mode 100644 index 000000000..b339d9b5a --- /dev/null +++ b/plugins/feature/skymap/webinterface.h @@ -0,0 +1,47 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_FEATURE_WEBINTERFACE_H_ +#define INCLUDE_FEATURE_WEBINTERFACE_H_ + +#include + +#include "websocketserver.h" + +// Interface between C++ code and Web page via Web Socket using JSON +class WebInterface : public WebSocketServer +{ +public: + + WebInterface(QObject *parent = nullptr); + void setView(double ra, double dec, float zoom=1.0f); + void setPosition(const QGeoCoordinate& position); + void setDateTime(QDateTime dateTime); + void track(const QString &name); + void setBackground(const QString& name); + void setProjection(const QString& name); + void showConstellations(bool show); + void showReticle(bool show); + void showGrid(bool show); + void showAntennaFoV(bool show); + void showNames(bool show); + void setAntennaFoV(float hpbw); + void setWWTSettings(const QHash& settings); + +}; + +#endif // INCLUDE_FEATURE_WEBINTERFACE_H_ diff --git a/plugins/feature/skymap/webserver.cpp b/plugins/feature/skymap/webserver.cpp new file mode 100644 index 000000000..1adb1b966 --- /dev/null +++ b/plugins/feature/skymap/webserver.cpp @@ -0,0 +1,222 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2022-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 +#include +#include +#include + +#include "webserver.h" + +// port - port to listen on / is listening on. Use 0 for any free port. +WebServer::WebServer(quint16 &port, QObject* parent) : + QTcpServer(parent), + m_defaultMimeType("application/octet-stream") +{ + listen(QHostAddress::Any, port); + port = serverPort(); + qDebug() << "WebServer on port " << port; + + m_mimeTypes.insert(".html", new MimeType("text/html; charset=\"utf-8\"", false)); + m_mimeTypes.insert(".png", new MimeType("image/png")); + m_mimeTypes.insert(".glb", new MimeType("model/gltf-binary")); + m_mimeTypes.insert(".glbe", new MimeType("model/gltf-binary")); + m_mimeTypes.insert(".js", new MimeType("text/javascript")); + m_mimeTypes.insert(".css", new MimeType("text/css")); + m_mimeTypes.insert(".json", new MimeType("application/json")); + m_mimeTypes.insert(".geojson", new MimeType("application/geo+json")); +} + +void WebServer::incomingConnection(qintptr socket) +{ + QTcpSocket* s = new QTcpSocket(this); + connect(s, SIGNAL(readyRead()), this, SLOT(readClient())); + connect(s, SIGNAL(disconnected()), this, SLOT(discardClient())); + s->setSocketDescriptor(socket); + //addPendingConnection(socket); +} + +// Don't include leading or trailing / in from +void WebServer::addPathSubstitution(const QString &from, const QString &to) +{ + qDebug() << "Mapping " << from << " to " << to; + m_pathSubstitutions.insert(from, to); +} + +void WebServer::addSubstitution(QString path, QString from, QString to) +{ + Substitution *s = new Substitution(from, to); + if (m_substitutions.contains(path)) + { + QList *list = m_substitutions.value(path); + QMutableListIterator i(*list); + while (i.hasNext()) { + Substitution *sub = i.next(); + if (sub->m_from == from) { + i.remove(); + delete sub; + } + } + list->append(s); + } + else + { + QList *list = new QList(); + list->append(s); + m_substitutions.insert(path, list); + } +} + +QString WebServer::substitute(QString path, QString html) +{ + QList *list = m_substitutions.value(path); + for (const auto s : *list) { + html = html.replace(s->m_from, s->m_to); + } + return html; +} + +void WebServer::addFile(const QString &path, const QByteArray &data) +{ + m_files.insert(path, data); +} + +void WebServer::sendFile(QTcpSocket* socket, const QByteArray &data, MimeType *mimeType, const QString &path) +{ + QString header = QString("HTTP/1.0 200 Ok\r\nContent-Type: %1\r\nAccess-Control-Allow-Origin \"*\"\r\n\r\n").arg(mimeType->m_type); + if (mimeType->m_binary) + { + // Send file as binary + QByteArray headerUtf8 = header.toUtf8(); + socket->write(headerUtf8); + socket->write(data); + } + else + { + // Send file as text + QString html = QString(data); + // Make any substitutions in the content of the file + if (m_substitutions.contains(path)) { + html = substitute(path, html); + } + QTextStream os(socket); + os.setAutoDetectUnicode(true); + os << header << html; + } +} + +void WebServer::readClient() +{ + QTcpSocket* socket = (QTcpSocket*)sender(); + if (socket->canReadLine()) + { + QString line = socket->readLine(); + //qDebug() << "WebServer HTTP Request: " << line; + + QStringList tokens = QString(line).split(QRegularExpression("[ \r\n][ \r\n]*")); + if (tokens[0] == "GET") + { + // Get file type from extension + QString path = tokens[1]; + MimeType *mimeType = &m_defaultMimeType; + int extensionIdx = path.lastIndexOf("."); + if (extensionIdx != -1) { + QString extension = path.mid(extensionIdx); + if (m_mimeTypes.contains(extension)) { + mimeType = m_mimeTypes[extension]; + } + } + + // Try skymapping path + QStringList dirs = path.split('/'); + if ((dirs.length() >= 2) && m_pathSubstitutions.contains(dirs[1])) + { + dirs[1] = m_pathSubstitutions.value(dirs[1]); + dirs.removeFirst(); + QString newPath = dirs.join('/'); + //qDebug() << "SkyMapping " << path << " to " << newPath; + path = newPath; + } + + // See if we can find the file in our resources + QResource res(path); +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + if (res.isValid() && (res.uncompressedSize() > 0)) + { + QByteArray data = res.uncompressedData(); + sendFile(socket, data, mimeType, path); + } +#else + if (res.isValid() && (res.size() > 0)) + { + QByteArray data = QByteArray::fromRawData((const char *)res.data(), res.size()); + if (res.isCompressed()) { + data = qUncompress(data); + } + sendFile(socket, data, mimeType, path); + } +#endif + else if (m_files.contains(path)) + { + // Path is a file held in memory + sendFile(socket, m_files.value(path).data(), mimeType, path); + } + else + { + // See if we can find a file on disk + QFile file(path); + if (file.open(QIODevice::ReadOnly)) + { + QByteArray data = file.readAll(); + if (path.endsWith(".glbe")) { + for (int i = 0; i < data.size(); i++) { + data[i] = data[i] ^ 0x55; + } + } + sendFile(socket, data, mimeType, path); + } + else + { + qDebug() << "WebServer " << path << " not found"; + // File not found + QTextStream os(socket); + os.setAutoDetectUnicode(true); + os << "HTTP/1.0 404 Not Found\r\n" + "Content-Type: text/html; charset=\"utf-8\"\r\n" + "\r\n" + "\n" + "\n" + "

404 Not Found

\n" + "\n" + "\n"; + } + } + + socket->close(); + + if (socket->state() == QTcpSocket::UnconnectedState) { + delete socket; + } + } + } +} + +void WebServer::discardClient() +{ + QTcpSocket* socket = (QTcpSocket*)sender(); + socket->deleteLater(); +} diff --git a/plugins/feature/skymap/webserver.h b/plugins/feature/skymap/webserver.h new file mode 100644 index 000000000..da3e034b3 --- /dev/null +++ b/plugins/feature/skymap/webserver.h @@ -0,0 +1,80 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2022-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_WEB_SERVER_H_ +#define INCLUDE_WEB_SERVER_H_ + +#include +#include + +// WebServer for making simple dynamic html pages and serving binaries from +// resources or local disk +class WebServer : public QTcpServer +{ + Q_OBJECT + + struct Substitution { + QString m_from; + QString m_to; + Substitution(const QString& from, const QString& to) : + m_from(from), + m_to(to) + { + } + }; + + struct MimeType { + QString m_type; + bool m_binary; + MimeType(const QString& type, bool binary=true) : + m_type(type), + m_binary(binary) + { + } + }; + +private: + + // Hash of a list of paths to substitute + QHash m_pathSubstitutions; + + // Hash of path to a list of substitutions to make in the file + QHash*> m_substitutions; + + // Hash of files held in memory + QHash m_files; + + // Hash of filename extension to MIME type information + QHash m_mimeTypes; + MimeType m_defaultMimeType; + +public: + WebServer(quint16 &port, QObject* parent = 0); + void incomingConnection(qintptr socket) override; + void addPathSubstitution(const QString &from, const QString &to); + void addSubstitution(QString path, QString from, QString to); + void addFile(const QString &path, const QByteArray &data); + QString substitute(QString path, QString html); + void sendFile(QTcpSocket* socket, const QByteArray &data, MimeType *mimeType, const QString &path); + +private slots: + void readClient(); + void discardClient(); + +}; + +#endif diff --git a/plugins/feature/skymap/websocketserver.cpp b/plugins/feature/skymap/websocketserver.cpp new file mode 100644 index 000000000..9d59886f3 --- /dev/null +++ b/plugins/feature/skymap/websocketserver.cpp @@ -0,0 +1,101 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2022 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 + +#include "websocketserver.h" + +WebSocketServer::WebSocketServer(QObject *parent) : + QObject(parent), + m_socket("", QWebSocketServer::NonSecureMode, this), + m_client(nullptr) +{ + connect(&m_socket, &QWebSocketServer::newConnection, this, &WebSocketServer::onNewConnection); + int port = 0; + if (!m_socket.listen(QHostAddress::Any, port)) { + qCritical() << "WebSocketServer - Unable to listen on port " << port; + } +} + +quint16 WebSocketServer::serverPort() +{ + return m_socket.serverPort(); +} + +void WebSocketServer::onNewConnection() +{ + QWebSocket *socket = m_socket.nextPendingConnection(); + + connect(socket, &QWebSocket::textMessageReceived, this, &WebSocketServer::processTextMessage); + connect(socket, &QWebSocket::binaryMessageReceived, this, &WebSocketServer::processBinaryMessage); + connect(socket, &QWebSocket::disconnected, this, &WebSocketServer::socketDisconnected); + + m_client = socket; + + emit connected(); +} + +void WebSocketServer::processTextMessage(QString message) +{ + //QWebSocket *client = qobject_cast(sender()); + //qDebug() << "WebSocketServer::processTextMessage - Received text " << message; + + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8(), &error); + if (!doc.isNull() && doc.isObject()) { + emit received(doc.object()); + } else { + qDebug() << "WebSocketServer::processTextMessage: " << error.errorString(); + } +} + +void WebSocketServer::processBinaryMessage(QByteArray message) +{ + //QWebSocket *client = qobject_cast(sender()); + // Shouldn't receive any binary messages for now + qDebug() << "WebSocketServer::processBinaryMessage - Received binary " << message; +} + +void WebSocketServer::socketDisconnected() +{ + QWebSocket *client = qobject_cast(sender()); + if (client) { + client->deleteLater(); + m_client = nullptr; + } +} + +bool WebSocketServer::isConnected() +{ + return m_client != nullptr; +} + +void WebSocketServer::send(const QJsonObject &obj) +{ + if (m_client) + { + QJsonDocument doc(obj); + QByteArray bytes = doc.toJson(); + qint64 bytesSent = m_client->sendTextMessage(bytes); + m_client->flush(); // Try to reduce latency + if (bytesSent != bytes.size()) { + qDebug() << "WebSocketServer::update - Sent only " << bytesSent << " bytes out of " << bytes.size(); + } + //qDebug() << obj; + } +} + diff --git a/plugins/feature/skymap/websocketserver.h b/plugins/feature/skymap/websocketserver.h new file mode 100644 index 000000000..26d4a1510 --- /dev/null +++ b/plugins/feature/skymap/websocketserver.h @@ -0,0 +1,59 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2015-2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2021-2022 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_FEATURE_WEBSOCKERSERVER_H_ +#define INCLUDE_FEATURE_WEBSOCKERSERVER_H_ + +#include +#include +#include +#include + +class WebSocketServer : public QObject +{ + Q_OBJECT + +private: + + QWebSocketServer m_socket; + QWebSocket *m_client; + +public: + + WebSocketServer(QObject *parent = nullptr); + quint16 serverPort(); + + bool isConnected(); + void send(const QJsonObject &obj); + +signals: + void connected(); + void received(const QJsonObject &obj); + +public slots: + + void onNewConnection(); + void processTextMessage(QString message); + void processBinaryMessage(QByteArray message); + void socketDisconnected(); + +}; + +#endif // INCLUDE_FEATURE_MAPWEBSOCKERSERVER_H_ diff --git a/plugins/feature/skymap/webview.cpp b/plugins/feature/skymap/webview.cpp new file mode 100644 index 000000000..f6c04c857 --- /dev/null +++ b/plugins/feature/skymap/webview.cpp @@ -0,0 +1,52 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "webview.h" + +#include +#include +#include + +WebView::WebView(QWidget *parent) : + QWebEngineView(parent) +{ +} + +QWebEngineView *WebView::createWindow(QWebEnginePage::WebWindowType type) +{ + QWebEngineView *view = new QWebEngineView(); + connect(view, &QWebEngineView::titleChanged, this, &WebView::on_titleChanged); + + QHBoxLayout *layout = new QHBoxLayout; + layout->addWidget(view); + + int tab = m_tabs->addTab(view, "Web"); + m_tabs->setCurrentIndex(tab); + + return view; +} + +void WebView::on_titleChanged(const QString& title) +{ + QWebEngineView *view = qobject_cast(sender()); + for (int i = 0; i < m_tabs->count(); i++) + { + if (m_tabs->widget(i) == view) { + m_tabs->setTabText(i, title); + } + } +} diff --git a/plugins/feature/skymap/webview.h b/plugins/feature/skymap/webview.h new file mode 100644 index 000000000..d0ed1a157 --- /dev/null +++ b/plugins/feature/skymap/webview.h @@ -0,0 +1,46 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_FEATURE_WEBVIEW_H_ +#define INCLUDE_FEATURE_WEBVIEW_H_ + +#include + +#include "websocketserver.h" + +class QTabWidget; + +class WebView : public QWebEngineView +{ + Q_OBJECT + +public: + + WebView(QWidget *parent = nullptr); + + QWebEngineView *createWindow(QWebEnginePage::WebWindowType type) override; + + void setTabs(QTabWidget *tabs) { m_tabs = tabs; } + +private slots: + void on_titleChanged(const QString& title); + +private: + QTabWidget *m_tabs; +}; + +#endif // INCLUDE_FEATURE_WEBVIEW_H_ diff --git a/plugins/feature/skymap/wtml.cpp b/plugins/feature/skymap/wtml.cpp new file mode 100644 index 000000000..c7210e2bb --- /dev/null +++ b/plugins/feature/skymap/wtml.cpp @@ -0,0 +1,105 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "wtml.h" + +#include +#include +#include +#include + +WTML::WTML() +{ + m_networkManager = new QNetworkAccessManager(); + QObject::connect(m_networkManager, &QNetworkAccessManager::finished, this, &WTML::handleReply); +} + +WTML::~WTML() +{ + QObject::disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &WTML::handleReply); + delete m_networkManager; +} + +void WTML::getData() +{ + // https://worldwidetelescope.org/wwtweb/catalog.aspx?X=ImageSets6 + // https://worldwidetelescope.org/wwtweb/catalog.aspx?W=exploreroot6 + QUrl url(QString("https://worldwidetelescope.org/wwtweb/catalog.aspx?X=ImageSets6")); + m_networkManager->get(QNetworkRequest(url)); +} + +void WTML::handleReply(QNetworkReply* reply) +{ + if (!reply->error()) + { + QXmlStreamReader xmlReader(reply->readAll()); + + QList dataSets; + + while (!xmlReader.atEnd() && !xmlReader.hasError()) + { + while (xmlReader.readNextStartElement()) + { + if (xmlReader.name() == QLatin1String("Folder")) + { + while(xmlReader.readNextStartElement()) + { + if (xmlReader.name() == QLatin1String("ImageSet")) + { + QString name = xmlReader.attributes().value("Name").toString(); + QString dataSetType = xmlReader.attributes().value("DataSetType").toString(); + + if (!name.isEmpty() && !dataSetType.isEmpty()) + { + ImageSet imageSet; + imageSet.m_name = name; + imageSet.m_dataSetType = dataSetType; + dataSets.append(imageSet); + //qDebug() << "Adding ImageSet " << name << dataSetType; + } + + // Credits, Thumbnail etc + while(xmlReader.readNextStartElement()) + { + xmlReader.skipCurrentElement(); + } + } + else + { + xmlReader.skipCurrentElement(); + } + } + } + else + { + xmlReader.skipCurrentElement(); + } + } + } + // Ignore "Premature end of document." here even if ok + if (!xmlReader.atEnd() && xmlReader.hasError()) { + qDebug() << "WTML::handleReply: Error parsing XML: " << xmlReader.errorString(); + } + + emit dataUpdated(dataSets); + } + else + { + qDebug() << "WTML::handleReply: error: " << reply->error(); + } + reply->deleteLater(); +} diff --git a/plugins/feature/skymap/wtml.h b/plugins/feature/skymap/wtml.h new file mode 100644 index 000000000..6e3db15fb --- /dev/null +++ b/plugins/feature/skymap/wtml.h @@ -0,0 +1,54 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_WTML_H +#define INCLUDE_WTML_H + +#include + +class QNetworkAccessManager; +class QNetworkReply; + +// World Wide Telescope WTML files containing imageset catalogs +class WTML : public QObject +{ + Q_OBJECT + +public: + + struct ImageSet { + QString m_name; + QString m_dataSetType; + }; + + WTML(); + ~WTML(); + + void getData(); + +public slots: + void handleReply(QNetworkReply* reply); + +signals: + void dataUpdated(const QList& dataSets); // Emitted when new data are available. + +private: + QNetworkAccessManager *m_networkManager; + +}; + +#endif /* INCLUDE_WTML_H */ diff --git a/plugins/feature/startracker/readme.md b/plugins/feature/startracker/readme.md index 39ab6f720..d2cbf6311 100644 --- a/plugins/feature/startracker/readme.md +++ b/plugins/feature/startracker/readme.md @@ -9,6 +9,7 @@ The Star Tracker feature plugin is for use in radio astronomy and EME (Earth-Moo * The overhead position of the Sun, Moon and selected star can be displayed on the Map Feature. * It can display local Sidereal time, solar flux density and sky temperature. * It can plot the line of sight through the Milky Way. +* It can send the target to the Sky Map plugin, to display associated imagery in a variety of wavelengths. It can also use the Sky Map to set the target. * The plugin can communicate with Stellarium, allowing Stellarium to control SDRangel as though it was a telescope and for the direction the antenna is pointing to be displayed in Stellarium.

Settings

@@ -66,7 +67,7 @@ Specifies the longitude in decimal degrees (East positive) of the observation po

8: Time

-Select the date and time at which the position of the target should be calculated. Select either Now, for the current time, or Custom to manually enter a date and time. +Select the local date and time at which the position of the target should be calculated. Select either Now, for the current time, or Custom to manually enter a date and time.

9: LST - Local Sidereal Time

@@ -100,6 +101,7 @@ To allow Stellarium to set the RA and Dec, select Custom RA/Dec, and ensure the | S8 | HI | IAU primary calibration region (l=207,b=-15) | Tb=72 peak | | S9 | HI | IAU secondary calibration region (l=356,b=-4) | Tb=85 peak | | SatelliteTracker | | Gets target Az/El from Satellite Tracker | | +| SkyMap | | Gets target RA/Dec from Sky Map Tracker | | References: @@ -240,6 +242,12 @@ The Star Tracker feature can send the overhead position of the Sun, Moon and tar When using the Find feature in the Map GUI, you can search for "Sun", "Moon" or "Star". +

Sky Map

+ +The Star Tracker feature will send the target RA/Dec, observation point (antenna location) and antenna beamwidth to the Sky Map. +If the Star Tracker is set as the Source plugin in the Sky Map, pressing the Track button in the Sky Map will result in the Sky Map tracking the target +selected in the Star Tracker. +

Stellarium Interface

In Star Tracker: @@ -288,15 +296,15 @@ Icons are by Freepik from Flaticon https://www.flaticon.com/ Full details of the API can be found in the Swagger documentation. Here is a quick example of how to set the target to the Moon at the current time: - curl -X PATCH "http://127.0.0.1:8091/sdrangel/featureset/0/feature/0/settings" -d '{"featureType": "StarTracker", "StarTrackerSettings": { "target": "Moon", "dateTime": "" }}' + curl -X PATCH "http://127.0.0.1:8091/sdrangel/featureset/feature/0/settings" -d '{"featureType": "StarTracker", "StarTrackerSettings": { "target": "Moon", "dateTime": "" }}' Or to a custom RA and declination on a given date and time: - curl -X PATCH "http://127.0.0.1:8091/sdrangel/featureset/0/feature/0/settings" -d '{"featureType": "StarTracker", "StarTrackerSettings": { "target": "Custom", "ra": "03h32m59.35s", "dec": "54d34m45.05s", "dateTime": "1921-04-15T10:17:05" }}' + curl -X PATCH "http://127.0.0.1:8091/sdrangel/featureset/feature/0/settings" -d '{"featureType": "StarTracker", "StarTrackerSettings": { "target": "Custom", "ra": "03h32m59.35s", "dec": "54d34m45.05s", "dateTime": "1921-04-15T10:17:05" }}' To start tracking: - curl -X POST "http://127.0.0.1:8091/sdrangel/featureset/0/feature/0/run" + curl -X POST "http://127.0.0.1:8091/sdrangel/featureset/feature/0/run"

Developer Notes

diff --git a/plugins/feature/startracker/startracker.cpp b/plugins/feature/startracker/startracker.cpp index 9f8572ee8..5e557084d 100644 --- a/plugins/feature/startracker/startracker.cpp +++ b/plugins/feature/startracker/startracker.cpp @@ -936,7 +936,7 @@ void StarTracker::scanAvailableFeatures() { Feature *feature = featureSet->getFeatureAt(fei); - if (feature->getURI() == "sdrangel.feature.satellitetracker") + if ((feature->getURI() == "sdrangel.feature.satellitetracker") || (feature->getURI() == "sdrangel.feature.skymap")) { StarTrackerSettings::AvailableFeature satelliteTracker = StarTrackerSettings::AvailableFeature{featureSet->getIndex(), fei, feature->getIdentifier()}; diff --git a/plugins/feature/startracker/startrackergui.cpp b/plugins/feature/startracker/startrackergui.cpp index 2fe36a244..23bc38513 100644 --- a/plugins/feature/startracker/startrackergui.cpp +++ b/plugins/feature/startracker/startrackergui.cpp @@ -232,15 +232,15 @@ bool StarTrackerGUI::handleMessage(const Message& message) void StarTrackerGUI::updateSatelliteTrackerList(const QList& satelliteTrackers) { - // Update list of satellite trackers + // Update list of plugins we can get target from ui->target->blockSignals(true); - // Remove Satellite Trackers no longer available + // Remove targets no longer available for (int i = 0; i < ui->target->count(); ) { QString text = ui->target->itemText(i); bool found = false; - if (text.contains("SatelliteTracker")) + if (text.contains("SatelliteTracker") || text.contains("SkyMap")) { for (const auto& satelliteTracker : satelliteTrackers) { @@ -262,7 +262,7 @@ void StarTrackerGUI::updateSatelliteTrackerList(const QListtarget->findText(m_settings.m_target); if (index >= 0) { diff --git a/plugins/feature/startracker/startrackergui.ui b/plugins/feature/startracker/startrackergui.ui index bc41ef77b..933b27733 100644 --- a/plugins/feature/startracker/startrackergui.ui +++ b/plugins/feature/startracker/startrackergui.ui @@ -141,10 +141,10 @@ Antenna half power (-3dB) beamwidth (degrees) - 1 + 2 - 0.100000000000000 + 0.010000000000000 360.000000000000000 diff --git a/plugins/feature/startracker/startrackersettings.h b/plugins/feature/startracker/startrackersettings.h index b4de84bd0..9ca7019b1 100644 --- a/plugins/feature/startracker/startrackersettings.h +++ b/plugins/feature/startracker/startrackersettings.h @@ -52,7 +52,7 @@ struct StarTrackerSettings double m_latitude; double m_longitude; QString m_target; // Sun, Moon, Custom - QString m_dateTime; // Date/time for observation, or "" for now + QString m_dateTime; // Local date/time for observation, or "" for now QString m_refraction; // Refraction correction. "None", "Saemundsson" or "Positional Astronomy Library" double m_pressure; // Air pressure in millibars double m_temperature; // Air temperature in C diff --git a/plugins/feature/startracker/startrackerworker.cpp b/plugins/feature/startracker/startrackerworker.cpp index 81516b39e..cd62c0bab 100644 --- a/plugins/feature/startracker/startrackerworker.cpp +++ b/plugins/feature/startracker/startrackerworker.cpp @@ -30,6 +30,7 @@ #include "SWGTargetAzimuthElevation.h" #include "SWGMapItem.h" #include "SWGStarTrackerTarget.h" +#include "SWGSkyMapTarget.h" #include "webapi/webapiadapterinterface.h" #include "webapi/webapiutils.h" @@ -384,7 +385,7 @@ void StarTrackerWorker::updateRaDec(RADec rd, QDateTime dt, bool lbTarget) // Send to Stellarium writeStellariumTarget(rdJ2000.ra, rdJ2000.dec); // Send to GUI - if (m_settings.m_target == "Sun" || m_settings.m_target == "Moon" || (m_settings.m_target == "Custom Az/El") || lbTarget || m_settings.m_target.contains("SatelliteTracker")) + if (m_settings.m_target == "Sun" || m_settings.m_target == "Moon" || (m_settings.m_target == "Custom Az/El") || lbTarget || m_settings.m_target.contains("SatelliteTracker") || m_settings.m_target.contains("SkyMap")) { if (getMessageQueueToGUI()) { @@ -517,6 +518,26 @@ void StarTrackerWorker::update() else qDebug() << "StarTrackerWorker::update - Failed to parse feature name " << m_settings.m_target; } + if (m_settings.m_target.contains("SkyMap")) + { + // Get RA/Dec from Sky Map + double ra, dec; + unsigned int featureSetIndex,featureIndex; + + if (MainCore::getFeatureIndexFromId(m_settings.m_target, featureSetIndex, featureIndex)) + { + if (ChannelWebAPIUtils::getFeatureReportValue(featureSetIndex, featureIndex, "ra", ra) + && ChannelWebAPIUtils::getFeatureReportValue(featureSetIndex, featureIndex, "dec", dec)) + { + m_settings.m_ra = QString::number(ra, 'f', 10); + m_settings.m_dec = QString::number(dec, 'f', 10); + } + else + qDebug() << "StarTrackerWorker::update - Failed to target from feature " << m_settings.m_target; + } + else + qDebug() << "StarTrackerWorker::update - Failed to parse feature name " << m_settings.m_target; + } if (m_settings.m_target == "Sun") { @@ -654,6 +675,33 @@ void StarTrackerWorker::update() } } + // Send RA/Dec, position, beamwidth and date to Sky Map + QList skyMapPipes; + MainCore::instance()->getMessagePipes().getMessagePipes(m_starTracker, "skymap.target", skyMapPipes); + for (const auto& pipe : skyMapPipes) + { + MessageQueue *messageQueue = qobject_cast(pipe->m_element); + SWGSDRangel::SWGSkyMapTarget *swgTarget = new SWGSDRangel::SWGSkyMapTarget(); + if (m_settings.m_jnow) + { + double jd = Astronomy::julianDate(dt); + RADec rdJ2000 = Astronomy::precess(rd, jd, Astronomy::jd_j2000()); + swgTarget->setRa(rdJ2000.ra); + swgTarget->setDec(rdJ2000.dec); + } + else + { + swgTarget->setRa(rd.ra); + swgTarget->setDec(rd.dec); + } + swgTarget->setLatitude(m_settings.m_latitude); + swgTarget->setLongitude(m_settings.m_longitude); + swgTarget->setAltitude(m_settings.m_heightAboveSeaLevel); + swgTarget->setDateTime(new QString(dt.toString(Qt::ISODateWithMs))); + swgTarget->setHpbw(m_settings.m_beamwidth); + messageQueue->push(MainCore::MsgSkyMapTarget::create(m_starTracker, swgTarget)); + } + // Send to Map if (m_settings.m_drawSunOnMap || m_settings.m_drawMoonOnMap || m_settings.m_drawStarOnMap) { diff --git a/sdrbase/maincore.cpp b/sdrbase/maincore.cpp index d1d899d34..bec2ece84 100644 --- a/sdrbase/maincore.cpp +++ b/sdrbase/maincore.cpp @@ -71,6 +71,7 @@ MESSAGE_CLASS_DEFINITION(MainCore::MsgTargetAzimuthElevation, Message) MESSAGE_CLASS_DEFINITION(MainCore::MsgStarTrackerTarget, Message) MESSAGE_CLASS_DEFINITION(MainCore::MsgStarTrackerDisplaySettings, Message) MESSAGE_CLASS_DEFINITION(MainCore::MsgStarTrackerDisplayLoSSettings, Message) +MESSAGE_CLASS_DEFINITION(MainCore::MsgSkyMapTarget, Message) MainCore::MainCore() { @@ -435,6 +436,100 @@ void MainCore::updateWakeLock() } #endif +QList MainCore::getAvailableChannels(const QStringList& uris) +{ + QList list; + std::vector& deviceSets = MainCore::instance()->getDeviceSets(); + + for (const auto deviceSet : m_deviceSets) + { + for (int chi = 0; chi < deviceSet->getNumberOfChannels(); chi++) + { + ChannelAPI* channel = deviceSet->getChannelAt(chi); + + if ((uris.size() == 0) || uris.contains(channel->getURI())) + { + QChar type = getDeviceSetTypeId(deviceSet); + int streamIdx = type == 'M' ? channel->getStreamIndex() : -1; + + AvailableChannelOrFeature item { + type, + deviceSet->getIndex(), + chi, + streamIdx, + channel->getIdentifier(), + channel + }; + list.append(item); + } + } + } + + return list; +} + +QList MainCore::getAvailableFeatures(const QStringList& uris) +{ + QList list; + std::vector& featureSets = MainCore::instance()->getFeatureeSets(); + + for (const auto& featureSet : featureSets) + { + for (int fei = 0; fei < featureSet->getNumberOfFeatures(); fei++) + { + Feature *feature = featureSet->getFeatureAt(fei); + + if ((uris.size() == 0) || uris.contains(feature->getURI())) + { + AvailableChannelOrFeature item { + 'F', + featureSet->getIndex(), + fei, + -1, + feature->getIdentifier(), + feature + }; + list.append(item); + } + } + } + + return list; +} + +QList MainCore::getAvailableChannelsAndFeatures(const QStringList& uris) +{ + QList list; + + list.append(getAvailableChannels(uris)); + list.append(getAvailableFeatures(uris)); + + return list; + +} + +QObject *MainCore::getAvailableChannelOrFeatureById(const QString& id, const QList& list) +{ + for (const auto& item : list) + { + if (item.getId() == id) { + return item.m_object; + } + } + return nullptr; +} + +QObject *MainCore::getAvailableChannelOrFeatureByLongId(const QString& longId, const QList& list) +{ + for (const auto& item : list) + { + if (item.getLongId() == longId) { + return item.m_object; + } + } + return nullptr; +} + QChar MainCore::getDeviceSetTypeId(const DeviceSet* deviceSet) { if (deviceSet->m_deviceMIMOEngine) { @@ -461,9 +556,11 @@ QString MainCore::getChannelId(const ChannelAPI* channel) DeviceSet* deviceSet = deviceSets[channel->getDeviceSetIndex()]; QString deviceSetId = getDeviceSetId(deviceSet); int index = channel->getIndexInDeviceSet(); - // FIXME: if (deviceSet->m_deviceMIMOEngine) { - // we should append stream index. E.g. "M0:0.0" However, only ChannelGUI seems to know what it is - return QString("%1:%2").arg(deviceSetId).arg(index); + if (deviceSet->m_deviceMIMOEngine) { + return QString("%1:%2.%3").arg(deviceSetId).arg(index).arg(channel->getStreamIndex()); + } else { + return QString("%1:%2").arg(deviceSetId).arg(index); + } } QStringList MainCore::getDeviceSetIds(bool rx, bool tx, bool mimo) diff --git a/sdrbase/maincore.h b/sdrbase/maincore.h index ee3e1e569..ae6f3bc9e 100644 --- a/sdrbase/maincore.h +++ b/sdrbase/maincore.h @@ -59,6 +59,7 @@ namespace SWGSDRangel class SWGStarTrackerTarget; class SWGStarTrackerDisplaySettings; class SWGStarTrackerDisplayLoSSettings; + class SWGSkyMapTarget; } class SDRBASE_API MainCore : public QObject @@ -842,7 +843,28 @@ public: { } }; + class SDRBASE_API MsgSkyMapTarget : public Message { + MESSAGE_CLASS_DECLARATION + public: + const QObject *getPipeSource() const { return m_pipeSource; } + SWGSDRangel::SWGSkyMapTarget *getSWGSkyMapTarget() const { return m_swgSkyMapTarget; } + + static MsgSkyMapTarget* create(const QObject *pipeSource, SWGSDRangel::SWGSkyMapTarget *swgSkyMapTarget) + { + return new MsgSkyMapTarget(pipeSource, swgSkyMapTarget); + } + + private: + const QObject *m_pipeSource; + SWGSDRangel::SWGSkyMapTarget *m_swgSkyMapTarget; + + MsgSkyMapTarget(const QObject *pipeSource, SWGSDRangel::SWGSkyMapTarget *swgSkyMapTarget) : + Message(), + m_pipeSource(pipeSource), + m_swgSkyMapTarget(swgSkyMapTarget) + { } + }; MainCore(); ~MainCore(); @@ -886,6 +908,40 @@ public: // Position const QGeoPositionInfo& getPosition() const; + struct AvailableChannelOrFeature + { + QChar m_kind; //!< 'R' or 'T' for channel, 'M' for MIMO channel, 'F' for feature as from MainCore::getDeviceSetTypeId + int m_superIndex; //!< Device Set index or Feature Set index + int m_index; //!< Channel or Feature index + int m_streamIndex; //!< For MIMO channels only + QString m_type; //!< Plugin type (E.g. NFMDemod) + QObject *m_object; //!< Pointer to the object (ChannelAPI or Feature object) + + AvailableChannelOrFeature() = default; + AvailableChannelOrFeature(const AvailableChannelOrFeature&) = default; + AvailableChannelOrFeature& operator=(const AvailableChannelOrFeature&) = default; + bool operator==(const AvailableChannelOrFeature& a) const { + return (m_kind == a.m_kind) && (m_superIndex == a.m_superIndex) && (m_index == a.m_index) && (m_type == a.m_type); + } + QString getId() const { // Eg: "R3:4" + QString id = QString("%1%2:%3").arg(m_kind).arg(m_superIndex).arg(m_index); + if (m_kind == "M") { + id.append(QString(".%1").arg(m_streamIndex)); + } + return id; + } + QString getLongId() const { // Eg: "F0:1 StarTracker" + return QString("%1 %2").arg(getId()).arg(m_type); + } + }; + + // Use QList so ordered numerically + QList getAvailableChannels(const QStringList& uris); // Get hash of available channels with given URIs or all if empty list. Hash hey is Id + QList getAvailableFeatures(const QStringList& uris); // Get hash of available features with given URIs or all if empty list. Hash key is Id + QList getAvailableChannelsAndFeatures(const QStringList& uris); // Get hash of available channels and features with given URIs or all if empty list. Hash key is Id + static QObject *getAvailableChannelOrFeatureById(const QString& id, const QList& list); + static QObject *getAvailableChannelOrFeatureByLongId(const QString& longId, const QList& list); + // Ids QChar getDeviceSetTypeId(const DeviceSet* deviceSet); //!< Get Type Id (E.g. 'R', 'T' or 'M') for the given device set QString getDeviceSetId(const DeviceSet* deviceSet); //!< Get Id (E.g. "R2") for the given device set diff --git a/sdrbase/util/units.h b/sdrbase/util/units.h index 8ff59e84b..59ad21f6d 100644 --- a/sdrbase/util/units.h +++ b/sdrbase/util/units.h @@ -339,6 +339,43 @@ public: return false; } + // Try to convert a string to Right Ascension (RA) and Declination. Returns false if not recognised format. + // This supports HMS/DMS or decimal. + // E.g.: + // 12 05 12.23 +17 06 21.0 + // 12:05:12.23 -17:06:21.0 + // 12h05m12.23s +17d06m21.0s + // 107.1324 -34.233 + static bool stringToRADec(const QString& string, float& ra, float& dec) + { + QRegExp dms("([0-9]+)[ :h]([0-9]+)[ :m]([0-9]+(\\.[0-9]+)?)s? *,? *([+-]?[0-9]+)[ :d]([0-9]+)[ :m]([0-9]+(\\.[0-9]+)?)s?"); + if (dms.exactMatch(string)) + { + int raHours = dms.capturedTexts()[1].toInt(); + int raMins = dms.capturedTexts()[2].toInt(); + float raSecs = dms.capturedTexts()[3].toFloat(); + ra = raHours + raMins / 60.0f + raSecs / (60.0f * 60.0f); + qDebug() << ra << raHours << raMins << raSecs; + int decDegs = dms.capturedTexts()[5].toInt(); + int decMins = dms.capturedTexts()[6].toInt(); + float decSecs = dms.capturedTexts()[7].toFloat(); + bool neg = decDegs < 0; + dec = abs(decDegs) + decMins / 60.0f + decSecs / (60.0f * 60.0f); + if (neg) { + dec = -dec; + } + return true; + } + QRegExp decimal("([0-9]+(\\.[0-9]+)?) *,? *([+-]?[0-9]+(\\.[0-9]+)?)"); + if (decimal.exactMatch(string)) + { + ra = decimal.capturedTexts()[1].toFloat(); + dec = decimal.capturedTexts()[3].toFloat(); + return true; + } + return false; + } + static float solarFluxUnitsToJansky(float sfu) { return sfu * 10000.0f; diff --git a/sdrbase/webapi/webapirequestmapper.cpp b/sdrbase/webapi/webapirequestmapper.cpp index 2e90c726c..675faa6af 100644 --- a/sdrbase/webapi/webapirequestmapper.cpp +++ b/sdrbase/webapi/webapirequestmapper.cpp @@ -5273,6 +5273,11 @@ bool WebAPIRequestMapper::getFeatureSettings( featureSettings->setSimplePttSettings(new SWGSDRangel::SWGSimplePTTSettings()); featureSettings->getSimplePttSettings()->fromJsonObject(settingsJsonObject); } + else if (featureSettingsKey == "SkyMapSettings") + { + featureSettings->setSkyMapSettings(new SWGSDRangel::SWGSkyMapSettings()); + featureSettings->getSkyMapSettings()->fromJsonObject(settingsJsonObject); + } else if (featureSettingsKey == "StarTrackerSettings") { featureSettings->setStarTrackerSettings(new SWGSDRangel::SWGStarTrackerSettings()); @@ -5366,6 +5371,11 @@ bool WebAPIRequestMapper::getFeatureActions( featureActions->setSimplePttActions(new SWGSDRangel::SWGSimplePTTActions()); featureActions->getSimplePttActions()->fromJsonObject(actionsJsonObject); } + else if (featureActionsKey == "SkyMapActions") + { + featureActions->setSkyMapActions(new SWGSDRangel::SWGSkyMapActions()); + featureActions->getSkyMapActions()->fromJsonObject(actionsJsonObject); + } else if (featureActionsKey == "StarTrackerActions") { featureActions->setStarTrackerActions(new SWGSDRangel::SWGStarTrackerActions()); @@ -5376,7 +5386,7 @@ bool WebAPIRequestMapper::getFeatureActions( featureActions->setVorLocalizerActions(new SWGSDRangel::SWGVORLocalizerActions()); featureActions->getVorLocalizerActions()->fromJsonObject(actionsJsonObject); } - else if (featureActionsKey == "DemodAnalyzerActions") + else if (featureActionsKey == "DemodAnalyzerActions") { featureActions->setDemodAnalyzerActions(new SWGSDRangel::SWGDemodAnalyzerActions()); featureActions->getDemodAnalyzerActions()->fromJsonObject(actionsJsonObject); @@ -5623,6 +5633,7 @@ void WebAPIRequestMapper::resetFeatureSettings(SWGSDRangel::SWGFeatureSettings& featureSettings.setPerTesterSettings(nullptr); featureSettings.setSatelliteTrackerSettings(nullptr); featureSettings.setSimplePttSettings(nullptr); + featureSettings.setSkyMapSettings(nullptr); featureSettings.setStarTrackerSettings(nullptr); featureSettings.setRadiosondeSettings(nullptr); featureSettings.setRigCtlServerSettings(nullptr); @@ -5639,6 +5650,7 @@ void WebAPIRequestMapper::resetFeatureReport(SWGSDRangel::SWGFeatureReport& feat featureReport.setMapReport(nullptr); featureReport.setSatelliteTrackerReport(nullptr); featureReport.setSimplePttReport(nullptr); + featureReport.setSkyMapReport(nullptr); featureReport.setStarTrackerReport(nullptr); featureReport.setVorLocalizerReport(nullptr); } @@ -5654,6 +5666,7 @@ void WebAPIRequestMapper::resetFeatureActions(SWGSDRangel::SWGFeatureActions& fe featureActions.setRigCtlServerActions(nullptr); featureActions.setSatelliteTrackerActions(nullptr); featureActions.setSimplePttActions(nullptr); + featureActions.setSkyMapActions(nullptr); featureActions.setStarTrackerActions(nullptr); featureActions.setVorLocalizerActions(nullptr); } diff --git a/sdrbase/webapi/webapiutils.cpp b/sdrbase/webapi/webapiutils.cpp index 857de129c..d0a524330 100644 --- a/sdrbase/webapi/webapiutils.cpp +++ b/sdrbase/webapi/webapiutils.cpp @@ -320,6 +320,7 @@ const QMap WebAPIUtils::m_featureTypeToSettingsKey = { {"RigCtlServer", "RigCtlServerSettings"}, {"SatelliteTracker", "SatelliteTrackerSettings"}, {"SimplePTT", "SimplePTTSettings"}, + {"SkyMap", "SkyMapSettings"}, {"StarTracker", "StarTrackerSettings"}, {"VORLocalizer", "VORLocalizerSettings"} }; @@ -334,6 +335,7 @@ const QMap WebAPIUtils::m_featureTypeToActionsKey = { {"RigCtlServer", "RigCtlServerActions"}, {"SatelliteTracker", "SatelliteTrackerActions"}, {"SimplePTT", "SimplePTTActions"}, + {"SkyMap", "SkyMapActions"}, {"StarTracker", "StarTrackerActions"}, {"VORLocalizer", "VORLocalizerActions"}, {"DemodAnalyzer", "DemodAnalyzerActions"} @@ -355,6 +357,7 @@ const QMap WebAPIUtils::m_featureURIToSettingsKey = { {"sdrangel.feature.rigctlserver", "RigCtlServerSettings"}, {"sdrangel.feature.satellitetracker", "SatelliteTrackerSettings"}, {"sdrangel.feature.simpleptt", "SimplePTTSettings"}, + {"sdrangel.feature.skymap", "SkyMapSettings"}, {"sdrangel.feature.startracker", "StarTrackerSettings"}, {"sdrangel.feature.vorlocalizer", "VORLocalizerSettings"} }; diff --git a/sdrgui/feature/featuregui.cpp b/sdrgui/feature/featuregui.cpp index 4bb61b9a8..123a26edb 100644 --- a/sdrgui/feature/featuregui.cpp +++ b/sdrgui/feature/featuregui.cpp @@ -417,3 +417,8 @@ void FeatureGUI::setDisplayedame(const QString& name) m_displayedName = name; m_indexLabel->setToolTip(tr("%1").arg(m_displayedName)); } + +void FeatureGUI::setStatusText(const QString& text) +{ + m_statusLabel->setText(text); +} diff --git a/sdrgui/feature/featuregui.h b/sdrgui/feature/featuregui.h index 1fc507d63..ebc72f8ae 100644 --- a/sdrgui/feature/featuregui.h +++ b/sdrgui/feature/featuregui.h @@ -68,6 +68,7 @@ public: void setIndex(int index); int getIndex() const { return m_featureIndex; } void setDisplayedame(const QString& name); + void setStatusText(const QString& text); protected: void closeEvent(QCloseEvent *event) override;