diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2fecf4985..f3efa9c4b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -518,6 +518,10 @@ elseif(ANDROID)
set(SGP4_INCLUDE_DIR "${EXTERNAL_LIBRARY_FOLDER}/${ANDROID_ABI}/sgp4/include/libsgp4" CACHE INTERNAL "")
set(SGP4_LIBRARIES "${EXTERNAL_LIBRARY_FOLDER}/${ANDROID_ABI}/sgp4/lib/libsgp4.a" CACHE INTERNAL "")
+ set(CSPICE_FOUND ON CACHE INTERNAL "")
+ set(CSPICE_INCLUDE_DIR "${EXTERNAL_LIBRARY_FOLDER}/${ANDROID_ABI}/cspice/include" CACHE INTERNAL "")
+ set(CSPICE_LIBRARIES "${EXTERNAL_LIBRARY_FOLDER}/${ANDROID_ABI}/cspice/lib/libcspice.a" CACHE INTERNAL "")
+
set(ZLIB_FOUND ON CACHE INTERNAL "")
set(ZLIB_INCLUDE_DIR "${EXTERNAL_LIBRARY_FOLDER}/${ANDROID_ABI}/zlib/include" CACHE INTERNAL "")
set(ZLIB_LIBRARIES "${EXTERNAL_LIBRARY_FOLDER}/${ANDROID_ABI}/zlib/lib/libz.a" CACHE INTERNAL "")
@@ -890,6 +894,7 @@ if (NOT ENABLE_EXTERNAL_LIBRARIES OR (ENABLE_EXTERNAL_LIBRARIES STREQUAL "AUTO")
find_package(SerialDV)
find_package(LibDSDcc)
find_package(Sgp4)
+ find_package(CSPICE)
find_package(AptDec)
find_package(LibDAB)
find_package(HIDAPI)
diff --git a/CMakePresets.json b/CMakePresets.json
index 84d8b7409..70fc2c96b 100644
--- a/CMakePresets.json
+++ b/CMakePresets.json
@@ -13,6 +13,7 @@
"BLADERF_DIR": "/opt/install/libbladeRF",
"CM256CC_DIR": "/opt/install/cm256cc",
"CODEC2_DIR": "/opt/install/codec2",
+ "CSPICE_DIR": "/opt/install/cspice",
"DAB_DIR": "/opt/install/libdab",
"DSDCC_DIR": "/opt/install/dsdcc",
"HACKRF_DIR": "/opt/install/libhackrf",
diff --git a/cmake/Modules/FindCSPICE.cmake b/cmake/Modules/FindCSPICE.cmake
new file mode 100644
index 000000000..d3c5cfc41
--- /dev/null
+++ b/cmake/Modules/FindCSPICE.cmake
@@ -0,0 +1,26 @@
+IF(NOT CSPICE_FOUND)
+ FIND_PATH(
+ CSPICE_INCLUDE_DIR
+ NAMES SpiceUsr.h
+ HINTS ${CSPICE_DIR}/include
+ PATHS /usr/local/include
+ /usr/include
+ )
+
+ FIND_LIBRARY(
+ CSPICE_LIBRARIES
+ NAMES cspice
+ HINTS ${CSPICE_DIR}/lib
+ PATHS /usr/local/lib
+ /usr/lib
+ /usr/lib64
+ )
+
+ message(STATUS "CSPICE LIBRARIES " ${CSPICE_LIBRARIES})
+ message(STATUS "CSPICE INCLUDE DIRS " ${CSPICE_INCLUDE_DIR})
+
+ INCLUDE(FindPackageHandleStandardArgs)
+ FIND_PACKAGE_HANDLE_STANDARD_ARGS(CSPICE DEFAULT_MSG CSPICE_LIBRARIES CSPICE_INCLUDE_DIR)
+ MARK_AS_ADVANCED(CSPICE_LIBRARIES CSPICE_INCLUDE_DIR)
+
+ENDIF(NOT CSPICE_FOUND)
diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt
index 1b265ff45..450522029 100644
--- a/external/CMakeLists.txt
+++ b/external/CMakeLists.txt
@@ -858,6 +858,37 @@ if(ENABLE_FEATURE_SATELLITETRACKER OR ENABLE_CHANNELRX_DEMODAPT)
endif ()
endif ()
+if(ENABLE_FEATURE_STARTRACKER)
+ if (WIN32)
+ set(CSPICE_LIBRARIES "${SDRANGEL_BINARY_LIB_DIR}/cspice.lib" CACHE INTERNAL "")
+ elseif (LINUX)
+ set(CSPICE_LIBRARIES "${EXTERNAL_BUILD_LIBRARIES}/lib${LIB_SUFFIX}/libcspice.a" CACHE INTERNAL "")
+ elseif (APPLE)
+ set(CSPICE_LIBRARIES "${EXTERNAL_BUILD_LIBRARIES}/cspice/src/cspice-build/libcspice/libcspice.a" CACHE INTERNAL "")
+ endif()
+ ExternalProject_Add(cspice
+ GIT_REPOSITORY https://github.com/srcejon/cspice-cmake
+ GIT_TAG "msvc"
+ PREFIX "${EXTERNAL_BUILD_LIBRARIES}/cspice"
+ CMAKE_ARGS ${COMMON_CMAKE_ARGS}
+ BUILD_BYPRODUCTS "${CSPICE_LIBRARIES}"
+ INSTALL_COMMAND ""
+ TEST_COMMAND ""
+ )
+ ExternalProject_Get_Property(cspice source_dir binary_dir)
+ set(CSPICE_DEPENDS cspice CACHE INTERNAL "")
+ set_global_cache(CSPICE_FOUND ON)
+ set(CSPICE_EXTERNAL ON CACHE INTERNAL "")
+ set(CSPICE_INCLUDE_DIR "${EXTERNAL_BUILD_LIBRARIES}/cspice/src/cspice/include" CACHE INTERNAL "")
+ if (WIN32)
+ install(FILES "${SDRANGEL_BINARY_BIN_DIR}/cspice${CMAKE_SHARED_LIBRARY_SUFFIX}" DESTINATION "${INSTALL_LIB_DIR}")
+ elseif (APPLE)
+ install(DIRECTORY "${binary_dir}/libcspice" DESTINATION "${INSTALL_LIB_DIR}"
+ FILES_MATCHING PATTERN "libcspice*${CMAKE_SHARED_LIBRARY_SUFFIX}")
+ set(MACOS_EXTERNAL_LIBS_FIXUP "${MACOS_EXTERNAL_LIBS_FIXUP};${binary_dir}/libcspice")
+ endif ()
+endif ()
+
if(ENABLE_CHANNELRX_REMOTETCPSINK)
if (WIN32)
set(FLAC_LIBRARIES "${SDRANGEL_BINARY_LIB_DIR}/FLAC.lib" CACHE INTERNAL "")
diff --git a/flatpak/org.sdrangel.SDRangel.yaml b/flatpak/org.sdrangel.SDRangel.yaml
index 0308de317..c8d87cbbe 100644
--- a/flatpak/org.sdrangel.SDRangel.yaml
+++ b/flatpak/org.sdrangel.SDRangel.yaml
@@ -297,6 +297,14 @@ modules:
# branch: new-namespaces
commit: 299dc8f9725f1733e5cc1ce8a69fbcf7f18a2f58
+ - name: cspice
+ buildsystem: cmake-ninja
+ sources:
+ - type: git
+ url: https://github.com/srcejon/cspice-cmake
+ # branch: msvc
+ commit: 817571e78709fd8c25d70a2fd1bacaee0829101e
+
- name: sgp4
buildsystem: cmake-ninja
sources:
diff --git a/plugins/feature/CMakeLists.txt b/plugins/feature/CMakeLists.txt
index beab035a8..0767ea3e9 100644
--- a/plugins/feature/CMakeLists.txt
+++ b/plugins/feature/CMakeLists.txt
@@ -97,10 +97,10 @@ else()
message(STATUS "Not building radiosonde (ENABLE_FEATURE_RADIOSONDE=${ENABLE_FEATURE_RADIOSONDE} Qt${QT_DEFAULT_MAJOR_VERSION}Charts_FOUND=${Qt${QT_DEFAULT_MAJOR_VERSION}Charts_FOUND})")
endif()
-if (ENABLE_FEATURE_STARTRACKER AND Qt${QT_DEFAULT_MAJOR_VERSION}Charts_FOUND)
+if (ENABLE_FEATURE_STARTRACKER AND CSPICE_FOUND AND Qt${QT_DEFAULT_MAJOR_VERSION}Charts_FOUND)
add_subdirectory(startracker)
else()
- message(STATUS "Not building startracker (ENABLE_FEATURE_STARTRACKER=${ENABLE_FEATURE_STARTRACKER} Qt${QT_DEFAULT_MAJOR_VERSION}Charts_FOUND=${Qt${QT_DEFAULT_MAJOR_VERSION}Charts_FOUND})")
+ message(STATUS "Not building startracker (ENABLE_FEATURE_STARTRACKER=${ENABLE_FEATURE_STARTRACKER} CSPICE_FOUND=${CSPICE_FOUND} Qt${QT_DEFAULT_MAJOR_VERSION}Charts_FOUND=${Qt${QT_DEFAULT_MAJOR_VERSION}Charts_FOUND})")
endif()
if (ENABLE_FEATURE_LIMERFE AND ENABLE_LIMESUITE AND LIMESUITE_FOUND)
diff --git a/plugins/feature/startracker/CMakeLists.txt b/plugins/feature/startracker/CMakeLists.txt
index 6b9b0a2f2..58ed672e6 100644
--- a/plugins/feature/startracker/CMakeLists.txt
+++ b/plugins/feature/startracker/CMakeLists.txt
@@ -9,6 +9,7 @@ set(startracker_SOURCES
startracker150mhzfits.qrc
startracker408mhzfits.qrc
startracker1420mhzfits.qrc
+ spice.cpp
)
set(startracker_HEADERS
@@ -18,10 +19,12 @@ set(startracker_HEADERS
startrackerreport.h
startrackerworker.h
startrackerwebapiadapter.h
+ spice.h
)
include_directories(
${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client
+ ${CSPICE_INCLUDE_DIR}
)
if(NOT SERVER_MODE)
@@ -36,11 +39,15 @@ if(NOT SERVER_MODE)
startracker408mhz.qrc
startracker1420mhz.qrc
startrackermilkyway.qrc
+ startrackerplanets.qrc
+ startrackerjupiter.qrc
+ spiceephemerides.cpp
)
set(startracker_HEADERS
${startracker_HEADERS}
startrackergui.h
startrackersettingsdialog.h
+ spiceephemerides.h
)
set(TARGET_NAME ${PLUGINS_PREFIX}featurestartracker)
@@ -64,11 +71,16 @@ if(NOT BUILD_SHARED_LIBS)
set_property(GLOBAL APPEND PROPERTY STATIC_PLUGINS_PROPERTY ${TARGET_NAME})
endif()
+if(CSPICE_EXTERNAL)
+ add_dependencies(${TARGET_NAME} cspice)
+endif()
+
target_link_libraries(${TARGET_NAME} PRIVATE
Qt::Core
${TARGET_LIB}
sdrbase
${TARGET_LIB_GUI}
+ ${CSPICE_LIBRARIES}
)
install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER})
diff --git a/plugins/feature/startracker/readme.md b/plugins/feature/startracker/readme.md
index d2cbf6311..d975f6727 100644
--- a/plugins/feature/startracker/readme.md
+++ b/plugins/feature/startracker/readme.md
@@ -6,11 +6,15 @@ The Star Tracker feature plugin is for use in radio astronomy and EME (Earth-Moo
* It calculates the azimuth and elevation of celestial objects and can send them to the Rotator Controller or other plugins to point an antenna at that object.
* It can plot drift scan paths in both equatorial and galactic charts.
-* The overhead position of the Sun, Moon and selected star can be displayed on the Map Feature.
+* The overhead position of the Sun, Moon and target object 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.
+* It has built-in models for calculating the position of the Sun and Moon, as well built-in coordiates for a few of the most significant radio objects,
+but can also use NASA JPL's SPICE toolkit or Horizons API for targetting other solar system objects, such as asteroids, comets, planets, planetary satellites and some spacecraft.
+* For Jupiter decameter radiation (DAM) observations, it can calculate and plot the phase of Io and Ganymede, relative to Jupiter's Central Meridian Longitude (CML) on a chart showing emission probability.
+* It can plot the positions of major solar system bodies in the Solar System Map.
Settings
@@ -36,26 +40,33 @@ When clicked, it sets the latitude, longitude and height fields to the values fr
Pressing this button displays a settings dialog, that allows you to set:
-* The epoch used when entering RA and Dec. This can be either J2000 (which is used for most catalogues) or JNOW which is the current date and time.
-* The units used for the display of the calculated azimuth and elevation. This can be either degrees, minutes and seconds or decimal degrees.
-* Whether to correct for atmospheric refraction. You can choose either no correction, the Saemundsson algorithm, typically used for optical astronomy or the more accurate Positional Astronomy Library calculation, which can be used for >250MHz radio frequencies or light. Note that there is only a very minor difference between the two.
-* API key for openweathermap.org which is used to download real-time weather (Air temperature, pressure and humidity) for the specified latitude (6) and longitude (7).
-* How often to download weather (in minutes).
-* Air pressure in millibars. This value can be automatically updated from OpenWeatherMap.
-* Air temperature in degrees Celsius. This value can be automatically updated from OpenWeatherMap.
-* Relative humidity in %. This value can be automatically updated from OpenWeatherMap.
-* Height above sea level in metres of the observation point (antenna location).
-* Temperature lapse rate in Kelvin per kilometre.
-* What data to display for the Solar flux measurement. Data can be selected from 2800 from DRAO or a number of different frequencies from Learmonth. Also, the Learmonth data can be linearly interpolated to the observation frequency set in the main window.
-* The units to display the solar flux in, either Solar Flux Units, Jansky or Wm^-2Hz-1. 1 sfu equals 10,000 Jansky or 10^-22 Wm^-2Hz-1.
-* The update period in seconds, which controls how frequently azimuth and elevation are re-calculated.
-* The IP port number the Stellarium server listens on.
-* Which rotators are displayed on the polar chart. This can be All, None or Matching target. When Matching target is selected, the rotator will
+* Settings Tab:
+
+ * The epoch used when entering RA and Dec. This can be either J2000 (which is used for most catalogues) or JNOW which is the current date and time.
+ * The units used for the display of the calculated azimuth and elevation. This can be either degrees, minutes and seconds or decimal degrees.
+ * Whether to correct for atmospheric refraction. You can choose either no correction, the Saemundsson algorithm, typically used for optical astronomy or the more accurate Positional Astronomy Library calculation, which can be used for >250MHz radio frequencies or light. Note that there is only a very minor difference between the two.
+ * API key for openweathermap.org which is used to download real-time weather (Air temperature, pressure and humidity) for the specified latitude (6) and longitude (7).
+ * How often to download weather (in minutes).
+ * Air pressure in millibars. This value can be automatically updated from OpenWeatherMap.
+ * Air temperature in degrees Celsius. This value can be automatically updated from OpenWeatherMap.
+ * Relative humidity in %. This value can be automatically updated from OpenWeatherMap.
+ * Height above sea level in metres of the observation point (antenna location).
+ * Temperature lapse rate in Kelvin per kilometre.
+ * What data to display for the Solar flux measurement. Data can be selected from 2800 from DRAO or a number of different frequencies from Learmonth. Also, the Learmonth data can be linearly interpolated to the observation frequency set in the main window.
+ * The units to display the solar flux in, either Solar Flux Units, Jansky or Wm^-2Hz-1. 1 sfu equals 10,000 Jansky or 10^-22 Wm^-2Hz-1.
+ * The update period in seconds, which controls how frequently azimuth and elevation are re-calculated.
+ * The IP port number the Stellarium server listens on.
+ * Which rotators are displayed on the polar chart. This can be All, None or Matching target. When Matching target is selected, the rotator will
only be displayed if the source in the Rotator Controller is set to this Star Tracker and Track is enabled.
-* Whether to start a Stellarium telescope server.
-* Whether to draw the Sun in the map.
-* Whether to draw the Moon on the map.
-* Whether to draw the target star (or galaxy) on the map.
+ * Whether to start a Stellarium telescope server.
+ * Whether to draw the Sun in the map.
+ * Whether to draw the Moon on the map.
+* * Whether to draw the target object on the map.
+
+* Ephemerides Tab:
+
+ * [SPICE](https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/) ephermerides files, containing target data.
+ * Solar System bodies, from the above ephermerides files, to draw on the Solar System map.
6: Latitude
@@ -67,7 +78,9 @@ Specifies the longitude in decimal degrees (East positive) of the observation po
8: 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.
+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.
+
+By default the date and time is the local. Check the UTC button is use UTC instead.
9: LST - Local Sidereal Time
@@ -79,9 +92,14 @@ Displays the Solar flux density. The observatory where the data is sourced from,
11: Target
-Select a target object to track from the list.
-To manually enter RA (right ascension) and Dec (declination) of an unlisted target, select Custom RA/Dec.
-To allow Stellarium to set the RA and Dec, select Custom RA/Dec, and ensure the Stellarium Server option is checked in the Star Tracker Settings dialog.
+Select a target object to track from the list.
+- To manually enter RA (right ascension) and Dec (declination) of an unlisted target, select Custom RA/Dec.
+- To allow Stellarium to set the RA and Dec, select Custom RA/Dec, and ensure the Stellarium Server option is checked in the Star Tracker Settings dialog.
+- To select a target from NASA JPL Horizons database of solar system objects (asteroids, comets, planets, planetary satellites and some spacecraft), select Horizons in the target source (12).
+Major bodies will be added to the target list. Other bodies can be targetting by typing in their numeric ID.
+- To select a target from a SPICE SPK file, select SPICE in the target source (12).
+
+SDRangel built-in targets:
| Target | Type | Details | Flux density (Jy) or Temperature (K) |
|------------------|-------------------|------------------------------------------------|---------------------------------------------
@@ -110,53 +128,68 @@ References:
* Repeating Jansky - https://www.gb.nrao.edu/~fghigo/JanskyAntenna/RepeatingJansky_memo10.pdf
* Studies of four regions for use as standards in 21CM observations - http://articles.adsabs.harvard.edu/pdf/1973A%26AS....8..505W
-12: Frequency
+12: Target Source
+
+Selects the source of targets listed in the Target list (11).
+
+- SDRangel: built-in targets as listed in (11).
+- SPICE: Solar System bodies from a SPICE SPK file.
+- Horizons: Solar System bodies from NASA JPL's Horizons database.
+
+SPICE SPK files are available from [NASA JPL's NAIF website](https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/).
+SPK files can be specified in the Star Tracker Settings dialog (5).
+
+When Horizons is selected, the Target list (11) will be populated with major bodies from the Horizons database. Other bodies can be specified by entering their ID manually.
+The Horizons database includes asteroids, comets, natural satellites, planets, the Sun, some spacecraft and dynamical points (L1 etc) and barycenters.
+[Horizons Manual](https://ssd.jpl.nasa.gov/horizons/manual.html). Using the Horizons API requires an internet connection.
+
+13: Frequency
Enter the frequency of observation in MHz. This value is used for sky temperature and refraction calculations.
-13: Beamwidth
+14: Beamwidth
Enter the half power (-3dB) beamwidth of your antenna. This value is used for sky temperature calculation.
-14: Right Ascension
+15: Right Ascension
When target is set to Custom RA/Dec, you can specify the right ascension in hours of the target object. This can be specified as a decimal (E.g. 12.23, from 0 to 24) or in hours, minutes and seconds (E.g. 12h05m10.2s or 12 05 10.2). Whether the epoch is J2000 or JNOW can be set in the Star Tracker Settings dialog.
When target is set to Custom Az/El or Custom l/b, this will display the corresponding right ascension.
-15: Declination
+16: Declination
When target is set to Custom RA/Dec, you can specify the declination in degrees of the target object. This can be specified as a decimal (E.g. 34.6, from -90.0 to 90.0) or in degrees, minutes and seconds (E.g. 34d12m5.6s, 34d12'5.6" 34 12 5.6). Whether the epoch is J2000 or JNOW can be set in the Star Tracker Settings dialog.
When target is set to Custom Az/El or Custom l/b, this will display the corresponding declination.
-16: Azimuth
+17: Azimuth
When target is set to Custom Az/El, you specify the azimuth in degrees of the target object. The corresponding RA/Dec and l/b will be calculated and displayed.
For all other target settings, this displays the calculated azimuth (angle in degrees, clockwise from North) to the object.
-17: Elevation
+18: Elevation
When target is set to Custom Az/El, you specify the elevation in degrees of the target object. The corresponding RA/Dec and l/b will be calculated and displayed.
For all other target settings, this displays the calculated elevation (angle in degrees - 0 to horizon and 90 to zenith) to the object.
-18: Az Offset
+19: Az Offset
An offset in degrees that is added to the computed target azimuth.
-19: El Offset
+20: El Offset
An offset in degrees that is added to the computed target elevation.
-20: l - Galactic Longitude
+21: l - Galactic Longitude
When the target is set to Custom l/b, you specify the galactic longitude (angle in degrees, Eastward from the galactic centre) of the target object.
For all other target settings, this displays the calculated galactic longitude to the object.
-21: b - Galactic Latitude
+22: b - Galactic Latitude
When the target is set to Custom l/b, you specify the galactic latitude (angle in degrees) of the target object.
@@ -164,15 +197,12 @@ For all other target settings, displays the calculated galactic latitude to the
Plots
-Light or dark theme
-
-Click on this icon  to switch between light and dark themes for the charts.
-
Elevation vs time
 
In order to assist in determining whether and when observations of the target object may be possible, an elevation vs time plot is drawn for the 24 hours encompassing the selected date and time.
+The day/night button can be used to select midnight to midnight (unchecked/day) or noon to noon (checked/night).
This can be plotted on Cartesian or polar axis.
Some objects may not be visible from a particular latitude for the specified time, in which case, the graph title will indicate the object is not visible on that particular date.
@@ -234,6 +264,33 @@ To start a new animation, press 
+Solar System Map
+
+The Solar System Map shows the positions of the bodies selected in the Settings Dialog (5). Positions can be plotted on either a linear or logarithmic scale. The positions are displayed top down on the equatorial plane.
+The map will be centered at the body selected in the combo box. Select '-' to be able to pan the map with the mouse.
+
+
+
+Jupiter CML and Moon Phase
+
+The Jupiter CML and Moon Phase plot is for assisting with Jupiter decameter radiation (DAM) observations. It displays the phase of either Io or Ganymede against Jupiter's Central Meridian Longitude (CML) overlaid
+on a chart showing emission probabilty.
+
+
+
+
+
+The moon is plotted at the selected date and time (8). The white line shows the path and time of the current or next visible pass of the moon.
+
+Underneath the chart, the elevation of Jupiter in degrees, the Central Meridan Longitude in degrees and moon phase in degrees, are shown for the selected date and time (8).
+
+The Central Meridan Longitude is the System III longitude of Jupiter that is currently facing Earth, taking in to account light travel time.
+A moon phase of 0 degrees is at the far side of Jupiter, while 180 degrees has the moon directly between Jupiter and the Earth.
+
+Light or dark theme
+
+Click on this icon  to switch between light and dark themes for the charts.
+
Map
The Star Tracker feature can send the overhead position of the Sun, Moon and target Star to the Map. These can be enabled individually in the settings dialog. The Moon should be displayed with an approximate phase. Stars (or galaxies) are displayed as an image of a pulsar.
@@ -292,6 +349,25 @@ Icons are by Adnen Kadri, iconsphere and Erik Madsen, from the Noun Project Noun
Icons are by Freepik from Flaticon https://www.flaticon.com/
+Io & Ganymede Phase vs CML plots from Jupiter radio emission induced by Ganymede and consequences for the radio detection of exoplanets, Zarka et all, 2018: https://www.aanda.org/articles/aa/full_html/2018/10/aa33586-18/aa33586-18.html
+
+Sun image from Solar Dynamics Observatory, NASA
+Mercury image from Messenger, NASA/Johns Hopkins University Applied Physics Laboratory/Carnegie Institution of Washington.
+Venus image by NASA/JPL-Caltech
+Blue Marble by crew of Apollo 17.
+Moon image by NASA.
+Mars image Viking Orbiter, NASA/JPL-Caltech
+Phobos image by NASA/JPL-Caltech/University of Arizona
+Deimos image by NASA/JPL-Caltech/University of Arizona
+Jupiter enhanced image by Kevin M. Gill (CC-BY) based on images provided courtesy of NASA/JPL-Caltech/SwRI/MSSS
+Io from Galileo by NASA/JPL/USGS
+Ganymede from Juno by NASA/JPL-Caltech/SwRI/MSSS/Kevin M. Gill
+Calisto NASA/JPL/DLR
+Saturn from Cassini, NASA/ESA
+Uranus from Voyager 2, NASA/JPL-Caltech
+Neptune from Voyager 2, NASA/JPL-Caltech
+Pluto image from New Horizons, NASA/JHUAPL/SwRI
+
API
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:
diff --git a/plugins/feature/startracker/spice.cpp b/plugins/feature/startracker/spice.cpp
new file mode 100644
index 000000000..d9de24526
--- /dev/null
+++ b/plugins/feature/startracker/spice.cpp
@@ -0,0 +1,433 @@
+///////////////////////////////////////////////////////////////////////////////////
+// Copyright (C) 2026 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 "spice.h"
+#include "util/units.h"
+#include "util/profiler.h"
+
+#include
+
+#include
+
+#include
+
+static QMutex spiceMutex; // SPICE isn't thread-safe, so we use a global mutex that should be locked via spiceLock() before calling any function in this file.
+static QStringList spiceEphemeridesLoaded;
+
+void spiceInit()
+{
+ QMutexLocker locker(&spiceMutex);
+
+ // Don't abort on error from SPICE
+ erract_c("SET", 0, (char *) "RETURN");
+}
+
+bool spiceLock(QStringList ephemerides)
+{
+ spiceMutex.lock();
+
+ QStringList files = ephemerides;
+ bool changed = false;
+
+ // Unload no longer used ephemerides
+ QMutableListIterator itr(spiceEphemeridesLoaded);
+ while (itr.hasNext())
+ {
+ QString file = itr.next();
+ if (!files.contains(file))
+ {
+ unload_c(file.toStdString().c_str());
+ itr.remove();
+ changed = true;
+ }
+ }
+
+ // Load new ephemerides - file may not have been downloaded yet
+ for (const auto& file : files)
+ {
+ if (!spiceEphemeridesLoaded.contains(file) && QFileInfo::exists(file))
+ {
+ furnsh_c(file.toStdString().c_str());
+ if (!failed_c())
+ {
+ spiceEphemeridesLoaded.append(file);
+ changed = true;
+ }
+ else
+ {
+ qWarning() << "StarTracker: Failed to load SPICE ephemeris file: " << file;
+ reset_c();
+ }
+ }
+ }
+
+ return changed;
+}
+
+void spiceUnlock()
+{
+ spiceMutex.unlock();
+}
+
+// Normalize angle to [0,360)
+static SpiceDouble normalize360(SpiceDouble deg)
+{
+ SpiceDouble d = fmod(deg, 360.0);
+ if (d < 0.0) {
+ d += 360.0;
+ }
+ return d;
+}
+
+// Normalize angle to (-180, +180]
+static SpiceDouble normalize180(SpiceDouble deg)
+{
+ SpiceDouble d = fmod(deg + 180.0, 360.0);
+ if (d < 0.0) {
+ d += 360.0;
+ }
+ return d - 180.0;
+}
+
+// Compute topocentric observer position in J2000 at ET
+static bool computeTopocenterJ2000(SpiceDouble latDeg, SpiceDouble lonDeg, SpiceDouble altKm, SpiceDouble et, SpiceDouble obsJ2000[3])
+{
+ SpiceInt n;
+ SpiceDouble radii[3];
+
+ // Earth body radii for geodetic -> rectangular
+ bodvrd_c("EARTH", "RADII", 3, &n, radii);
+ if (n < 3)
+ {
+ reset_c();
+ return false;
+ }
+ SpiceDouble re = radii[0];
+ SpiceDouble rp = radii[2];
+ SpiceDouble flatten = (re - rp) / re;
+
+ // Geodetic to IAU_EARTH body-fixed rectangular coords
+ SpiceDouble lonRad = Units::degreesToRadians(lonDeg);
+ SpiceDouble latRad = Units::degreesToRadians(latDeg);
+ SpiceDouble topoIAE[3];
+ georec_c(lonRad, latRad, altKm, re, flatten, topoIAE);
+
+ // Transform IAU_EARTH -> J2000 at ET
+ SpiceDouble xformIAEToJ2000[3][3];
+ pxform_c("IAU_EARTH", "J2000", et, xformIAEToJ2000);
+ SpiceDouble topoJ2000[3];
+ mxv_c(xformIAEToJ2000, topoIAE, topoJ2000);
+
+ /// Earth center in J2000 w.r.t. SSB
+ SpiceDouble earthPosJ2000[3], ltDummy;
+ spkpos_c("EARTH", et, "J2000", "NONE", "SOLAR SYSTEM BARYCENTER", earthPosJ2000, <Dummy);
+ if (failed_c())
+ {
+ reset_c();
+ return false;
+ }
+
+ // observer = earth_center + topo offset
+ vadd_c(earthPosJ2000, topoJ2000, obsJ2000);
+
+ return true;
+}
+
+// Convert QT date time to SPICE time (TBD seconds past J2000 epoch)
+bool dateTimeToET(const QDateTime& dateTime, double &et)
+{
+ const QByteArray utcBA = dateTime.toUTC().toString(Qt::ISODateWithMs).toLatin1();
+ const char *utc = utcBA.data();
+
+ str2et_c(utc, &et);
+ if (failed_c())
+ {
+ reset_c();
+ return false;
+ }
+ return true;
+}
+
+// Get list of targets (bodies) from a SPICE SPK file. Non SPK files will be ignored.
+QStringList getSPICETargets(const QString& file)
+{
+ QStringList targets;
+
+ const QByteArray fileBA = file.toLatin1();
+ const char *fileStr = fileBA.constData();
+
+ // Check file is SPK
+ SpiceChar arch[32];
+ SpiceChar kernelType[32];
+
+ getfat_c(fileStr, sizeof(arch), sizeof(kernelType), arch, kernelType);
+
+ if (strcmp(arch, "DAF") || strcmp(kernelType, "SPK")) {
+ return targets;
+ }
+
+ SPICEINT_CELL(ids, 1000);
+
+ spkobj_c(fileStr, &ids);
+ if (!failed_c())
+ {
+ for (int i = 0; i < card_c(&ids); i++)
+ {
+ SpiceBoolean found;
+ SpiceInt id;
+ char bodyName[33];
+
+ id = SPICE_CELL_ELEM_I(&ids, i);
+ bodc2n_c(id, sizeof(bodyName), bodyName, &found);
+ if (!failed_c())
+ {
+ QString nameStr = QString::fromLatin1(bodyName);
+ if (!targets.contains(nameStr)) {
+ targets.append(nameStr);
+ }
+ }
+ else
+ {
+ reset_c();
+ }
+ }
+ }
+ else
+ {
+ reset_c();
+ }
+
+ return targets;
+}
+
+// Get position of named body relative to Solar System Barycentre. No abberation correction
+bool getSSBPositionFromSPICE(const QString& name, double et, QVector3D &positionKm)
+{
+ SpiceDouble posJ2000[3];
+ SpiceDouble lightTime;
+ const QByteArray nameBA = name.toLatin1();
+ const char *nameStr = nameBA.constData();
+
+ // Get position of named body, relative to SSB.
+ spkpos_c(nameStr, et, "J2000", "NONE", "SOLAR SYSTEM BARYCENTER", posJ2000, &lightTime);
+ if (!failed_c())
+ {
+ positionKm.setX(posJ2000[0]);
+ positionKm.setY(posJ2000[1]);
+ positionKm.setZ(posJ2000[2]);
+
+ return true;
+ }
+ else
+ {
+ qDebug() << "StarTrackerWorker::calculateSolarSystemPositions: Failed to get position for" << name;
+ reset_c();
+ return false;
+ }
+}
+
+// Calculate azimuth and elevation of a target body from a position on Earth using SPICE
+bool getAzElFromSPICE(const QString& target, double et, double latitude, double longitude, double altitudeKm, double &azimuth, double &elevation)
+{
+ QByteArray targetBA = target.toLatin1();
+ const char *targetStr = targetBA.constData();
+
+ // Get body radii for the observer's central body
+ SpiceInt n;
+ SpiceDouble radii[3];
+ bodvrd_c("EARTH", "RADII", 3, &n, radii);
+
+ // Convert geodetic coordinates to body-fixed rectangular coordinates
+ SpiceDouble bodfix[3];
+ SpiceDouble longitudeRad = Units::degreesToRadians(longitude);
+ SpiceDouble latitudeRad = Units::degreesToRadians(latitude);
+ georec_c(longitudeRad, latitudeRad, altitudeKm, radii[0], (radii[0] - radii[2]) / radii[0], bodfix);
+
+ // Calculate Alz/Az
+ SpiceDouble lt;
+ SpiceDouble azlsta[6];
+ azlcpo_c("ELLIPSOID", targetStr, et, "LT+S", SPICEFALSE, SPICETRUE, bodfix, "EARTH", "IAU_EARTH", azlsta, <);
+ if (!failed_c())
+ {
+ azimuth = Units::radiansToDegrees(azlsta[1]);
+ elevation = Units::radiansToDegrees(azlsta[2]);
+ }
+ else
+ {
+ reset_c();
+ return false;
+ }
+
+ return true;
+}
+
+// Calculate RA and Dec of a target body from Earth using SPICE
+bool getRADecFromSPICE(const QString& target, double et, double &ra, double &dec)
+{
+ QByteArray targetBA = target.toLatin1();
+ const char *targetStr = targetBA.constData();
+
+ // Get state (position & velocity) of a target body, relative to observing body (Earth), corrected for light time and stellar aberation
+ SpiceDouble state[6];
+ SpiceDouble lightTime;
+
+ spkezr_c(targetStr, et, "J2000", "LT+S", "EARTH", state, &lightTime);
+ if (!failed_c())
+ {
+ SpiceDouble radec[3];
+
+ // Convert geocentric rectangular coordinates to RA/DEC (spherical coordinates)
+ recrad_c(state, &radec[0], &radec[1], &radec[2]);
+
+ ra = Units::radiansToDegrees(radec[1]);
+ dec = Units::radiansToDegrees(radec[2]);
+
+ // Normalize RA to 0-360 range
+ if (ra < 0.0) {
+ ra += 360.0;
+ }
+
+ // Convert from degrees to hours
+ ra = ra / (360.0 / 24.0);
+ }
+ else
+ {
+ reset_c();
+ return false;
+ }
+
+ return true;
+}
+
+// Calculate Jupiter's CML (Central Meridian Longitude) as seen from a location on Earth and the phase of the given Jovian moon (IO or GANYMEDE)
+bool calculateJupiterMoonPhase(const QString& moon, double et, double latitude, double longitude, double altitudeMetres, double &cml, double &phase)
+{
+ const QByteArray moonBA = moon.toUpper().toLatin1();
+ const char *moonName = moonBA.data();
+
+ SpiceDouble te = et; // Emission epoch
+ SpiceDouble moonPosSSB[3]; // Io position w.r.t SSB at te
+ SpiceDouble obsPosSSB[3]; // Pbserver position w.r.t SSB at reception (et)
+ SpiceDouble lt = 0.0; // Light time
+
+ // Compute topocenter obsPosSSB at reception ET
+ if (!computeTopocenterJ2000(latitude, longitude, altitudeMetres / 1000.0, et, obsPosSSB))
+ {
+ reset_c();
+ return false;
+ }
+
+ // Iterate to find Io emission epoch te
+ SpiceDouble moonPosGuess[3], ltDummy;
+ spkpos_c(moonName, et, "J2000", "NONE", "SOLAR SYSTEM BARYCENTER", moonPosGuess, <Dummy);
+ if (failed_c())
+ {
+ reset_c();
+ return false;
+ }
+
+ const SpiceDouble c = clight_c();
+ SpiceDouble vec[3];
+ vsub_c(obsPosSSB, moonPosGuess, vec);
+ lt = vnorm_c(vec) / c;
+
+ const int MAX_ITERS = 50;
+ const SpiceDouble TOL = 1e-9;
+ int iter;
+ for (iter = 0; iter < MAX_ITERS; ++iter)
+ {
+ te = et - lt;
+ spkpos_c(moonName, te, "J2000", "NONE", "SOLAR SYSTEM BARYCENTER", moonPosSSB, <Dummy);
+ if (failed_c())
+ {
+ reset_c();
+ return false;
+ }
+ vsub_c(obsPosSSB, moonPosSSB, vec);
+ SpiceDouble new_lt = vnorm_c(vec) / c;
+ if (fabs(new_lt - lt) < TOL)
+ {
+ lt = new_lt;
+ break;
+ }
+ lt = new_lt;
+ }
+ if (iter >= MAX_ITERS) {
+ qDebug() << "calculateIoPhase: light-time iteration did not converge";
+ }
+
+ // Moon relative to Jupiter (J2000) at te
+ SpiceDouble moonPosJupiterJ2000[3];
+ spkpos_c(moonName, te, "J2000", "NONE", "JUPITER", moonPosJupiterJ2000, <Dummy);
+ if (failed_c())
+ {
+ reset_c();
+ return false;
+ }
+
+ // Jupiter position (SSB) at te to compute observer-relative vector
+ SpiceDouble jupPosSSBTe[3];
+ spkpos_c("JUPITER", te, "J2000", "NONE", "SOLAR SYSTEM BARYCENTER", jupPosSSBTe, <Dummy);
+ if (failed_c())
+ {
+ reset_c();
+ return false;
+ }
+
+ // Observer vector relative to Jupiter in J2000: obs_ssb (at et) - jup_pos_ssb_te
+ SpiceDouble obsPosJupiterJ2000[3];
+ vsub_c(obsPosSSB, jupPosSSBTe, obsPosJupiterJ2000);
+
+ // Transform both vectors into IAU_JUPITER body-fixed frame evaluated at te
+ SpiceDouble xform[3][3];
+ pxform_c("J2000", "IAU_JUPITER", te, xform);
+
+ SpiceDouble io_bf[3], obs_bf[3];
+ mxv_c(xform, moonPosJupiterJ2000, io_bf);
+ mxv_c(xform, obsPosJupiterJ2000, obs_bf);
+
+ // Jupiter radii
+ SpiceInt nrad;
+ SpiceDouble radiiJupiter[3];
+ bodvrd_c("JUPITER", "RADII", 3, &nrad, radiiJupiter);
+ if (nrad < 3)
+ {
+ reset_c();
+ return false;
+ }
+ SpiceDouble re_j = radiiJupiter[0];
+ SpiceDouble rp_j = radiiJupiter[2];
+ SpiceDouble f_j = (re_j - rp_j) / re_j;
+
+ // Moon planetographic longitude using the same convention as CML
+ SpiceDouble moonLonRad, moonLatRad, moonAltKm;
+ recpgr_c("JUPITER", io_bf, re_j, f_j, &moonLonRad, &moonLatRad, &moonAltKm);
+ SpiceDouble moonLonDeg = normalize360(Units::radiansToDegrees(moonLonRad));
+
+ // CML (sub-observer planetographic longitude)
+ SpiceDouble subObsLonRad, subObsLatRad, subObsAltKm;
+ recpgr_c("JUPITER", obs_bf, re_j, f_j, &subObsLonRad, &subObsLatRad, &subObsAltKm);
+ cml = normalize360(Units::radiansToDegrees(subObsLonRad));
+
+ // Moon offset from CML
+ SpiceDouble delta_deg = normalize180(moonLonDeg - cml);
+
+ // Moon phase (0 deg on far side)
+ phase = normalize360(cml - moonLonDeg + 180);
+
+ return true;
+}
diff --git a/plugins/feature/startracker/spice.h b/plugins/feature/startracker/spice.h
new file mode 100644
index 000000000..80031f890
--- /dev/null
+++ b/plugins/feature/startracker/spice.h
@@ -0,0 +1,36 @@
+///////////////////////////////////////////////////////////////////////////////////
+// Copyright (C) 2026 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 STARTRACKER_SPICE_H
+#define STARTRACKER_SPICE_H
+
+#include
+#include
+#include
+
+void spiceInit();
+bool spiceLock(QStringList ephemerides);
+void spiceUnlock();
+
+bool dateTimeToET(const QDateTime& dateTime, double &et);
+QStringList getSPICETargets(const QString& file);
+bool getSSBPositionFromSPICE(const QString& name, double et, QVector3D &position);
+bool getAzElFromSPICE(const QString& target, double et, double latitude, double longitude, double altitudeKm, double &azimuth, double &elevation);
+bool getRADecFromSPICE(const QString& target, double et, double &ra, double &dec);
+bool calculateJupiterMoonPhase(const QString& moon, double et, double latitude, double longitude, double altitudeMetres, double& cml, double &phase);
+
+#endif // STARTRACKER_SPICE_H
diff --git a/plugins/feature/startracker/spiceephemerides.cpp b/plugins/feature/startracker/spiceephemerides.cpp
new file mode 100644
index 000000000..d76e1bb33
--- /dev/null
+++ b/plugins/feature/startracker/spiceephemerides.cpp
@@ -0,0 +1,139 @@
+///////////////////////////////////////////////////////////////////////////////////
+// Copyright (C) 2026 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 "spiceephemerides.h"
+#include "spice.h"
+
+SpiceEphemerides::SpiceEphemerides(QWidget *parentWidget) :
+ m_parentWidget(parentWidget)
+{
+ connect(&m_dlm, &HttpDownloadManager::downloadComplete, this, &SpiceEphemerides::downloadComplete);
+}
+
+// Returns if download was required
+bool SpiceEphemerides::download(const QStringList &emphemerides)
+{
+ QDir downloadDir = QDir(HttpDownloadManager::downloadDir());
+ QString ephemeridesDirPath = downloadDir.path() + "/ephemerides";
+ QDir ephemeridesDir(ephemeridesDirPath);
+ bool downloadRequired = false;
+
+ if (!ephemeridesDir.exists())
+ {
+ if (!downloadDir.mkdir("ephemerides")) {
+ qWarning() << "Failed to make directory" << (downloadDir.path() + "/ephemerides");
+ }
+ }
+ if (ephemeridesDir.exists())
+ {
+ for (const auto& ephemeris : emphemerides)
+ {
+ if (ephemeris.startsWith("http://") || ephemeris.startsWith("https://"))
+ {
+ QUrl ephemerisURL(ephemeris);
+ QString ephemerisFilename = urlToFilename(ephemeris);
+
+ if (!QFileInfo::exists(ephemerisFilename))
+ {
+ qDebug() << "Downloading ephemeris from" << ephemerisURL << "to" << ephemerisFilename;
+ m_pendingDownloads.append(ephemerisFilename);
+ m_dlm.download(ephemerisURL, ephemerisFilename, m_parentWidget);
+ downloadRequired = true;
+ }
+ else
+ {
+ qDebug() << "Ephemeris" << ephemerisFilename << "already downloaded";
+ }
+ }
+ }
+ }
+
+ return downloadRequired;
+}
+
+void SpiceEphemerides::downloadComplete(const QString &filename, bool success, const QString &url, const QString &errorMessage)
+{
+ m_completedDownloads.append(filename);
+ if (m_completedDownloads == m_pendingDownloads)
+ {
+ m_pendingDownloads.clear();
+ m_completedDownloads.clear();
+ emit allDownloadsComplete();
+ }
+}
+
+bool SpiceEphemerides::checkDownloaded(const QStringList &emphemeridesURLs) const
+{
+ bool downloaded = true;
+
+ for (const auto& file : getEphemeridesFiles(emphemeridesURLs))
+ {
+ if (!QFileInfo::exists(file))
+ {
+ downloaded = false;
+ break;
+ }
+ }
+
+ return downloaded;
+}
+
+QStringList SpiceEphemerides::getEphemeridesFiles(const QStringList &emphemeridesURLs)
+{
+ QStringList ephemeridesFiles;
+
+ for (const auto& ephemeris : emphemeridesURLs) {
+ ephemeridesFiles.append(urlToFilename(ephemeris));
+ }
+ return ephemeridesFiles;
+}
+
+QString SpiceEphemerides::urlToFilename(const QString &ephemerisURL)
+{
+ if (ephemerisURL.startsWith("http://") || ephemerisURL.startsWith("https://"))
+ {
+ QString ephemeridesDirPath = HttpDownloadManager::downloadDir() + "/ephemerides";
+
+ return ephemeridesDirPath + "/" + QUrl(ephemerisURL).fileName();
+ }
+ else
+ {
+ return ephemerisURL;
+ }
+}
+
+QStringList SpiceEphemerides::getTargets(const QStringList &emphemeridesURLs)
+{
+ QStringList ephemeridesFiles = getEphemeridesFiles(emphemeridesURLs);
+ QStringList targets;
+
+ spiceLock(ephemeridesFiles);
+
+ for (const auto& file : ephemeridesFiles) {
+ targets.append(getSPICETargets(file));
+ }
+
+ spiceUnlock();
+
+ targets.removeDuplicates();
+
+ return targets;
+}
\ No newline at end of file
diff --git a/plugins/feature/startracker/spiceephemerides.h b/plugins/feature/startracker/spiceephemerides.h
new file mode 100644
index 000000000..9bb85ed16
--- /dev/null
+++ b/plugins/feature/startracker/spiceephemerides.h
@@ -0,0 +1,56 @@
+///////////////////////////////////////////////////////////////////////////////////
+// Copyright (C) 2026 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_SPICE_EPHEMERIDES_H_
+#define INCLUDE_SPICE_EPHEMERIDES_H_
+
+#include
+#include
+
+#include "gui/httpdownloadmanagergui.h"
+
+class SpiceEphemerides : public QObject
+{
+ Q_OBJECT
+
+public:
+
+ explicit SpiceEphemerides(QWidget *parentWidget = nullptr);
+ bool download(const QStringList &emphemerides);
+ bool checkDownloaded(const QStringList &emphemerides) const;
+ QStringList getTargets(const QStringList &ephemerisURL);
+ static QStringList getEphemeridesFiles(const QStringList &emphemeridesURLs);
+
+private:
+
+ static QString urlToFilename(const QString &ephemerisURL);
+
+ QWidget *m_parentWidget;
+ HttpDownloadManagerGUI m_dlm;
+
+ QStringList m_pendingDownloads;
+ QStringList m_completedDownloads;
+
+private slots:
+ void downloadComplete(const QString &filename, bool success, const QString &url, const QString &errorMessage);
+
+signals:
+ void allDownloadsComplete();
+
+};
+
+#endif // INCLUDE_SPICE_EPHEMERIDES_H_
diff --git a/plugins/feature/startracker/startracker.cpp b/plugins/feature/startracker/startracker.cpp
index 6740c269b..3f3c9d054 100644
--- a/plugins/feature/startracker/startracker.cpp
+++ b/plugins/feature/startracker/startracker.cpp
@@ -34,6 +34,7 @@
#include "startrackerworker.h"
#include "startracker.h"
+#include "spice.h"
MESSAGE_CLASS_DEFINITION(StarTracker::MsgConfigureStarTracker, Message)
MESSAGE_CLASS_DEFINITION(StarTracker::MsgStartStop, Message)
@@ -74,6 +75,8 @@ StarTracker::StarTracker(WebAPIAdapterInterface *webAPIAdapterInterface) :
m_availableChannelHandler.scanAvailableChannelsAndFeatures();
QObject::connect(&m_availableFeatureHandler, &AvailableChannelOrFeatureHandler::channelsOrFeaturesChanged, this, &StarTracker::featuresChanged);
m_availableFeatureHandler.scanAvailableChannelsAndFeatures();
+
+ spiceInit();
}
StarTracker::~StarTracker()
@@ -246,7 +249,8 @@ void StarTracker::applySettings(const StarTrackerSettings& settings, const QList
}
}
- if (m_worker) {
+ if (m_worker)
+ {
StarTrackerWorker::MsgConfigureStarTrackerWorker *msg = StarTrackerWorker::MsgConfigureStarTrackerWorker::create(
settings, settingsKeys, force
);
diff --git a/plugins/feature/startracker/startracker/callisto-250.png b/plugins/feature/startracker/startracker/callisto-250.png
new file mode 100644
index 000000000..c1a2914f6
Binary files /dev/null and b/plugins/feature/startracker/startracker/callisto-250.png differ
diff --git a/plugins/feature/startracker/startracker/deimos-250.png b/plugins/feature/startracker/startracker/deimos-250.png
new file mode 100644
index 000000000..5e4e268ce
Binary files /dev/null and b/plugins/feature/startracker/startracker/deimos-250.png differ
diff --git a/plugins/feature/startracker/startracker/earth-250.png b/plugins/feature/startracker/startracker/earth-250.png
new file mode 100644
index 000000000..624b11a01
Binary files /dev/null and b/plugins/feature/startracker/startracker/earth-250.png differ
diff --git a/plugins/feature/startracker/startracker/ganymede-250.png b/plugins/feature/startracker/startracker/ganymede-250.png
new file mode 100644
index 000000000..fa85c085e
Binary files /dev/null and b/plugins/feature/startracker/startracker/ganymede-250.png differ
diff --git a/plugins/feature/startracker/startracker/ganymede-phase-vs-cml.png b/plugins/feature/startracker/startracker/ganymede-phase-vs-cml.png
new file mode 100644
index 000000000..3f01cb3c0
Binary files /dev/null and b/plugins/feature/startracker/startracker/ganymede-phase-vs-cml.png differ
diff --git a/plugins/feature/startracker/startracker/io-250.png b/plugins/feature/startracker/startracker/io-250.png
new file mode 100644
index 000000000..a524eea52
Binary files /dev/null and b/plugins/feature/startracker/startracker/io-250.png differ
diff --git a/plugins/feature/startracker/startracker/io-phase-vs-cml.png b/plugins/feature/startracker/startracker/io-phase-vs-cml.png
new file mode 100644
index 000000000..7e9b165b4
Binary files /dev/null and b/plugins/feature/startracker/startracker/io-phase-vs-cml.png differ
diff --git a/plugins/feature/startracker/startracker/jupiter-250.png b/plugins/feature/startracker/startracker/jupiter-250.png
new file mode 100644
index 000000000..8755b79d5
Binary files /dev/null and b/plugins/feature/startracker/startracker/jupiter-250.png differ
diff --git a/plugins/feature/startracker/startracker/mars-250.png b/plugins/feature/startracker/startracker/mars-250.png
new file mode 100644
index 000000000..058d6d6e8
Binary files /dev/null and b/plugins/feature/startracker/startracker/mars-250.png differ
diff --git a/plugins/feature/startracker/startracker/mercury-250.png b/plugins/feature/startracker/startracker/mercury-250.png
new file mode 100644
index 000000000..ca68f6ef5
Binary files /dev/null and b/plugins/feature/startracker/startracker/mercury-250.png differ
diff --git a/plugins/feature/startracker/startracker/moon-250.png b/plugins/feature/startracker/startracker/moon-250.png
new file mode 100644
index 000000000..1c830eb38
Binary files /dev/null and b/plugins/feature/startracker/startracker/moon-250.png differ
diff --git a/plugins/feature/startracker/startracker/neptune-250.png b/plugins/feature/startracker/startracker/neptune-250.png
new file mode 100644
index 000000000..91634361f
Binary files /dev/null and b/plugins/feature/startracker/startracker/neptune-250.png differ
diff --git a/plugins/feature/startracker/startracker/phase-cml-legend.png b/plugins/feature/startracker/startracker/phase-cml-legend.png
new file mode 100644
index 000000000..431faad55
Binary files /dev/null and b/plugins/feature/startracker/startracker/phase-cml-legend.png differ
diff --git a/plugins/feature/startracker/startracker/phobos-250.png b/plugins/feature/startracker/startracker/phobos-250.png
new file mode 100644
index 000000000..5099c920b
Binary files /dev/null and b/plugins/feature/startracker/startracker/phobos-250.png differ
diff --git a/plugins/feature/startracker/startracker/pluto-250.png b/plugins/feature/startracker/startracker/pluto-250.png
new file mode 100644
index 000000000..37c5e29ef
Binary files /dev/null and b/plugins/feature/startracker/startracker/pluto-250.png differ
diff --git a/plugins/feature/startracker/startracker/saturn-250.png b/plugins/feature/startracker/startracker/saturn-250.png
new file mode 100644
index 000000000..9c8f3f715
Binary files /dev/null and b/plugins/feature/startracker/startracker/saturn-250.png differ
diff --git a/plugins/feature/startracker/startracker/sun-250.png b/plugins/feature/startracker/startracker/sun-250.png
new file mode 100644
index 000000000..daad0e16c
Binary files /dev/null and b/plugins/feature/startracker/startracker/sun-250.png differ
diff --git a/plugins/feature/startracker/startracker/uranus-250.png b/plugins/feature/startracker/startracker/uranus-250.png
new file mode 100644
index 000000000..5384981bb
Binary files /dev/null and b/plugins/feature/startracker/startracker/uranus-250.png differ
diff --git a/plugins/feature/startracker/startracker/venus-250.png b/plugins/feature/startracker/startracker/venus-250.png
new file mode 100644
index 000000000..9d175b5e2
Binary files /dev/null and b/plugins/feature/startracker/startracker/venus-250.png differ
diff --git a/plugins/feature/startracker/startrackergui.cpp b/plugins/feature/startracker/startrackergui.cpp
index 87639ab65..9e71b7089 100644
--- a/plugins/feature/startracker/startrackergui.cpp
+++ b/plugins/feature/startracker/startrackergui.cpp
@@ -1,5 +1,5 @@
///////////////////////////////////////////////////////////////////////////////////
-// Copyright (C) 2021-2024 Jon Beniston, M7RCE //
+// Copyright (C) 2021-2026 Jon Beniston, M7RCE //
// Copyright (C) 2021-2022 Edouard Griffiths, F4EXB //
// //
// This program is free software; you can redistribute it and/or modify //
@@ -46,12 +46,12 @@
#include "util/astronomy.h"
#include "util/interpolation.h"
#include "util/png.h"
+#include "util/profiler.h"
#include "maincore.h"
#include "ui_startrackergui.h"
#include "startracker.h"
#include "startrackergui.h"
-#include "startrackerreport.h"
#include "startrackersettingsdialog.h"
StarTrackerGUI* StarTrackerGUI::create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature)
@@ -148,6 +148,16 @@ bool StarTrackerGUI::handleMessage(const Message& message)
raDecChanged();
return true;
}
+ else if (StarTrackerReport::MsgReportAzElVsTime::match(message))
+ {
+ StarTrackerReport::MsgReportAzElVsTime& azElVsTime = (StarTrackerReport::MsgReportAzElVsTime&) message;
+ m_azElVsTimeTarget = azElVsTime.getTarget();
+ m_azimuths = azElVsTime.getAzimuths();
+ m_elevations = azElVsTime.getElevations();
+ m_dateTimes = azElVsTime.getDateTimes();
+ plotChart();
+ return true;
+ }
else if (StarTrackerReport::MsgReportGalactic::match(message))
{
StarTrackerReport::MsgReportGalactic& galactic = (StarTrackerReport::MsgReportGalactic&) message;
@@ -159,6 +169,28 @@ bool StarTrackerGUI::handleMessage(const Message& message)
blockApplySettings(false);
return true;
}
+ else if (StarTrackerReport::MsgReportSolarSystemPositions::match(message))
+ {
+ StarTrackerReport::MsgReportSolarSystemPositions& report = (StarTrackerReport::MsgReportSolarSystemPositions&) message;
+ updateSolarSystemPositions(report.getNames(), report.getPositions());
+ return true;
+ }
+ else if (StarTrackerReport::MsgReportJupiter::match(message))
+ {
+ StarTrackerReport::MsgReportJupiter& report = (StarTrackerReport::MsgReportJupiter&) message;
+ ui->jupiterElevation->setText(QString("%1%2").arg((int) std::round(report.getElevation())).arg(QChar(0xb0)));
+ ui->cml->setText(QString("%1%2").arg((int) std::round(report.getCML())).arg(QChar(0xb0)));
+ ui->ioPhase->setText(QString("%1%2").arg((int) std::round(report.getIoPhase())).arg(QChar(0xb0)));
+ ui->ganymedePhase->setText(QString("%1%2").arg((int) std::round(report.getGanymedePhase())).arg(QChar(0xb0)));
+ updateJupiterMoonPosition(report.getCML(), report.getIoPhase(), report.getGanymedePhase());
+ return true;
+ }
+ else if (StarTrackerReport::MsgReportJupiterData::match(message))
+ {
+ StarTrackerReport::MsgReportJupiterData& report = (StarTrackerReport::MsgReportJupiterData&) message;
+ updateJupiterMoonPositions(report);
+ return true;
+ }
else if (MainCore::MsgStarTrackerDisplaySettings::match(message))
{
if (m_settings.m_link)
@@ -234,49 +266,60 @@ void StarTrackerGUI::updateFeatureList(const AvailableChannelOrFeatureList& feat
// Update list of plugins we can get target from
ui->target->blockSignals(true);
- // Remove targets no longer available
- for (int i = 0; i < ui->target->count(); )
+ if (m_settings.m_targetSource == "SDRangel")
{
- QString text = ui->target->itemText(i);
- bool found = false;
- if (text.contains("SatelliteTracker") || text.contains("SkyMap"))
+ // Remove targets no longer available
+ for (int i = 0; i < ui->target->count(); )
{
- for (const auto& feature : features)
+ QString text = ui->target->itemText(i);
+ bool found = false;
+ if (text.contains("SatelliteTracker") || text.contains("SkyMap"))
{
- if (feature.getLongId() == text)
+ for (const auto& feature : features)
{
- found = true;
- break;
+ if (feature.getLongId() == text)
+ {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ ui->target->removeItem(i);
+ } else {
+ i++;
}
}
- if (!found) {
- ui->target->removeItem(i);
- } else {
+ else
+ {
i++;
}
}
- else
+
+ // Add new targets
+ for (const auto& feature : features)
{
- i++;
+ QString name = feature.getLongId();
+ if (ui->target->findText(name) == -1) {
+ ui->target->addItem(name);
+ }
+ }
+
+ // Features can be created after this plugin, so select it
+ // if the chosen tracker appears
+ int index = ui->target->findText(m_settings.m_target);
+ if (index >= 0) {
+ ui->target->setCurrentIndex(index);
}
}
-
- // Add new targets
- for (const auto& feature : features)
+ else
{
- QString name = feature.getLongId();
- if (ui->target->findText(name) == -1) {
- ui->target->addItem(name);
+ // Save feature list for use in updateTargetList
+ m_availableFeatures.clear();
+ for (const auto& feature : features) {
+ m_availableFeatures.append(feature.getLongId());
}
}
- // Features can be created after this plugin, so select it
- // if the chosen tracker appears
- int index = ui->target->findText(m_settings.m_target);
- if (index >= 0) {
- ui->target->setCurrentIndex(index);
- }
-
ui->target->blockSignals(false);
}
@@ -315,6 +358,9 @@ StarTrackerGUI::StarTrackerGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet,
m_azElPolarChart(nullptr),
m_solarFluxChart(nullptr),
m_networkManager(nullptr),
+ m_startAfterDownload(false),
+ m_jplHorizons(nullptr),
+ m_spiceEphemerides(this),
m_solarFlux(0.0),
m_solarFluxesValid(false),
m_images{QImage(":/startracker/startracker/150mhz_ra_dec.png"),
@@ -323,8 +369,32 @@ StarTrackerGUI::StarTrackerGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet,
QImage(":/startracker/startracker/408mhz_galactic.png"),
QImage(":/startracker/startracker/1420mhz_ra_dec.png"),
QImage(":/startracker/startracker/1420mhz_galactic.png")},
+ m_zoom(nullptr),
m_milkyWayImages{QPixmap(":/startracker/startracker/milkyway.png"),
QPixmap(":/startracker/startracker/milkywayannotated.png")},
+ m_solarSystemLabelFontMetrics(font()),
+ m_planetImages{
+ {"callisto", QPixmap(":/startracker/startracker/callisto-250.png")},
+ {"deimos", QPixmap(":/startracker/startracker/deimos-250.png")},
+ {"earth", QPixmap(":/startracker/startracker/earth-250.png")},
+ {"ganymede", QPixmap(":/startracker/startracker/ganymede-250.png")},
+ {"io", QPixmap(":/startracker/startracker/io-250.png")},
+ {"jupiter", QPixmap(":/startracker/startracker/jupiter-250.png")},
+ {"mars", QPixmap(":/startracker/startracker/mars-250.png")},
+ {"mercury", QPixmap(":/startracker/startracker/mercury-250.png")},
+ {"moon", QPixmap(":/startracker/startracker/moon-250.png")},
+ {"neptune", QPixmap(":/startracker/startracker/neptune-250.png")},
+ {"phobos", QPixmap(":/startracker/startracker/phobos-250.png")},
+ {"pluto", QPixmap(":/startracker/startracker/pluto-250.png")},
+ {"saturn", QPixmap(":/startracker/startracker/saturn-250.png")},
+ {"sun", QPixmap(":/startracker/startracker/sun-250.png")},
+ {"uranus", QPixmap(":/startracker/startracker/uranus-250.png")},
+ {"venus", QPixmap(":/startracker/startracker/venus-250.png")}
+ },
+ m_jupiterImages{QPixmap(":/startracker/startracker/io-phase-vs-cml.png"),
+ QPixmap(":/startracker/startracker/ganymede-phase-vs-cml.png"),
+ QPixmap(":/startracker/startracker/phase-cml-legend.png")
+ },
m_sunRA(0.0),
m_sunDec(0.0),
m_moonRA(0.0),
@@ -452,14 +522,30 @@ StarTrackerGUI::StarTrackerGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet,
autoUpdateSolarFlux();
createGalacticLineOfSightScene();
+ createSolarSystemScene();
+ createJupiterScene();
plotChart();
StarTracker::MsgRequestAvailableFeatures *message = StarTracker::MsgRequestAvailableFeatures::create();
m_starTracker->getInputMessageQueue()->push(message);
+
+ connect(&m_spiceEphemerides, &SpiceEphemerides::allDownloadsComplete, this, &StarTrackerGUI::spiceDownloadsComplete);
+
+ m_jplHorizons = JPLHorizons::create();
+ if (m_jplHorizons)
+ {
+ connect(m_jplHorizons, &JPLHorizons::majorBodiesUpdated, this, &StarTrackerGUI::majorBodiesUpdated);
+ m_jplHorizons->getMajorBodiesList();
+ }
}
StarTrackerGUI::~StarTrackerGUI()
{
+ if (m_jplHorizons)
+ {
+ disconnect(m_jplHorizons, &JPLHorizons::majorBodiesUpdated, this, &StarTrackerGUI::majorBodiesUpdated);
+ delete m_jplHorizons;
+ }
QObject::disconnect(
m_networkManager,
&QNetworkAccessManager::finished,
@@ -507,11 +593,18 @@ void StarTrackerGUI::displaySettings()
}
m_chart.setTheme(m_settings.m_chartsDarkTheme ? QChart::ChartThemeDark : QChart::ChartThemeLight);
+ ui->night->setChecked(m_settings.m_night);
+ ui->logScale->setChecked(m_settings.m_logScale);
ui->drawSun->setChecked(m_settings.m_drawSunOnSkyTempChart);
ui->drawMoon->setChecked(m_settings.m_drawMoonOnSkyTempChart);
ui->link->setChecked(m_settings.m_link);
ui->latitude->setValue(m_settings.m_latitude);
ui->longitude->setValue(m_settings.m_longitude);
+ ui->targetSource->setCurrentIndex(ui->targetSource->findText(m_settings.m_targetSource));
+ ui->target->setEditable(m_settings.m_targetSource == "Horizons");
+ if (ui->target->lineEdit()) {
+ QObject::connect(ui->target->lineEdit(), &QLineEdit::editingFinished, this, &StarTrackerGUI::on_target_editingFinished, Qt::UniqueConnection);
+ }
ui->target->setCurrentIndex(ui->target->findText(m_settings.m_target));
ui->azimuth->setUnits((DMSSpinBox::DisplayUnits)m_settings.m_azElUnits);
ui->elevation->setUnits((DMSSpinBox::DisplayUnits)m_settings.m_azElUnits);
@@ -543,18 +636,26 @@ void StarTrackerGUI::displaySettings()
{
ui->dateTimeSelect->setCurrentIndex(0);
ui->dateTime->setVisible(false);
+ ui->utc->setVisible(false);
+ ui->setTimeToNow->setVisible(false);
}
else
{
ui->dateTime->setDateTime(QDateTime::fromString(m_settings.m_dateTime, Qt::ISODateWithMs));
ui->dateTime->setVisible(true);
ui->dateTimeSelect->setCurrentIndex(1);
+ ui->utc->setVisible(true);
+ ui->setTimeToNow->setVisible(true);
}
+ ui->utc->setChecked(m_settings.m_utc);
if ((m_settings.m_solarFluxData != StarTrackerSettings::DRAO_2800) && !m_solarFluxesValid) {
autoUpdateSolarFlux();
}
+ ui->chartSelect->setCurrentIndex((int) m_settings.m_chartSelect);
+ ui->chartSubSelect->setCurrentIndex(m_settings.m_chartSubSelect);
+
ui->frequency->setValue(m_settings.m_frequency/1000000.0);
ui->beamwidth->setValue(m_settings.m_beamwidth);
updateForTarget();
@@ -604,12 +705,54 @@ void StarTrackerGUI::onMenuDialogCalled(const QPoint &p)
resetContextMenuType();
}
+void StarTrackerGUI::downloadSPICEEphemerides()
+{
+ m_spiceEphemerides.download(m_settings.m_spiceEphemerides);
+}
+
+bool StarTrackerGUI::checkSPICEEphemerides()
+{
+ return m_spiceEphemerides.checkDownloaded(m_settings.m_spiceEphemerides);
+}
+
+void StarTrackerGUI::spiceDownloadsComplete()
+{
+ updateTargetList();
+ on_chartSelect_currentIndexChanged(ui->chartSelect->currentIndex());
+
+ if (m_startAfterDownload)
+ {
+ if (checkSPICEEphemerides())
+ {
+ m_startAfterDownload = false;
+ StarTracker::MsgStartStop *message = StarTracker::MsgStartStop::create(true);
+ m_starTracker->getInputMessageQueue()->push(message);
+ }
+ }
+}
+
void StarTrackerGUI::on_startStop_toggled(bool checked)
{
if (m_doApplySettings)
{
- StarTracker::MsgStartStop *message = StarTracker::MsgStartStop::create(checked);
- m_starTracker->getInputMessageQueue()->push(message);
+ if (checked)
+ {
+ if (checkSPICEEphemerides())
+ {
+ StarTracker::MsgStartStop *message = StarTracker::MsgStartStop::create(checked);
+ m_starTracker->getInputMessageQueue()->push(message);
+ }
+ else
+ {
+ m_startAfterDownload = true;
+ downloadSPICEEphemerides();
+ }
+ }
+ else
+ {
+ StarTracker::MsgStartStop *message = StarTracker::MsgStartStop::create(checked);
+ m_starTracker->getInputMessageQueue()->push(message);
+ }
}
}
@@ -695,44 +838,32 @@ void StarTrackerGUI::on_galacticLongitude_valueChanged(double value)
void StarTrackerGUI::updateForTarget()
{
- if (m_settings.m_target == "Sun")
+ const QStringList raDecTargets = {
+ "PSR B0329+54", "PSR B0833-45", "Sagittarius A", "Cassiopeia A", "Cygnus A", "Taurus A (M1)", "Virgo A (M87)"
+ };
+ const QStringList lbTargets = {
+ "S7", "S8", "S9"
+ };
+
+ if ((m_settings.m_target == "Sun") || (m_settings.m_target == "Moon"))
{
ui->rightAscension->setReadOnly(true);
ui->declination->setReadOnly(true);
+ ui->azimuth->setReadOnly(true);
+ ui->elevation->setReadOnly(true);
+ ui->galacticLatitude->setReadOnly(true);
+ ui->galacticLongitude->setReadOnly(true);
ui->rightAscension->setText("");
ui->declination->setText("");
}
- else if (m_settings.m_target == "Moon")
- {
- ui->rightAscension->setReadOnly(true);
- ui->declination->setReadOnly(true);
- ui->rightAscension->setText("");
- ui->declination->setText("");
- }
- else if (m_settings.m_target == "Custom RA/Dec")
- {
- ui->rightAscension->setReadOnly(false);
- ui->declination->setReadOnly(false);
- }
- else if (m_settings.m_target == "S7")
- {
- ui->galacticLatitude->setValue(-1.0);
- ui->galacticLongitude->setValue(132.0);
- }
- else if (m_settings.m_target == "S8")
- {
- ui->galacticLatitude->setValue(-15.0);
- ui->galacticLongitude->setValue(207.0);
- }
- else if (m_settings.m_target == "S9")
- {
- ui->galacticLatitude->setValue(-4.0);
- ui->galacticLongitude->setValue(356.0);
- }
- else
+ else if (raDecTargets.contains(m_settings.m_target))
{
ui->rightAscension->setReadOnly(true);
ui->declination->setReadOnly(true);
+ ui->azimuth->setReadOnly(true);
+ ui->elevation->setReadOnly(true);
+ ui->galacticLatitude->setReadOnly(true);
+ ui->galacticLongitude->setReadOnly(true);
if (m_settings.m_target == "PSR B0329+54")
{
ui->rightAscension->setText("03h32m59.35s");
@@ -771,32 +902,88 @@ void StarTrackerGUI::updateForTarget()
on_rightAscension_editingFinished();
on_declination_editingFinished();
}
- if (m_settings.m_target.contains("SatelliteTracker"))
+ else if (lbTargets.contains(m_settings.m_target))
{
- ui->azimuth->setReadOnly(true);
- ui->elevation->setReadOnly(true);
ui->rightAscension->setReadOnly(true);
ui->declination->setReadOnly(true);
- }
- else if (m_settings.m_target != "Custom Az/El")
- {
ui->azimuth->setReadOnly(true);
ui->elevation->setReadOnly(true);
- // Clear as no longer valid when target has changed
- ui->azimuth->setText("");
- ui->elevation->setText("");
+ ui->galacticLatitude->setReadOnly(true);
+ ui->galacticLongitude->setReadOnly(true);
+ ui->rightAscension->setText("");
+ ui->declination->setText("");
+ if (m_settings.m_target == "S7")
+ {
+ ui->galacticLatitude->setValue(-1.0);
+ ui->galacticLongitude->setValue(132.0);
+ }
+ else if (m_settings.m_target == "S8")
+ {
+ ui->galacticLatitude->setValue(-15.0);
+ ui->galacticLongitude->setValue(207.0);
+ }
+ else if (m_settings.m_target == "S9")
+ {
+ ui->galacticLatitude->setValue(-4.0);
+ ui->galacticLongitude->setValue(356.0);
+ }
}
- else
+ else if (m_settings.m_target == "Custom RA/Dec")
+ {
+ ui->rightAscension->setReadOnly(false);
+ ui->declination->setReadOnly(false);
+ ui->azimuth->setReadOnly(true);
+ ui->elevation->setReadOnly(true);
+ ui->galacticLatitude->setReadOnly(true);
+ ui->galacticLongitude->setReadOnly(true);
+ m_settingsKeys.append("ra");
+ m_settingsKeys.append("dec");
+ applySettings();
+ }
+ else if (m_settings.m_target == "Custom Az/El")
{
ui->rightAscension->setReadOnly(true);
ui->declination->setReadOnly(true);
ui->azimuth->setReadOnly(false);
ui->elevation->setReadOnly(false);
+ ui->galacticLatitude->setReadOnly(true);
+ ui->galacticLongitude->setReadOnly(true);
+ ui->rightAscension->setText("");
+ ui->declination->setText("");
+ m_settingsKeys.append("azimuth");
+ m_settingsKeys.append("elevation");
+ applySettings();
+ }
+ else if (m_settings.m_target == "Custom l/b")
+ {
+ ui->rightAscension->setReadOnly(true);
+ ui->declination->setReadOnly(true);
+ ui->azimuth->setReadOnly(true);
+ ui->elevation->setReadOnly(true);
+ ui->galacticLatitude->setReadOnly(false);
+ ui->galacticLongitude->setReadOnly(false);
+ ui->rightAscension->setText("");
+ ui->declination->setText("");
+ m_settingsKeys.append("l");
+ m_settingsKeys.append("b");
+ applySettings();
+ }
+ else if (m_settings.m_target.contains("SatelliteTracker"))
+ {
+ ui->azimuth->setReadOnly(true);
+ ui->elevation->setReadOnly(true);
+ ui->rightAscension->setReadOnly(true);
+ ui->declination->setReadOnly(true);
+ ui->galacticLatitude->setReadOnly(true);
+ ui->galacticLongitude->setReadOnly(true);
+ ui->rightAscension->setText("");
+ ui->declination->setText("");
}
}
-void StarTrackerGUI::on_target_currentTextChanged(const QString &text)
+void StarTrackerGUI::on_target_currentIndexChanged(int index)
{
+ QString text = ui->target->currentText();
if (!text.isEmpty())
{
m_settings.m_target = text;
@@ -807,14 +994,55 @@ void StarTrackerGUI::on_target_currentTextChanged(const QString &text)
}
}
+void StarTrackerGUI::on_target_editingFinished()
+{
+ QString text = ui->target->currentText();
+ if (!text.isEmpty())
+ {
+ m_settings.m_target = text;
+ m_settingsKeys.append("target");
+ applySettings();
+ updateForTarget();
+ plotChart();
+ }
+}
+
+void StarTrackerGUI::on_targetSource_currentIndexChanged(int index)
+{
+ (void) index;
+
+ m_settings.m_targetSource = ui->targetSource->currentText();
+ m_settingsKeys.append("targetSource");
+ applySettings();
+
+ if (m_settings.m_targetSource == "SPICE")
+ {
+ if (!checkSPICEEphemerides()) {
+ downloadSPICEEphemerides();
+ }
+ }
+
+ ui->target->setEditable(m_settings.m_targetSource == "Horizons");
+ if (ui->target->lineEdit()) {
+ QObject::connect(ui->target->lineEdit(), &QLineEdit::editingFinished, this, &StarTrackerGUI::on_target_editingFinished, Qt::UniqueConnection);
+ }
+ updateTargetList();
+ on_target_currentIndexChanged(ui->target->currentIndex()); // updateTargetList blocks signals, so update target manually
+}
+
void StarTrackerGUI::updateLST()
{
QDateTime dt;
if (m_settings.m_dateTime.isEmpty()) {
dt = QDateTime::currentDateTime();
- } else {
+ }
+ else
+ {
dt = QDateTime::fromString(m_settings.m_dateTime, Qt::ISODateWithMs);
+ if (m_settings.m_utc) {
+ dt.setTimeZone(QTimeZone::utc());
+ }
}
double lst = Astronomy::localSiderealTime(dt, m_settings.m_longitude);
@@ -907,9 +1135,14 @@ void StarTrackerGUI::on_displaySettings_clicked()
ui->galacticLongitude->setUnits((DMSSpinBox::DisplayUnits)m_settings.m_azElUnits);
displaySolarFlux();
- if (ui->chartSelect->currentIndex() <= 1) {
+ if (ui->chartSelect->currentIndex() <= StarTrackerSettings::CHART_SOLAR_FLUX_VS_FREQUENCY) {
plotChart();
}
+ if (ui->chartSelect->currentIndex() == StarTrackerSettings::CHART_SOLAR_SYSTEM) {
+ on_chartSelect_currentIndexChanged(ui->chartSelect->currentIndex()); // Update list of bodies
+ }
+
+ checkSPICEEphemerides();
}
}
@@ -919,11 +1152,15 @@ void StarTrackerGUI::on_dateTimeSelect_currentTextChanged(const QString &text)
{
m_settings.m_dateTime = "";
ui->dateTime->setVisible(false);
+ ui->utc->setVisible(false);
+ ui->setTimeToNow->setVisible(false);
}
else
{
m_settings.m_dateTime = ui->dateTime->dateTime().toString(Qt::ISODateWithMs);
ui->dateTime->setVisible(true);
+ ui->utc->setVisible(true);
+ ui->setTimeToNow->setVisible(true);
}
m_settingsKeys.append("dateTime");
@@ -944,12 +1181,26 @@ void StarTrackerGUI::on_dateTime_dateTimeChanged(const QDateTime &datetime)
}
}
+void StarTrackerGUI::on_utc_clicked(bool checked)
+{
+ m_settings.m_utc = checked;
+ m_settingsKeys.append("utc");
+ applySettings();
+}
+
+void StarTrackerGUI::on_setTimeToNow_clicked(bool checked)
+{
+ (void) checked;
+
+ ui->dateTime->setDateTime(QDateTime::currentDateTime());
+}
+
void StarTrackerGUI::plotChart()
{
if (!m_doPlotChart) {
return;
}
- if (ui->chartSelect->currentIndex() == 0)
+ if (ui->chartSelect->currentIndex() == StarTrackerSettings::CHART_ELEVATION_VS_TIME)
{
if (ui->chartSubSelect->currentIndex() == 0) {
plotElevationLineChart();
@@ -957,25 +1208,33 @@ void StarTrackerGUI::plotChart()
plotElevationPolarChart();
}
}
- else if (ui->chartSelect->currentIndex() == 1)
+ else if (ui->chartSelect->currentIndex() == StarTrackerSettings::CHART_SOLAR_FLUX_VS_FREQUENCY)
{
plotSolarFluxChart();
}
- else if (ui->chartSelect->currentIndex() == 2)
+ else if (ui->chartSelect->currentIndex() == StarTrackerSettings::CHART_SKY_TEMPERATURE)
{
plotSkyTemperatureChart();
}
- else if (ui->chartSelect->currentIndex() == 3)
+ else if (ui->chartSelect->currentIndex() == StarTrackerSettings::CHART_GALACTIC_LINE_OF_SIGHT)
{
plotGalacticLineOfSight();
}
+ else if (ui->chartSelect->currentIndex() == StarTrackerSettings::CHART_SOLAR_SYSTEM)
+ {
+ plotSolarSystem();
+ }
+ else if (ui->chartSelect->currentIndex() == StarTrackerSettings::CHART_JUPITER)
+ {
+ plotJupiter();
+ }
}
void StarTrackerGUI::raDecChanged()
{
- if (ui->chartSelect->currentIndex() == 2) {
+ if (ui->chartSelect->currentIndex() == StarTrackerSettings::CHART_SKY_TEMPERATURE) {
plotSkyTemperatureChart();
- } else if (ui->chartSelect->currentIndex() == 3) {
+ } else if (ui->chartSelect->currentIndex() == StarTrackerSettings::CHART_GALACTIC_LINE_OF_SIGHT) {
plotGalacticLineOfSight();
}
}
@@ -1002,7 +1261,7 @@ void StarTrackerGUI::on_beamwidth_valueChanged(double value)
applySettings();
updateChartSubSelect();
- if (ui->chartSelect->currentIndex() == 2) {
+ if (ui->chartSelect->currentIndex() == StarTrackerSettings::CHART_SKY_TEMPERATURE) {
plotChart();
}
}
@@ -1011,6 +1270,8 @@ void StarTrackerGUI::plotSolarFluxChart()
{
ui->chart->setVisible(true);
ui->image->setVisible(false);
+ ui->night->setVisible(false);
+ ui->logScale->setVisible(false);
ui->drawSun->setVisible(false);
ui->drawMoon->setVisible(false);
ui->darkTheme->setVisible(true);
@@ -1019,6 +1280,14 @@ void StarTrackerGUI::plotSolarFluxChart()
ui->addAnimationFrame->setVisible(false);
ui->clearAnimation->setVisible(false);
ui->saveAnimation->setVisible(false);
+ ui->jupiterElevationLabel->setVisible(false);
+ ui->jupiterElevation->setVisible(false);
+ ui->cmlLabel->setVisible(false);
+ ui->cml->setVisible(false);
+ ui->ioPhaseLabel->setVisible(false);
+ ui->ioPhase->setVisible(false);
+ ui->ganymedePhaseLabel->setVisible(false);
+ ui->ganymedePhase->setVisible(false);
QChart *oldChart = m_solarFluxChart;
@@ -1101,11 +1370,7 @@ QList StarTrackerGUI::createDriftScan(bool galactic)
QDateTime dt;
// Get date and time to calculate position at
- if (m_settings.m_dateTime == "") {
- dt = QDateTime::currentDateTime();
- } else {
- dt = QDateTime::fromString(m_settings.m_dateTime, Qt::ISODateWithMs);
- }
+ dt = m_settings.getDateTime();
// Create a list of RA/Dec points of drift scan path
AzAlt aa;
@@ -1203,40 +1468,29 @@ QColor StarTrackerGUI::getSeriesColor(int series)
void StarTrackerGUI::createGalacticLineOfSightScene()
{
- m_zoom = new GraphicsViewZoom(ui->image); // Deleted automatically when view is deleted
-
- QGraphicsScene *scene = new QGraphicsScene(ui->image);
- scene->setBackgroundBrush(QBrush(Qt::black));
+ m_galacticLineOfSightScene = new QGraphicsScene(ui->image);
+ m_galacticLineOfSightScene->setBackgroundBrush(QBrush(Qt::black));
// Milkyway images
for (int i = 0; i < m_milkyWayImages.size(); i++)
{
- m_milkyWayItems.append(scene->addPixmap(m_milkyWayImages[i]));
+ m_milkyWayItems.append(m_galacticLineOfSightScene->addPixmap(m_milkyWayImages[i]));
m_milkyWayItems[i]->setPos(0, 0);
m_milkyWayItems[i]->setVisible(i == 0);
}
// Line of sight
QPen pen(QColor(255, 0, 0), 4, Qt::SolidLine);
- m_lineOfSight = scene->addLine(511, 708, 511, 708, pen);
-
- ui->image->setScene(scene);
- ui->image->show();
-
- ui->image->setDragMode(QGraphicsView::ScrollHandDrag);
+ m_lineOfSight = m_galacticLineOfSightScene->addLine(511, 708, 511, 708, pen);
}
+// Draw top-down image of Milky Way
void StarTrackerGUI::plotGalacticLineOfSight()
{
- if (!ui->image->isVisible())
- {
- // Start zoomed out
- ui->image->fitInView(m_milkyWayItems[0], Qt::KeepAspectRatio);
- }
-
- // Draw top-down image of Milky Way
ui->chart->setVisible(false);
ui->image->setVisible(true);
+ ui->night->setVisible(false);
+ ui->logScale->setVisible(false);
ui->drawSun->setVisible(false);
ui->drawMoon->setVisible(false);
ui->darkTheme->setVisible(false);
@@ -1245,6 +1499,29 @@ void StarTrackerGUI::plotGalacticLineOfSight()
ui->addAnimationFrame->setVisible(true);
ui->clearAnimation->setVisible(true);
ui->saveAnimation->setVisible(true);
+ ui->jupiterElevationLabel->setVisible(false);
+ ui->jupiterElevation->setVisible(false);
+ ui->cmlLabel->setVisible(false);
+ ui->cml->setVisible(false);
+ ui->ioPhaseLabel->setVisible(false);
+ ui->ioPhase->setVisible(false);
+ ui->ganymedePhaseLabel->setVisible(false);
+ ui->ganymedePhase->setVisible(false);
+
+ if (ui->image->scene() != m_galacticLineOfSightScene)
+ {
+ ui->image->setScene(m_galacticLineOfSightScene);
+ ui->image->resetTransform();
+ // Start zoomed out
+ ui->image->fitInView(m_milkyWayItems[0], Qt::KeepAspectRatio);
+ ui->image->setDragMode(QGraphicsView::ScrollHandDrag);
+ }
+
+ if (!m_zoom)
+ {
+ m_zoom = new GraphicsViewZoom(ui->image); // Deleted automatically when view is deleted
+ connect(m_zoom, &GraphicsViewZoom::zoomed, this, &StarTrackerGUI::zoomed);
+ }
// Select which Milky Way image to show
int imageIdx = ui->chartSubSelect->currentIndex();
@@ -1294,6 +1571,13 @@ void StarTrackerGUI::on_zoomOut_clicked()
m_zoom->gentleZoom(0.75);
}
+void StarTrackerGUI::zoomed()
+{
+ if (ui->chartSelect->currentIndex() == StarTrackerSettings::CHART_SOLAR_SYSTEM) {
+ scaleSolarSystemItems();
+ }
+}
+
void StarTrackerGUI::on_addAnimationFrame_clicked()
{
QImage image(ui->image->size(), QImage::Format_ARGB32);
@@ -1334,10 +1618,561 @@ void StarTrackerGUI::on_saveAnimation_clicked()
}
}
+void StarTrackerGUI::createSolarSystemScene()
+{
+ m_solarSystemScene = new QGraphicsScene(ui->image);
+ m_solarSystemScene->setBackgroundBrush(QBrush(Qt::black));
+
+ QDateTime dt = m_settings.getDateTime();
+ double t;
+ t = (Astronomy::julianDate(dt) - 2451545.0) / 365250.0;
+
+ m_solarSystemLabelFont = m_solarSystemScene->font();
+ m_solarSystemLabelFont.setPointSize(6);
+ m_solarSystemLabelFontMetrics = QFontMetrics(m_solarSystemLabelFont);
+
+ if (m_settings.m_logScale)
+ {
+ // Pluto is ~56 from Sun on log scale
+ double r = 57;
+ m_solarSystemScene->setSceneRect(-r, -r, r * 2, r * 2);
+ }
+ else
+ {
+ // Mercury is 0.4AU. Neptune is 30 AU
+ double scale = 20;
+ //m_solarSystemScene->setSceneRect(-30 * scale, -30 * scale, 30 * 2 * scale, 30 * 2 * scale);
+ }
+}
+
+QPixmap *StarTrackerGUI::getPlanetPixmap(const QString& name)
+{
+ QString nameLower = name.toLower();
+ if (m_planetImages.contains(nameLower)) {
+ return &m_planetImages[nameLower];
+ } else {
+ return &m_planetImages["mercury"]; // Use mercury as generic grey moon
+ }
+}
+
+static float getScale(const QString& name)
+{
+ if (name == "mercury") {
+ return 0.5f;
+ } else if (name == "venus") {
+ return 0.7f;
+ } else if (name == "earth") {
+ return 0.7f;
+ } else if (name == "moon") {
+ return 0.5f;
+ } else if (name == "mars") {
+ return 0.6f;
+ } else if (name == "jupiter") {
+ return 1.0f;
+ } else if (name == "saturn") {
+ return 0.9f;
+ } else if (name == "uranus") {
+ return 0.8f;
+ } else if (name == "neptune") {
+ return 0.8f;
+ } else if (name == "pluto") {
+ return 0.4f;
+ } else {
+ return 1.0f;
+ }
+}
+
+static void logScale(QVector3D &p)
+{
+ // Rectangular to spherical
+ float r = p.length();
+ float az = std::atan2(p.y(), p.x());
+ float el = r > 0.0f ? std::acos(p.z() / r) : 0.0f;
+
+ // Log scale radius (log10(mercuryRadiusKM) = 7.7, log10(neptuneRadiusKM) = 9.6)
+ if (r >= 1e7) {
+ r = (std::log10(r) - 7.0f) * 20.0f;
+ } else {
+ r = 0.0f;
+ }
+
+ // Spherical to rectangular
+ p.setX(r * std::sin(el) * std::cos(az));
+ p.setY(r * std::sin(el) * std::sin(az));
+ p.setZ(r * std::cos(el));
+}
+
+
+
+QRectF getVisibleRect( QGraphicsView * view )
+{
+ QPointF A = view->mapToScene( QPoint(0, 0) );
+ QPointF B = view->mapToScene( QPoint(
+ view->viewport()->width(),
+ view->viewport()->height() ));
+ return QRectF( A, B );
+}
+
+
+void StarTrackerGUI::updateSolarSystemPositions(const QStringList &names, const QList &positions)
+{
+ const double kmToAU = 6.68459e-9; // Convert position from kM to AU
+ const double pixelScale = 20.0; // Mercury is 0.4AU. Neptune is 30 AU
+ bool scale = false;
+
+ for (int i = 0; i < names.size(); i++)
+ {
+ SolarSystemItem *item;
+
+ if (m_solarSystemItems.contains(names[i]))
+ {
+ item = m_solarSystemItems.value(names[i]);
+ }
+ else
+ {
+ QString name = names[i];
+ name = name.replace(" BARYCENTER", "");
+
+ QPixmap* pixmap = getPlanetPixmap(name);
+
+ QBrush brush(QColor(200, 200, 200));
+ item = new SolarSystemItem();
+ item->m_textItem = m_solarSystemScene->addText(name, m_solarSystemLabelFont);
+ item->m_scale = getScale(name.toLower());
+ item->m_pixmapItem = m_solarSystemScene->addPixmap(*pixmap);
+ item->m_pixmapItem->setOffset(-pixmap->width() / 2, -pixmap->height() / 2);
+ m_solarSystemItems.insert(names[i], item);
+ scale = true;
+ }
+
+ // Optional log scaling of orbital radius, so outer planets have similar separation to inner planets
+ QVector3D scaledPos = positions[i];
+ if (m_settings.m_logScale) {
+ logScale(scaledPos);
+ } else {
+ scaledPos = pixelScale * kmToAU * scaledPos;
+ }
+
+ // Position label to right
+ const int textYOffset = m_solarSystemLabelFontMetrics.height();
+ item->m_pixmapItem->setPos(scaledPos.x(), scaledPos.y());
+ item->m_textItem->setPos(scaledPos.x() - 1.1 * item->m_pixmapItem->offset().x() * item->m_pixmapItem->scale(), scaledPos.y() - textYOffset * item->m_textItem->scale());
+ }
+
+ if (scale) {
+ scaleSolarSystemItems();
+ }
+
+ // Remove no longer needed items
+ QMutableHashIterator itr(m_solarSystemItems);
+
+ while (itr.hasNext())
+ {
+ itr.next();
+ if (!names.contains(itr.key()))
+ {
+ SolarSystemItem* item = itr.value();
+ m_solarSystemScene->removeItem(item->m_pixmapItem);
+ m_solarSystemScene->removeItem(item->m_textItem);
+ delete item;
+ itr.remove();
+ }
+ }
+}
+
+void StarTrackerGUI::scaleSolarSystemItems()
+{
+ float pixmapScale;
+ float textScale;
+ QTransform tf = ui->image->transform();
+
+ if (tf.m11() <= 1)
+ {
+ pixmapScale = 0.01 / tf.m11(); // Keep fixed size as we zoom out
+ textScale = 1.0 / tf.m11();
+ }
+ else if (tf.m11() >= 15)
+ {
+ pixmapScale = 0.01 * 15 / tf.m11(); // Keep fixed size as we zoom in to planetary scale
+ textScale = 1.0 / tf.m11();
+ }
+ else
+ {
+ pixmapScale = 0.01; // Get larger as we zoom in
+ textScale = 1.0 / tf.m11(); // Keep fixed size
+ }
+
+ for (auto& item : m_solarSystemItems)
+ {
+ item->m_textItem->setScale(textScale);
+
+ if (item->m_pixmapItem)
+ {
+ item->m_pixmapItem->setScale(pixmapScale * item->m_scale);
+ QPointF pos = item->m_pixmapItem->pos();
+ int textYOffset = m_solarSystemLabelFontMetrics.height();
+ item->m_textItem->setPos(pos.x() - 1.1 * item->m_pixmapItem->offset().x() * item->m_pixmapItem->scale(), pos.y() - textYOffset * item->m_textItem->scale());
+ }
+ }
+
+ centerOnSolarSystemBody();
+}
+
+// Draw top-down image of Solar System
+void StarTrackerGUI::plotSolarSystem()
+{
+ ui->chart->setVisible(false);
+ ui->image->setVisible(true);
+ ui->night->setVisible(false);
+ ui->logScale->setVisible(true);
+ ui->drawSun->setVisible(false);
+ ui->drawMoon->setVisible(false);
+ ui->darkTheme->setVisible(false);
+ ui->zoomIn->setVisible(true);
+ ui->zoomOut->setVisible(true);
+ ui->addAnimationFrame->setVisible(false);
+ ui->clearAnimation->setVisible(false);
+ ui->saveAnimation->setVisible(false);
+ ui->jupiterElevationLabel->setVisible(false);
+ ui->jupiterElevation->setVisible(false);
+ ui->jupiterElevationLabel->setVisible(false);
+ ui->jupiterElevation->setVisible(false);
+ ui->cmlLabel->setVisible(false);
+ ui->cml->setVisible(false);
+ ui->ioPhaseLabel->setVisible(false);
+ ui->ioPhase->setVisible(false);
+ ui->ganymedePhaseLabel->setVisible(false);
+ ui->ganymedePhase->setVisible(false);
+
+ if (ui->image->scene() != m_solarSystemScene)
+ {
+ ui->image->setScene(m_solarSystemScene);
+ ui->image->resetTransform();
+ ui->image->fitInView(m_solarSystemScene->sceneRect(), Qt::KeepAspectRatio);
+ scaleSolarSystemItems();
+ }
+
+ if (!m_zoom)
+ {
+ m_zoom = new GraphicsViewZoom(ui->image); // Deleted automatically when view is deleted
+ connect(m_zoom, &GraphicsViewZoom::zoomed, this, &StarTrackerGUI::zoomed);
+ }
+
+ centerOnSolarSystemBody();
+}
+
+void StarTrackerGUI::centerOnSolarSystemBody()
+{
+ // Centre on selected body
+ QString selectedBody = ui->chartSubSelect->currentText();
+ if (m_solarSystemItems.contains(selectedBody))
+ {
+ SolarSystemItem *item = m_solarSystemItems.value(selectedBody);
+ //qDebug() << "scene rect" << m_solarSystemScene->sceneRect() << "view scene rect" << ui->image->sceneRect() << "visible rect" << getVisibleRect(ui->image) << "centre on" << selectedBody << item->m_pixmapItem->pos();
+ ui->image->centerOn(item->m_pixmapItem);
+ ui->image->setDragMode(QGraphicsView::NoDrag);
+ }
+ else
+ {
+ ui->image->setDragMode(QGraphicsView::ScrollHandDrag);
+ }
+}
+
+QList StarTrackerGUI::createJupiterLegendLabels(int legendXRight, int legendYBottom, int legendStep, int legendMax)
+{
+ QList list;
+
+ int legendSteps = legendMax / legendStep;
+ int legendYStep = m_jupiterImages[2].height() / legendSteps;
+ int maxLegendYLabelWidth = 0;
+ for (int i = 0; i <= legendSteps; i++)
+ {
+ QString legendStr = QString::number(i * legendStep);
+ QGraphicsTextItem *legendLabel = m_jupiterScene->addText(legendStr);
+ int yw = legendLabel->boundingRect().width();
+ maxLegendYLabelWidth = std::max(maxLegendYLabelWidth, yw);
+ legendLabel->setPos(legendXRight, legendYBottom - i * legendYStep - legendLabel->boundingRect().height() / 2);
+ list.append(legendLabel);
+ }
+
+ return list;
+}
+
+void StarTrackerGUI::createJupiterScene()
+{
+ m_jupiterScene = new QGraphicsScene(ui->image);
+ m_jupiterScene->setBackgroundBrush(QBrush(Qt::black));
+
+ // Phase vs CML images
+ for (int i = 0; i < m_jupiterImages.size(); i++) {
+ m_jupiterItems.append(m_jupiterScene->addPixmap(m_jupiterImages[i]));
+ }
+ m_jupiterItems[0]->setPos(0, 0);
+ m_jupiterItems[1]->setPos(0, 0);
+ m_jupiterItems[1]->setVisible(false);
+
+ // Legend
+
+ int legendSpacing = 10;
+ int legendYTop = (m_jupiterImages[0].height() - m_jupiterImages[2].height()) / 2;
+ int legendYBottom = legendYTop + m_jupiterImages[2].height();
+ int legendXLeft = m_jupiterImages[0].width() + legendSpacing;
+ int legendXRight = legendXLeft + m_jupiterImages[2].width();
+ m_jupiterItems[2]->setPos(legendXLeft, legendYTop);
+ m_ioLegendLabels = createJupiterLegendLabels(legendXRight, legendYBottom, 10, 60);
+ m_ganymedeLegendLabels = createJupiterLegendLabels(legendXRight, legendYBottom, 5, 15);
+
+ QGraphicsTextItem *legendLabel = m_jupiterScene->addText("Probability (%)");
+ legendLabel->setTransformOriginPoint(legendLabel->boundingRect().center());
+ legendLabel->setRotation(-90);
+ legendLabel->setPos(legendXRight, m_jupiterImages[0].height() / 2);
+
+ // Y axis labels
+
+ int phaseMax = 360;
+ int phaseStep = 45;
+ int phaseSteps = phaseMax / phaseStep;
+ int yStep = m_jupiterImages[0].height() / phaseSteps;
+ int maxYLabelWidth = 0;
+
+ for (int i = 0; i <= phaseSteps; i++)
+ {
+ QString phaseStr = QString::number(i * phaseStep);
+ QGraphicsTextItem *yLabel = m_jupiterScene->addText(phaseStr);
+ int yw = yLabel->boundingRect().width();
+ maxYLabelWidth = std::max(maxYLabelWidth, yw);
+ yLabel->setPos(-yw, m_jupiterImages[0].height() - i * yStep - yLabel->boundingRect().height() / 2);
+ }
+
+ QGraphicsTextItem *phaseLabel = m_jupiterScene->addText(QString("Phase (%1)").arg(QChar(0xb0)));
+ phaseLabel->setTransformOriginPoint(phaseLabel->boundingRect().center());
+ phaseLabel->setRotation(-90);
+ phaseLabel->setPos(-2 * phaseLabel->boundingRect().height() - maxYLabelWidth, m_jupiterImages[0].height() / 2);
+
+ // X axis labels
+
+ QGraphicsTextItem *cmlLabel = m_jupiterScene->addText(QString("CML (%1)").arg(QChar(0xb0)));
+ int cmlLabelWidth = cmlLabel->boundingRect().width();
+ cmlLabel->setPos(m_jupiterImages[0].width() / 2 - cmlLabelWidth / 2, m_jupiterImages[0].height() + cmlLabel->boundingRect().height());
+
+ int cmlMax = 360;
+ int cmlStep = 45;
+ int cmlSteps = cmlMax / cmlStep;
+ int xStep = m_jupiterImages[0].width() / cmlSteps;
+
+ for (int i = 0; i <= cmlSteps; i++)
+ {
+ QString cmlStr = QString::number(i * cmlStep);
+ QGraphicsTextItem *xLabel = m_jupiterScene->addText(cmlStr);
+ int xw = xLabel->boundingRect().width();
+ xLabel->setPos(i * xStep - xw / 2, m_jupiterImages[0].height());
+ }
+
+ // Moon images
+
+ const qreal moonScale = 0.1;
+
+ QPixmap *ioPixmap = getPlanetPixmap("io");
+ m_ioItem = m_jupiterScene->addPixmap(*ioPixmap);
+ m_ioItem->setOffset(-ioPixmap->width() / 2, -ioPixmap->height() / 2);
+ m_ioItem->setScale(moonScale);
+ m_ioItem->setZValue(2);
+
+ QPixmap *ganymedePixmap = getPlanetPixmap("ganymede");
+ m_ganymedeItem = m_jupiterScene->addPixmap(*ganymedePixmap);
+ m_ganymedeItem->setOffset(-ganymedePixmap->width() / 2, -ganymedePixmap->height() / 2);
+ m_ganymedeItem->setScale(moonScale);
+ m_ganymedeItem->setZValue(2);
+
+ int yLabelAreaWidth = maxYLabelWidth + phaseLabel->boundingRect().height();
+ int topMargin = (ioPixmap->height() * moonScale) / 2;
+
+ m_jupiterRect.setRect(
+ -yLabelAreaWidth,
+ -topMargin,
+ yLabelAreaWidth + m_jupiterImages[0].width() + legendSpacing + m_jupiterImages[2].width(),
+ m_jupiterImages[0].height() + 2 * cmlLabel->boundingRect().height() + topMargin
+ );
+}
+
+void StarTrackerGUI::updateJupiterMoonPosition(double cml, double ioPhase, double ganymedePhase)
+{
+ int x = (cml / 360.0) * m_jupiterImages[0].width();
+ int y = m_jupiterImages[0].height() - ((ioPhase / 360.0) * m_jupiterImages[0].height());
+ m_ioItem->setPos(x, y);
+
+ y = m_jupiterImages[0].height() - ((ganymedePhase / 360.0) * m_jupiterImages[0].height());
+ m_ganymedeItem->setPos(x, y);
+}
+
+void StarTrackerGUI::updateJupiterMoonPositions(const StarTrackerReport::MsgReportJupiterData& report)
+{
+ QList jupiterData = report.getJupiterData();
+ QList moonData = report.getMoonData();
+
+ qDeleteAll(m_jupiterLines);
+ m_jupiterLines.clear();
+ qDeleteAll(m_jupiterTexts);
+ m_jupiterTexts.clear();
+
+ for (int i = 0; i < jupiterData.size() - 1; i++)
+ {
+ QString hour = QString::number(jupiterData[i].m_dateTime.time().hour());
+ double cml1 = moonData[i].m_cml;
+ double cml2 = moonData[i+1].m_cml;
+ double phase1 = moonData[i].m_phase;
+ double phase2 = moonData[i+1].m_phase;
+
+ const int w = m_jupiterImages[0].width();
+ const int h = m_jupiterImages[0].height();
+ int x1 = (cml1 / 360.0) * w;
+ int y1 = h - ((phase1 / 360.0) * h);
+ int x2 = (cml2 / 360.0) * w;
+ int y2 = h - ((phase2 / 360.0) * h);
+ int x2n, y2n;
+
+ bool interpolated = false;
+ if ((x2 < x1) && (y2 > y1))
+ {
+ x2n = 0;
+ y2n = h - Interpolation::interpolate(x1 - w, y1, x2, h - y2, 0);
+ x2 = Interpolation::interpolate(y1, x1, y2 - h, x2 + w, 0);
+ y2 = 0;
+ interpolated = true;
+ }
+ else if (x2 < x1)
+ {
+ y2 = Interpolation::interpolate(x1, y1, x2 + w, y2, w);
+ x2 = w;
+ interpolated = true;
+ x2n = 0;
+ y2n = y2;
+ }
+ else if (y2 > y1)
+ {
+ x2 = Interpolation::interpolate(y1, x1, y2 - h, x2, 0);
+ y2 = 0;
+ interpolated = true;
+ x2n = x2;
+ y2n = h;
+ }
+
+ QGraphicsLineItem *line = m_jupiterScene->addLine(x1, y1, x2, y2, QPen(Qt::white));
+ m_jupiterLines.append(line);
+
+ if (interpolated && (i < jupiterData.size() - 1))
+ {
+ int x3 = (cml2 / 360.0) * w;
+ int y3 = h - ((phase2 / 360.0) * h);
+
+ x2 = x2n;
+ y2 = y2n;
+
+ line = m_jupiterScene->addLine(x2, y2, x3, y3, QPen(Qt::white));
+ m_jupiterLines.append(line);
+ }
+
+ QGraphicsLineItem *dash = m_jupiterScene->addLine(x1, y1, x1, y1 + 3, QPen(Qt::white));
+ m_jupiterLines.append(dash);
+
+ QGraphicsTextItem *text = m_jupiterScene->addText(hour);
+ const int tw = text->boundingRect().width();
+ text->setPos(x1 - tw / 2, y1);
+ m_jupiterTexts.append(text);
+ }
+
+ if (jupiterData.size() > 0)
+ {
+ int i = jupiterData.size() - 1;
+
+ QString hour = QString::number(jupiterData[i].m_dateTime.time().hour());
+ double cml1 = moonData[i].m_cml;
+ double phase1 = moonData[i].m_phase;
+
+ int x1 = (cml1 / 360.0) * m_jupiterImages[0].width();
+ int y1 = m_jupiterImages[0].height() - ((phase1 / 360.0) * m_jupiterImages[0].height());
+
+ QGraphicsLineItem *dash = m_jupiterScene->addLine(x1, y1, x1, y1 + 3, QPen(Qt::white));
+ m_jupiterLines.append(dash);
+
+ QGraphicsTextItem *text = m_jupiterScene->addText(hour);
+ int w = text->boundingRect().width();
+ text->setPos(x1 - w / 2, y1);
+ m_jupiterTexts.append(text);
+ }
+
+}
+
+void StarTrackerGUI::plotJupiter()
+{
+ // Select which Moon to show
+ int imageIdx = ui->chartSubSelect->currentIndex();
+ bool plotIO = imageIdx == 0;
+ bool ployGanymede = imageIdx == 1;
+
+ ui->chart->setVisible(false);
+ ui->image->setVisible(true);
+ ui->night->setVisible(false);
+ ui->logScale->setVisible(false);
+ ui->drawSun->setVisible(false);
+ ui->drawMoon->setVisible(false);
+ ui->darkTheme->setVisible(false);
+ ui->zoomIn->setVisible(false);
+ ui->zoomOut->setVisible(false);
+ ui->addAnimationFrame->setVisible(false);
+ ui->clearAnimation->setVisible(false);
+ ui->saveAnimation->setVisible(false);
+ ui->jupiterElevationLabel->setVisible(true);
+ ui->jupiterElevation->setVisible(true);
+ ui->cmlLabel->setVisible(true);
+ ui->cml->setVisible(true);
+ ui->ioPhaseLabel->setVisible(plotIO);
+ ui->ioPhase->setVisible(plotIO);
+ ui->ganymedePhaseLabel->setVisible(ployGanymede);
+ ui->ganymedePhase->setVisible(ployGanymede);
+
+ if (ui->image->scene() != m_jupiterScene)
+ {
+ ui->image->setScene(m_jupiterScene);
+ ui->image->setDragMode(QGraphicsView::NoDrag);
+ }
+
+ if (m_zoom)
+ {
+ delete m_zoom;
+ m_zoom = nullptr;
+ }
+
+ m_jupiterItems[0]->setVisible(plotIO);
+ m_jupiterItems[1]->setVisible(ployGanymede);
+
+ m_ioItem->setVisible(plotIO);
+ m_ganymedeItem->setVisible(ployGanymede);
+ for (auto& item : m_ioLegendLabels) {
+ item->setVisible(plotIO);
+ }
+ for (auto& item : m_ganymedeLegendLabels) {
+ item->setVisible(ployGanymede);
+ }
+
+ // Expand to available view
+ ui->image->fitInView(m_jupiterRect, Qt::KeepAspectRatio);
+}
+
+void StarTrackerGUI::resizeEvent(QResizeEvent *event)
+{
+ if (ui->chartSelect->currentIndex() == StarTrackerSettings::CHART_JUPITER) {
+ ui->image->fitInView(m_jupiterRect, Qt::KeepAspectRatio);
+ }
+ FeatureGUI::resizeEvent(event);
+}
+
void StarTrackerGUI::plotSkyTemperatureChart()
{
ui->chart->setVisible(true);
ui->image->setVisible(false);
+ ui->night->setVisible(false);
+ ui->logScale->setVisible(false);
ui->drawSun->setVisible(true);
ui->drawMoon->setVisible(true);
ui->darkTheme->setVisible(false);
@@ -1346,6 +2181,14 @@ void StarTrackerGUI::plotSkyTemperatureChart()
ui->addAnimationFrame->setVisible(false);
ui->clearAnimation->setVisible(false);
ui->saveAnimation->setVisible(false);
+ ui->jupiterElevationLabel->setVisible(false);
+ ui->jupiterElevation->setVisible(false);
+ ui->cmlLabel->setVisible(false);
+ ui->cml->setVisible(false);
+ ui->ioPhaseLabel->setVisible(false);
+ ui->ioPhase->setVisible(false);
+ ui->ganymedePhaseLabel->setVisible(false);
+ ui->ganymedePhase->setVisible(false);
bool galactic = (ui->chartSubSelect->currentIndex() & 1) == 1;
@@ -1601,6 +2444,8 @@ void StarTrackerGUI::plotElevationLineChart()
{
ui->chart->setVisible(true);
ui->image->setVisible(false);
+ ui->night->setVisible(true);
+ ui->logScale->setVisible(false);
ui->drawSun->setVisible(false);
ui->drawMoon->setVisible(false);
ui->darkTheme->setVisible(true);
@@ -1609,6 +2454,14 @@ void StarTrackerGUI::plotElevationLineChart()
ui->addAnimationFrame->setVisible(false);
ui->clearAnimation->setVisible(false);
ui->saveAnimation->setVisible(false);
+ ui->jupiterElevationLabel->setVisible(false);
+ ui->jupiterElevation->setVisible(false);
+ ui->cmlLabel->setVisible(false);
+ ui->cml->setVisible(false);
+ ui->ioPhaseLabel->setVisible(false);
+ ui->ioPhase->setVisible(false);
+ ui->ganymedePhaseLabel->setVisible(false);
+ ui->ganymedePhase->setVisible(false);
QChart *oldChart = m_azElLineChart;
@@ -1623,8 +2476,6 @@ void StarTrackerGUI::plotElevationLineChart()
m_azElLineChart->layout()->setContentsMargins(0, 0, 0, 0);
m_azElLineChart->setMargins(QMargins(1, 1, 1, 1));
- double maxElevation = -90.0;
-
QLineSeries *elSeries = new QLineSeries();
QList azSeriesList;
QLineSeries *azSeries = new QLineSeries();
@@ -1632,66 +2483,55 @@ void StarTrackerGUI::plotElevationLineChart()
QPen pen(getSeriesColor(1), 2, Qt::SolidLine);
azSeries->setPen(pen);
- QDateTime dt;
+ double maxElevation = -90.0;
+ double prevAz;
+ QDateTime startTime;
+ QDateTime endTime;
+
+ if (m_settings.m_target == m_azElVsTimeTarget)
+ {
+ if (!m_dateTimes.isEmpty()) {
+ startTime = m_dateTimes[0];
+ }
+
+ for (int i = 0; i < m_dateTimes.size(); i++)
+ {
+ QDateTime dt = m_dateTimes[i];
+
+ if (m_elevations[i] > maxElevation) {
+ maxElevation = m_elevations[i];
+ }
+
+ if (i == 0) {
+ prevAz = m_azimuths[i];
+ }
+ if (((prevAz >= 270) && (m_azimuths[i] < 90)) || ((prevAz < 90) && (m_azimuths[i] >= 270)))
+ {
+ azSeries = new QLineSeries();
+ azSeriesList.append(azSeries);
+ azSeries->setPen(pen);
+ }
+
+ azSeries->append(dt.toMSecsSinceEpoch(), m_azimuths[i]);
+ elSeries->append(dt.toMSecsSinceEpoch(), m_elevations[i]);
+
+ endTime = dt;
+ prevAz = m_azimuths[i];
+ }
+ }
+
QDateTime currentTime;
- if (m_settings.m_dateTime.isEmpty()) {
- currentTime = QDateTime::currentDateTime();
- } else {
- currentTime = QDateTime::fromString(m_settings.m_dateTime, Qt::ISODateWithMs);
- }
- dt = currentTime;
-
- dt.setTime(QTime(0,0));
- QDateTime startTime = dt;
- QDateTime endTime = dt;
- double prevAz;
- int timestep = 10*60;
-
- for (int step = 0; step <= 24*60*60/timestep; step++)
+ if (m_settings.m_dateTime.isEmpty())
{
- AzAlt aa;
- RADec rd;
-
- // Calculate elevation of desired object
- if (m_settings.m_target == "Sun")
- {
- Astronomy::sunPosition(aa, rd, m_settings.m_latitude, m_settings.m_longitude, dt);
+ currentTime = QDateTime::currentDateTime();
+ }
+ else
+ {
+ currentTime = QDateTime::fromString(m_settings.m_dateTime, Qt::ISODateWithMs);
+ if (m_settings.m_utc) {
+ currentTime.setTimeZone(QTimeZone::utc());
}
- else if (m_settings.m_target == "Moon")
- {
- Astronomy::moonPosition(aa, rd, m_settings.m_latitude, m_settings.m_longitude, dt);
- }
- else
- {
- rd.ra = Units::raToDecimal(m_settings.m_ra);
- rd.dec = Units::decToDecimal(m_settings.m_dec);
- aa = Astronomy::raDecToAzAlt(rd, m_settings.m_latitude, m_settings.m_longitude, dt, !m_settings.m_jnow);
- }
-
- if (aa.alt > maxElevation) {
- maxElevation = aa.alt;
- }
-
- // Skip adjusting for refraction, as it's too slow to calculate using Astronomy::refractionPAL
- // in this loop, and doesn't typically make a visible difference in the chart
-
- if (step == 0) {
- prevAz = aa.az;
- }
-
- if (((prevAz >= 270) && (aa.az < 90)) || ((prevAz < 90) && (aa.az >= 270)))
- {
- azSeries = new QLineSeries();
- azSeriesList.append(azSeries);
- azSeries->setPen(pen);
- }
-
- elSeries->append(dt.toMSecsSinceEpoch(), aa.alt);
- azSeries->append(dt.toMSecsSinceEpoch(), aa.az);
- endTime = dt;
- prevAz = aa.az;
- dt = dt.addSecs(timestep); // addSecs accounts for daylight savings jumps
}
m_azElLineChart->addAxis(xAxis, Qt::AlignBottom);
@@ -1730,7 +2570,11 @@ void StarTrackerGUI::plotElevationLineChart()
elSeries->attachAxis(xAxis);
elSeries->attachAxis(yLeftAxis);
- xAxis->setTitleText(QString("%1 %2").arg(startTime.date().toString()).arg(startTime.timeZoneAbbreviation()));
+ if (m_settings.m_night) {
+ xAxis->setTitleText(QString("%1 - %2 %3").arg(startTime.date().toString()).arg(startTime.date().addDays(1).toString()).arg(startTime.timeZoneAbbreviation()));
+ } else {
+ xAxis->setTitleText(QString("%1 %2").arg(startTime.date().toString()).arg(startTime.timeZoneAbbreviation()));
+ }
xAxis->setFormat("hh");
xAxis->setTickCount(7);
xAxis->setRange(startTime, endTime);
@@ -1739,7 +2583,9 @@ void StarTrackerGUI::plotElevationLineChart()
yRightAxis->setRange(0.0, 360.0);
yRightAxis->setTitleText(QString("Azimuth (%1)").arg(QChar(0xb0)));
- if (maxElevation < 0) {
+ if ((m_settings.m_target != m_azElVsTimeTarget) || m_dateTimes.isEmpty()) {
+ m_azElLineChart->setTitle("Waiting for data");
+ } else if (maxElevation < 0) {
m_azElLineChart->setTitle("Not visible from this latitude");
} else if (m_settings.m_target.contains("SatelliteTracker")) {
m_azElLineChart->setTitle("See Satellite Tracker for chart that accounts for satellite's movement");
@@ -1776,6 +2622,8 @@ void StarTrackerGUI::plotElevationPolarChart()
{
ui->chart->setVisible(true);
ui->image->setVisible(false);
+ ui->night->setVisible(true);
+ ui->logScale->setVisible(false);
ui->drawSun->setVisible(false);
ui->drawMoon->setVisible(false);
ui->darkTheme->setVisible(true);
@@ -1784,6 +2632,14 @@ void StarTrackerGUI::plotElevationPolarChart()
ui->addAnimationFrame->setVisible(false);
ui->clearAnimation->setVisible(false);
ui->saveAnimation->setVisible(false);
+ ui->jupiterElevationLabel->setVisible(false);
+ ui->jupiterElevation->setVisible(false);
+ ui->cmlLabel->setVisible(false);
+ ui->cml->setVisible(false);
+ ui->ioPhaseLabel->setVisible(false);
+ ui->ioPhase->setVisible(false);
+ ui->ganymedePhaseLabel->setVisible(false);
+ ui->ganymedePhase->setVisible(false);
QChart *oldChart = m_azElPolarChart;
@@ -1824,118 +2680,98 @@ void StarTrackerGUI::plotElevationPolarChart()
}
dt = currentTime;
dt.setTime(QTime(0,0));
- QDateTime startTime = dt;
QDateTime endTime = dt;
QDateTime riseTime;
QDateTime setTime;
int riseIdx = -1;
int setIdx = -1;
int idx = 0;
- int timestep = 10*60; // Rise/set times accurate to nearest 10 minutes
double prevAlt;
- for (int step = 0; step <= 24*60*60/timestep; step++)
+ if (m_settings.m_target == m_azElVsTimeTarget)
{
- AzAlt aa;
- RADec rd;
-
- // Calculate elevation of desired object
- if (m_settings.m_target == "Sun")
+ for (int i = 0; i < m_dateTimes.size(); i++)
{
- Astronomy::sunPosition(aa, rd, m_settings.m_latitude, m_settings.m_longitude, dt);
+ QDateTime dt = m_dateTimes[i];
+
+ if (m_elevations[i] > maxElevation) {
+ maxElevation = m_elevations[i];
+ }
+
+ if (i == 0) {
+ prevAlt = m_elevations[i];
+ }
+
+ // We can have set before rise in a day, if the object starts > 0
+ if ((m_elevations[i] >= 0.0) && (prevAlt < 0.0))
+ {
+ riseTime = dt;
+ riseIdx = idx;
+ }
+
+ if (( m_elevations[i] < 0.0) && (prevAlt >= 0.0))
+ {
+ setTime = endTime;
+ setIdx = idx;
+ }
+
+ polarSeries->append(m_azimuths[i], 90 - m_elevations[i]);
+ idx++;
+ endTime = dt;
+ prevAlt = m_elevations[i];
}
- else if (m_settings.m_target == "Moon")
+
+ // Polar charts can't handle points that are more than 180 degrees apart, so
+ // we need to split passes that cross from 359 -> 0 degrees (or the reverse)
+ QList series;
+ series.append(new QLineSeries());
+ QLineSeries *s = series.first();
+ QPen pen(getSeriesColor(0), 2, Qt::SolidLine);
+ s->setPen(pen);
+
+ qreal prevAz = polarSeries->at(0).x();
+ qreal prevEl = polarSeries->at(0).y();
+
+ for (int i = 1; i < polarSeries->count(); i++)
{
- Astronomy::moonPosition(aa, rd, m_settings.m_latitude, m_settings.m_longitude, dt);
+ qreal az = polarSeries->at(i).x();
+ qreal el = polarSeries->at(i).y();
+
+ if ((prevAz > 270.0) && (az <= 90.0))
+ {
+ double elMid = Interpolation::interpolate(prevAz, prevEl, az+360.0, el, 360.0);
+ s->append(360.0, elMid);
+ series.append(new QLineSeries());
+ s = series.last();
+ s->setPen(pen);
+ s->append(0.0, elMid);
+ s->append(az, el);
+ }
+ else if ((prevAz <= 90.0) && (az > 270.0))
+ {
+ double elMid = Interpolation::interpolate(prevAz, prevEl, az-360.0, el, 0.0);
+ s->append(0.0, elMid);
+ series.append(new QLineSeries());
+ s = series.last();
+ s->setPen(pen);
+ s->append(360.0, elMid);
+ s->append(az, el);
+ }
+ else
+ {
+ s->append(polarSeries->at(i));
+ }
+
+ prevAz = az;
+ prevEl = el;
}
- else
+
+ for (int i = 0; i < series.length(); i++)
{
- rd.ra = Units::raToDecimal(m_settings.m_ra);
- rd.dec = Units::decToDecimal(m_settings.m_dec);
- aa = Astronomy::raDecToAzAlt(rd, m_settings.m_latitude, m_settings.m_longitude, dt, !m_settings.m_jnow);
+ m_azElPolarChart->addSeries(series[i]);
+ series[i]->attachAxis(angularAxis);
+ series[i]->attachAxis(radialAxis);
}
-
- if (aa.alt > maxElevation) {
- maxElevation = aa.alt;
- }
-
- // Skip adjusting for refraction, as it's too slow to calculate using Astronomy::refractionPAL
- // in this loop, and doesn't typically make a visible difference in the chart
-
- if (idx == 0) {
- prevAlt = aa.alt;
- }
-
- // We can have set before rise in a day, if the object starts > 0
- if ((aa.alt >= 0.0) && (prevAlt < 0.0))
- {
- riseTime = dt;
- riseIdx = idx;
- }
-
- if ((aa.alt < 0.0) && (prevAlt >= 0.0))
- {
- setTime = endTime;
- setIdx = idx;
- }
-
- polarSeries->append(aa.az, 90 - aa.alt);
- idx++;
- endTime = dt;
- prevAlt = aa.alt;
- dt = dt.addSecs(timestep); // addSecs accounts for daylight savings jumps
- }
-
- // Polar charts can't handle points that are more than 180 degrees apart, so
- // we need to split passes that cross from 359 -> 0 degrees (or the reverse)
- QList series;
- series.append(new QLineSeries());
- QLineSeries *s = series.first();
- QPen pen(getSeriesColor(0), 2, Qt::SolidLine);
- s->setPen(pen);
-
- qreal prevAz = polarSeries->at(0).x();
- qreal prevEl = polarSeries->at(0).y();
-
- for (int i = 1; i < polarSeries->count(); i++)
- {
- qreal az = polarSeries->at(i).x();
- qreal el = polarSeries->at(i).y();
-
- if ((prevAz > 270.0) && (az <= 90.0))
- {
- double elMid = Interpolation::interpolate(prevAz, prevEl, az+360.0, el, 360.0);
- s->append(360.0, elMid);
- series.append(new QLineSeries());
- s = series.last();
- s->setPen(pen);
- s->append(0.0, elMid);
- s->append(az, el);
- }
- else if ((prevAz <= 90.0) && (az > 270.0))
- {
- double elMid = Interpolation::interpolate(prevAz, prevEl, az-360.0, el, 0.0);
- s->append(0.0, elMid);
- series.append(new QLineSeries());
- s = series.last();
- s->setPen(pen);
- s->append(360.0, elMid);
- s->append(az, el);
- }
- else
- {
- s->append(polarSeries->at(i));
- }
-
- prevAz = az;
- prevEl = el;
- }
-
- for (int i = 0; i < series.length(); i++)
- {
- m_azElPolarChart->addSeries(series[i]);
- series[i]->attachAxis(angularAxis);
- series[i]->attachAxis(radialAxis);
}
if (m_settings.m_drawRotators != StarTrackerSettings::NO_ROTATORS)
@@ -2072,7 +2908,9 @@ void StarTrackerGUI::plotElevationPolarChart()
posSeries->attachAxis(radialAxis);
}
- if (maxElevation < 0) {
+ if ((m_settings.m_target != m_azElVsTimeTarget) || m_dateTimes.isEmpty()) {
+ m_azElPolarChart->setTitle("Waiting for data");
+ } else if (maxElevation < 0) {
m_azElPolarChart->setTitle("Not visible from this latitude");
} else if (m_settings.m_target.contains("SatelliteTracker")) {
m_azElPolarChart->setTitle("See Satellite Tracker for chart that accounts for satellite's movement");
@@ -2095,7 +2933,7 @@ void StarTrackerGUI::on_viewOnMap_clicked()
void StarTrackerGUI::updateChartSubSelect()
{
- if (ui->chartSelect->currentIndex() == 2)
+ if (ui->chartSelect->currentIndex() == StarTrackerSettings::CHART_SKY_TEMPERATURE)
{
ui->chartSubSelect->setItemText(6, QString("%1 MHz %2%3 Equatorial")
.arg((int)std::round(m_settings.m_frequency/1e6))
@@ -2113,6 +2951,8 @@ void StarTrackerGUI::on_chartSelect_currentIndexChanged(int index)
bool oldState = ui->chartSubSelect->blockSignals(true);
ui->chartSubSelect->clear();
+ QString currentText = ui->chartSubSelect->currentText();
+
if (index == 0)
{
ui->chartSubSelect->addItem("Az/El vs time");
@@ -2136,14 +2976,42 @@ void StarTrackerGUI::on_chartSelect_currentIndexChanged(int index)
ui->chartSubSelect->addItem("Milky Way");
ui->chartSubSelect->addItem("Milky Way annotated");
}
+ else if (index == 4)
+ {
+ for (const auto& body : m_settings.m_solarSystemBodies) {
+ ui->chartSubSelect->addItem(body);
+ }
+ ui->chartSubSelect->addItem("-");
+ }
+ else if (index == 5)
+ {
+ ui->chartSubSelect->addItem("Io phase vs CML");
+ ui->chartSubSelect->addItem("Ganymede phase vs CML");
+ }
+
+ int idx = ui->chartSubSelect->findText(currentText);
+ if (idx >= 0) {
+ ui->chartSubSelect->setCurrentIndex(idx);
+ }
ui->chartSubSelect->blockSignals(oldState);
plotChart();
+
+ m_settings.m_chartSelect = (StarTrackerSettings::ChartSelect) index;
+ m_settingsKeys.append("chartSelect");
+ m_settings.m_chartSubSelect = ui->chartSubSelect->currentIndex();
+ m_settingsKeys.append("chartSubSelect");
+ applySettings();
}
void StarTrackerGUI::on_chartSubSelect_currentIndexChanged(int index)
{
(void) index;
+
+ m_settings.m_chartSubSelect = index;
+ m_settingsKeys.append("chartSubSelect");
+ applySettings();
+
plotChart();
}
@@ -2393,6 +3261,22 @@ void StarTrackerGUI::on_darkTheme_clicked(bool checked)
applySettings();
}
+void StarTrackerGUI::on_night_clicked(bool checked)
+{
+ m_settings.m_night = checked;
+ plotChart();
+ m_settingsKeys.append("night");
+ applySettings();
+}
+
+void StarTrackerGUI::on_logScale_clicked(bool checked)
+{
+ m_settings.m_logScale = checked;
+ plotChart();
+ m_settingsKeys.append("logScale");
+ applySettings();
+}
+
void StarTrackerGUI::on_drawSun_clicked(bool checked)
{
m_settings.m_drawSunOnSkyTempChart = checked;
@@ -2413,8 +3297,80 @@ void StarTrackerGUI::downloadFinished(const QString& filename, bool success)
{
(void) filename;
- if (success) {
+ if (success && filename.endsWith("solar_flux.srd")) {
readSolarFlux();
+ } else if (!success) {
+ QMessageBox::warning(this, "Failed to download file", QString("Failed to download %1").arg(filename));
+ }
+}
+
+void StarTrackerGUI::majorBodiesUpdated(const QHash& bodies)
+{
+ m_jplBodies = QStringList();
+
+ QHashIterator itr(bodies);
+ while (itr.hasNext())
+ {
+ itr.next();
+ const JPLHorizons::BodyID& body = itr.value();
+ m_jplBodies.append(body.m_name);
+ }
+
+ m_jplBodies.sort();
+ updateTargetList();
+}
+
+void StarTrackerGUI::updateTargetList()
+{
+ bool block = ui->target->blockSignals(true);
+
+ ui->target->clear();
+
+ if (m_settings.m_targetSource == "SDRangel")
+ {
+ QStringList builtinTargets = {
+ "Sun",
+ "Moon",
+ "PSR B0329+54",
+ "PSR B0833-45",
+ "Sagittarius A",
+ "Cassiopeia A",
+ "Cygnus A",
+ "Taurus A (M1)",
+ "Virgo A (M87)",
+ "Custom RA/Dec",
+ "Custom Az/El",
+ "Custom l/b",
+ "S7",
+ "S8",
+ "S9"
+ };
+
+ for (const auto& target : builtinTargets) {
+ ui->target->addItem(target);
+ }
+ for (const auto& target : m_availableFeatures) {
+ ui->target->addItem(target);
+ }
+ }
+ else if (m_settings.m_targetSource == "SPICE")
+ {
+ for (const auto& target : m_spiceEphemerides.getTargets(m_settings.m_spiceEphemerides)) {
+ ui->target->addItem(target);
+ }
+ }
+ else if (m_settings.m_targetSource == "Horizons")
+ {
+ for (const auto& body : m_jplBodies) {
+ ui->target->addItem(body);
+ }
+ }
+
+ ui->target->blockSignals(block);
+
+ int index = ui->target->findText(m_settings.m_target, Qt::MatchFixedString); // Case insensitive
+ if (index >= 0) {
+ ui->target->setCurrentIndex(index);
}
}
@@ -2435,10 +3391,13 @@ void StarTrackerGUI::makeUIConnections()
QObject::connect(ui->galacticLongitude, &DMSSpinBox::valueChanged, this, &StarTrackerGUI::on_galacticLongitude_valueChanged);
QObject::connect(ui->frequency, qOverload(&QSpinBox::valueChanged), this, &StarTrackerGUI::on_frequency_valueChanged);
QObject::connect(ui->beamwidth, qOverload(&QDoubleSpinBox::valueChanged), this, &StarTrackerGUI::on_beamwidth_valueChanged);
- QObject::connect(ui->target, &QComboBox::currentTextChanged, this, &StarTrackerGUI::on_target_currentTextChanged);
+ QObject::connect(ui->target, qOverload(&QComboBox::currentIndexChanged), this, &StarTrackerGUI::on_target_currentIndexChanged);
+ QObject::connect(ui->targetSource, qOverload(&QComboBox::currentIndexChanged), this, &StarTrackerGUI::on_targetSource_currentIndexChanged);
QObject::connect(ui->displaySettings, &QToolButton::clicked, this, &StarTrackerGUI::on_displaySettings_clicked);
QObject::connect(ui->dateTimeSelect, &QComboBox::currentTextChanged, this, &StarTrackerGUI::on_dateTimeSelect_currentTextChanged);
QObject::connect(ui->dateTime, &WrappingDateTimeEdit::dateTimeChanged, this, &StarTrackerGUI::on_dateTime_dateTimeChanged);
+ QObject::connect(ui->utc, &QToolButton::clicked, this, &StarTrackerGUI::on_utc_clicked);
+ QObject::connect(ui->setTimeToNow, &QToolButton::clicked, this, &StarTrackerGUI::on_setTimeToNow_clicked);
QObject::connect(ui->viewOnMap, &QToolButton::clicked, this, &StarTrackerGUI::on_viewOnMap_clicked);
QObject::connect(ui->chartSelect, qOverload(&QComboBox::currentIndexChanged), this, &StarTrackerGUI::on_chartSelect_currentIndexChanged);
QObject::connect(ui->chartSubSelect, qOverload(&QComboBox::currentIndexChanged), this, &StarTrackerGUI::on_chartSubSelect_currentIndexChanged);
@@ -2449,7 +3408,8 @@ void StarTrackerGUI::makeUIConnections()
QObject::connect(ui->addAnimationFrame, &QToolButton::clicked, this, &StarTrackerGUI::on_addAnimationFrame_clicked);
QObject::connect(ui->clearAnimation, &QToolButton::clicked, this, &StarTrackerGUI::on_clearAnimation_clicked);
QObject::connect(ui->saveAnimation, &QToolButton::clicked, this, &StarTrackerGUI::on_saveAnimation_clicked);
+ QObject::connect(ui->logScale, &QToolButton::clicked, this, &StarTrackerGUI::on_logScale_clicked);
+ QObject::connect(ui->night, &QToolButton::clicked, this, &StarTrackerGUI::on_night_clicked);
QObject::connect(ui->drawSun, &QToolButton::clicked, this, &StarTrackerGUI::on_drawSun_clicked);
QObject::connect(ui->drawMoon, &QToolButton::clicked, this, &StarTrackerGUI::on_drawMoon_clicked);
}
-
diff --git a/plugins/feature/startracker/startrackergui.h b/plugins/feature/startracker/startrackergui.h
index 1a5b1d4d5..260f97e02 100644
--- a/plugins/feature/startracker/startrackergui.h
+++ b/plugins/feature/startracker/startrackergui.h
@@ -30,11 +30,14 @@
#include "feature/featuregui.h"
#include "util/messagequeue.h"
+#include "util/jplhorizons.h"
#include "gui/httpdownloadmanagergui.h"
#include "settings/rollupstate.h"
#include "availablechannelorfeature.h"
#include "startrackersettings.h"
+#include "startrackerreport.h"
+#include "spiceephemerides.h"
class PluginAPI;
class FeatureUISet;
@@ -109,6 +112,13 @@ private:
QNetworkAccessManager *m_networkManager;
QNetworkRequest m_networkRequest;
HttpDownloadManagerGUI m_dlm;
+ bool m_startAfterDownload;
+
+ JPLHorizons *m_jplHorizons;
+ QStringList m_jplBodies;
+ QStringList m_spiceTargets;
+ QStringList m_availableFeatures; // Names of SatelliteTrackers and SkyMaps for target selection
+ SpiceEphemerides m_spiceEphemerides;
// Solar flux plot
double m_solarFlux; // 10.7cm/2800MHz
@@ -119,12 +129,38 @@ private:
// Sky temperature
QList m_images;
+ GraphicsViewZoom* m_zoom;
+
// Galactic line of sight
QList m_milkyWayImages;
- GraphicsViewZoom* m_zoom;
QList m_milkyWayItems;
QGraphicsLineItem* m_lineOfSight;
QList m_lineOfSightMarkers;
+ QGraphicsScene *m_galacticLineOfSightScene;
+
+ // Solar system
+ QGraphicsScene *m_solarSystemScene;
+ struct SolarSystemItem {
+ QGraphicsPixmapItem *m_pixmapItem;
+ QGraphicsTextItem *m_textItem;
+ float m_scale;
+ };
+ QFont m_solarSystemLabelFont;
+ QFontMetrics m_solarSystemLabelFontMetrics;
+ QHash m_solarSystemItems;
+ QHash m_planetImages;
+
+ // Jupiter Phase/CML
+ QList m_jupiterImages;
+ QGraphicsScene *m_jupiterScene;
+ QList m_jupiterItems;
+ QGraphicsPixmapItem *m_ioItem;
+ QGraphicsPixmapItem *m_ganymedeItem;
+ QList m_ioLegendLabels;
+ QList m_ganymedeLegendLabels;
+ QList m_jupiterLines;
+ QList m_jupiterTexts;
+ QRect m_jupiterRect;
// Images that are part of the animation
QList m_animationImages;
@@ -135,6 +171,12 @@ private:
double m_moonRA;
double m_moonDec;
+ // Data for chart from worker
+ QString m_azElVsTimeTarget;
+ QList m_azimuths;
+ QList m_elevations;
+ QList m_dateTimes;
+
explicit StarTrackerGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent = nullptr);
virtual ~StarTrackerGUI();
@@ -155,6 +197,17 @@ private:
void plotSolarFluxChart();
void plotGalacticLineOfSight();
void createGalacticLineOfSightScene();
+ void plotSolarSystem();
+ void centerOnSolarSystemBody();
+ void plotJupiter();
+ void updateJupiterMoonPosition(double cml, double ioPhase, double ganymedePhase);
+ void updateJupiterMoonPositions(const StarTrackerReport::MsgReportJupiterData& report);
+ void updateSolarSystemPositions(const QStringList &names, const QList &positions);
+ void scaleSolarSystemItems();
+ void createSolarSystemScene();
+ QPixmap *getPlanetPixmap(const QString& name);
+ QList createJupiterLegendLabels(int legendXRight, int legendYBottom, int legendStep, int legendMax);
+ void createJupiterScene();
void plotGalacticMarker(LoSMarker* marker);
void plotChart();
void removeAllAxes();
@@ -170,6 +223,9 @@ private:
void makeUIConnections();
void limitAzElRange(double& azimuth, double& elevation) const;
void updateFeatureList(const AvailableChannelOrFeatureList& features);
+ void updateTargetList();
+ bool checkSPICEEphemerides();
+ void downloadSPICEEphemerides();
private slots:
void onMenuDialogCalled(const QPoint &p);
@@ -190,10 +246,14 @@ private slots:
void on_galacticLongitude_valueChanged(double value);
void on_frequency_valueChanged(int value);
void on_beamwidth_valueChanged(double value);
- void on_target_currentTextChanged(const QString &text);
+ void on_target_editingFinished();
+ void on_target_currentIndexChanged(int index);
+ void on_targetSource_currentIndexChanged(int index);
void on_displaySettings_clicked();
void on_dateTimeSelect_currentTextChanged(const QString &text);
void on_dateTime_dateTimeChanged(const QDateTime &datetime);
+ void on_utc_clicked(bool checked=false);
+ void on_setTimeToNow_clicked(bool checked=false);
void updateStatus();
void on_viewOnMap_clicked();
void on_chartSelect_currentIndexChanged(int index);
@@ -202,16 +262,23 @@ private slots:
void autoUpdateSolarFlux();
void on_downloadSolarFlux_clicked();
void on_darkTheme_clicked(bool checked);
+ void on_night_clicked(bool checked=false);
void on_zoomIn_clicked();
void on_zoomOut_clicked();
+ void zoomed();
void on_addAnimationFrame_clicked();
void on_clearAnimation_clicked();
void on_saveAnimation_clicked();
+ void on_logScale_clicked(bool checked);
void on_drawSun_clicked(bool checked);
void on_drawMoon_clicked(bool checked);
void networkManagerFinished(QNetworkReply *reply);
void downloadFinished(const QString& filename, bool success);
+ void spiceDownloadsComplete();
+ void majorBodiesUpdated(const QHash& bodies);
+
+protected:
+ void resizeEvent(QResizeEvent *event) override;
};
-
#endif // INCLUDE_FEATURE_STARTRACKERGUI_H_
diff --git a/plugins/feature/startracker/startrackergui.ui b/plugins/feature/startracker/startrackergui.ui
index 933b27733..9b4b1c4c0 100644
--- a/plugins/feature/startracker/startrackergui.ui
+++ b/plugins/feature/startracker/startrackergui.ui
@@ -67,48 +67,21 @@
-
-
-
-
-
-
- Liberation Sans
- 9
- true
-
-
-
- l
-
-
-
- -
-
+
-
+
- Azimuth in degrees to the target from the observation point
-
-
-
- -
-
-
- Time
-
-
-
- -
-
-
- Offset in degrees to add to calculated target elevation
-
-
- 3
-
-
- -180.000000000000000
-
-
- 180.000000000000000
+ Select time to calculate target's position at
+
-
+
+ Now
+
+
+ -
+
+ Custom
+
+
-
@@ -121,17 +94,63 @@
- -
-
+
-
+
- Elevation
+ Solar Flux
- -
-
+
-
+
+
+ Declination of the target object
+
+This can be specified as a decimal (E.g. 34.23) or in degrees, minutes and seconds (E.g. 34d12m10.2s, 34d12'10.2" 34 12 10.2)
+
- Dec
+ -90d59'59.59"
+
+
+
+ -
+
+
+ Galactic longitude in degrees (positive Eastward from galactic center)
+
+
+
+ -
+
+
+ RA
+
+
+
+ -
+
+
+ Observation frequency (MHz)
+
+
+ 50
+
+
+ 100000
+
+
+
+ -
+
+
+ Time
+
+
+
+ -
+
+
+ Longitude
@@ -154,10 +173,82 @@
- -
-
+
-
+
- Longitude
+ Dec
+
+
+
+ -
+
+
+ Longitude in decimal degrees (East positive) of observation point / antenna location
+
+
+ 6
+
+
+ -180.000000000000000
+
+
+ 180.000000000000000
+
+
+ -180.000000000000000
+
+
+
+ -
+
+
+ Target
+
+
+
+ -
+
+
+ Galactic latitude in degrees
+
+
+
+ -
+
+
+ Azimuth
+
+
+
+ -
+
+
+ Offset in degrees to add to calculated target elevation
+
+
+ 3
+
+
+ -180.000000000000000
+
+
+ 180.000000000000000
+
+
+
+ -
+
+
+ Offset in degrees to added to calculated target azimuth
+
+
+ 3
+
+
+ -360.000000000000000
+
+
+ 360.000000000000000
@@ -167,7 +258,7 @@
- 150
+ 180
0
@@ -257,7 +348,7 @@
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
@@ -267,15 +358,30 @@
+ -
+
+
+ Source of targets
+
+
-
+
+ SDRangel
+
+
+ -
+
+ SPICE
+
+
+ -
+
+ Horizons
+
+
+
+
- -
-
-
- Solar Flux
-
-
-
-
@@ -290,34 +396,45 @@
- -
-
-
- Azimuth
-
-
-
- -
-
+
-
+
- Elevation in degrees to the target from the observation point
+ Azimuth in degrees to the target from the observation point
- -
-
-
- Solar flux density
-
-
- true
-
-
-
- -
-
+
-
+
- Target
+ Beamwidth
+
+
+
+ -
+
+
+ El Offset
+
+
+
+ -
+
+
+ Az Offset
+
+
+
+ -
+
+
+
+ Liberation Sans
+ 9
+ true
+
+
+
+ l
@@ -333,6 +450,49 @@ This can be specified as a decimal (E.g. 12.23) or in hours, minutes and seconds
+ -
+
+
+ Latitude in decimal degrees (North positive) of observation point / antenna location
+
+
+ 6
+
+
+ -90.000000000000000
+
+
+ 90.000000000000000
+
+
+ -90.000000000000000
+
+
+
+ -
+
+
+ Latitude
+
+
+
+ -
+
+
+ Elevation
+
+
+
+ -
+
+
+ Solar flux density
+
+
+ true
+
+
+
-
-
@@ -353,7 +513,7 @@ This can be specified as a decimal (E.g. 12.23) or in hours, minutes and seconds
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
@@ -435,164 +595,6 @@ This can be specified as a decimal (E.g. 12.23) or in hours, minutes and seconds
- -
-
-
- RA
-
-
-
- -
-
-
- Declination of the target object
-
-This can be specified as a decimal (E.g. 34.23) or in degrees, minutes and seconds (E.g. 34d12m10.2s, 34d12'10.2" 34 12 10.2)
-
-
- -90d59'59.59"
-
-
-
- -
-
-
- Offset in degrees to added to calculated target azimuth
-
-
- 3
-
-
- -360.000000000000000
-
-
- 360.000000000000000
-
-
-
- -
-
-
- Beamwidth
-
-
-
- -
-
-
- Latitude in decimal degrees (North positive) of observation point / antenna location
-
-
- 6
-
-
- -90.000000000000000
-
-
- 90.000000000000000
-
-
- -90.000000000000000
-
-
-
- -
-
-
- Galactic latitude in degrees
-
-
-
- -
-
-
- Observation frequency (MHz)
-
-
- 50
-
-
- 100000
-
-
-
- -
-
-
- Az Offset
-
-
-
- -
-
-
- Date and time to use when calculating target's position
-
-
- dd/MM/yyyy HH:mm:ss
-
-
- true
-
-
-
- -
-
-
- Longitude in decimal degrees (East positive) of observation point / antenna location
-
-
- 6
-
-
- -180.000000000000000
-
-
- 180.000000000000000
-
-
- -180.000000000000000
-
-
-
- -
-
-
- Latitude
-
-
-
- -
-
-
- Select time to calculate target's position at
-
-
-
-
- Now
-
-
- -
-
- Custom
-
-
-
-
- -
-
-
- El Offset
-
-
-
- -
-
-
- Galactic longitude in degrees (positive Eastward from galactic center)
-
-
-
-
@@ -607,6 +609,57 @@ This can be specified as a decimal (E.g. 34.23) or in degrees, minutes and secon
+ -
+
+
+ Elevation in degrees to the target from the observation point
+
+
+
+ -
+
+
-
+
+
+ Date and time to use when calculating target's position
+
+
+ dd/MM/yyyy HH:mm:ss
+
+
+ true
+
+
+
+ -
+
+
+ Check for UTC time. Uncheck for local time.
+
+
+ UTC
+
+
+ true
+
+
+
+ -
+
+
+ Set time to now
+
+
+
+
+
+
+ :/recycle.png:/recycle.png
+
+
+
+
+
@@ -635,21 +688,21 @@ This can be specified as a decimal (E.g. 34.23) or in degrees, minutes and secon
Plots
-
+
2
- 3
+ 2
- 3
+ 2
- 3
+ 2
- 3
+ 2
-
@@ -678,11 +731,39 @@ This can be specified as a decimal (E.g. 34.23) or in degrees, minutes and secon
Galactic line of sight
+ -
+
+ Solar system
+
+
+ -
+
+ Jupiter CML and Moon Phase
+
+
-
+ -
+
+
+ When checked, draws planetary orbits on log scale, rather than linear
+
+
+
+
+
+
+ :/linear.png
+ :/logarithmic.png:/linear.png
+
+
+ true
+
+
+
-
@@ -768,6 +849,24 @@ This can be specified as a decimal (E.g. 34.23) or in degrees, minutes and secon
+ -
+
+
+ Day / night. When unchecked, time runs from midnight to midnight. When checked, time runs from noon to noon.
+
+
+
+
+
+
+ :/startracker/startracker/sun-button-24.png
+ :/startracker/startracker/moon-button-24.png:/startracker/startracker/sun-button-24.png
+
+
+ true
+
+
+
-
@@ -799,7 +898,7 @@ This can be specified as a decimal (E.g. 34.23) or in degrees, minutes and secon
:/zoomin.png:/zoomin.png
- true
+ false
@@ -816,7 +915,7 @@ This can be specified as a decimal (E.g. 34.23) or in degrees, minutes and secon
:/zoomout.png:/zoomout.png
- true
+ false
@@ -842,6 +941,72 @@ This can be specified as a decimal (E.g. 34.23) or in degrees, minutes and secon
+ -
+
+
-
+
+
+ El
+
+
+
+ -
+
+
+ Elevation of Jupiter from specified latitude and longitude in degrees
+
+
+
+ -
+
+
+ CML
+
+
+
+ -
+
+
+ Jupiter Central Meridian Longitude in degrees
+
+
+ true
+
+
+
+ -
+
+
+ Io Phase
+
+
+
+ -
+
+
+ Io phase angle in degrees
+
+
+ true
+
+
+
+ -
+
+
+ Ganymede Phase
+
+
+
+ -
+
+
+ Ganymede phase angle in degrees
+
+
+
+
+
@@ -883,7 +1048,6 @@ This can be specified as a decimal (E.g. 34.23) or in degrees, minutes and secon
latitude
longitude
dateTimeSelect
- dateTime
lst
solarFlux
target
diff --git a/plugins/feature/startracker/startrackerjupiter.qrc b/plugins/feature/startracker/startrackerjupiter.qrc
new file mode 100644
index 000000000..47d76d27c
--- /dev/null
+++ b/plugins/feature/startracker/startrackerjupiter.qrc
@@ -0,0 +1,7 @@
+
+
+ startracker/io-phase-vs-cml.png
+ startracker/ganymede-phase-vs-cml.png
+ startracker/phase-cml-legend.png
+
+
diff --git a/plugins/feature/startracker/startrackerplanets.qrc b/plugins/feature/startracker/startrackerplanets.qrc
new file mode 100644
index 000000000..77fb55b47
--- /dev/null
+++ b/plugins/feature/startracker/startrackerplanets.qrc
@@ -0,0 +1,20 @@
+
+
+ startracker/callisto-250.png
+ startracker/deimos-250.png
+ startracker/earth-250.png
+ startracker/ganymede-250.png
+ startracker/io-250.png
+ startracker/jupiter-250.png
+ startracker/mars-250.png
+ startracker/mercury-250.png
+ startracker/moon-250.png
+ startracker/neptune-250.png
+ startracker/phobos-250.png
+ startracker/pluto-250.png
+ startracker/saturn-250.png
+ startracker/sun-250.png
+ startracker/uranus-250.png
+ startracker/venus-250.png
+
+
diff --git a/plugins/feature/startracker/startrackerreport.h b/plugins/feature/startracker/startrackerreport.h
index cf8000ad4..84726ea31 100644
--- a/plugins/feature/startracker/startrackerreport.h
+++ b/plugins/feature/startracker/startrackerreport.h
@@ -23,6 +23,7 @@
#include
#include
+#include
#include "util/message.h"
@@ -105,6 +106,142 @@ public:
}
};
+ class MsgReportAzElVsTime : public Message {
+ MESSAGE_CLASS_DECLARATION
+
+ public:
+ QString getTarget() const { return m_target; }
+ const QList &getAzimuths() const { return m_azimuths; }
+ const QList &getElevations() const { return m_elevations; }
+ const QList &getDateTimes() const { return m_dateTimes; }
+
+ static MsgReportAzElVsTime* create(const QString&target, const QList &azimuths, const QList &elevations, const QList &dateTimes)
+ {
+ return new MsgReportAzElVsTime(target, azimuths, elevations, dateTimes);
+ }
+
+ private:
+ QString m_target;
+ QList m_azimuths;
+ QList m_elevations;
+ QList m_dateTimes;
+
+ MsgReportAzElVsTime(const QString&target, const QList &azimuths, const QList &elevations, const QList &dateTimes) :
+ Message(),
+ m_target(target),
+ m_azimuths(azimuths),
+ m_elevations(elevations),
+ m_dateTimes(dateTimes)
+ {
+ }
+
+ };
+
+ class MsgReportSolarSystemPositions : public Message {
+ MESSAGE_CLASS_DECLARATION
+
+ public:
+
+ QDateTime getDateTime() const { return m_dateTime; }
+ const QStringList &getNames() const { return m_names; }
+ const QList &getPositions() const { return m_positions; }
+
+ static MsgReportSolarSystemPositions* create(const QDateTime& dateTime, const QStringList &names, const QList &positions)
+ {
+ return new MsgReportSolarSystemPositions(dateTime, names, positions);
+ }
+
+ private:
+ QDateTime m_dateTime;
+ QStringList m_names;
+ QList m_positions;
+
+ MsgReportSolarSystemPositions(const QDateTime& dateTime, const QStringList &names, const QList &positions) :
+ Message(),
+ m_dateTime(dateTime),
+ m_names(names),
+ m_positions(positions)
+ {
+ }
+
+ };
+
+ struct JupiterData {
+ QDateTime m_dateTime;
+ double m_azimuth;
+ double m_elevation;
+ };
+
+ struct JupiterMoonData {
+ double m_cml;
+ double m_phase;
+ };
+
+
+ class MsgReportJupiter : public Message {
+ MESSAGE_CLASS_DECLARATION
+
+ public:
+
+ QDateTime getDateTime() const { return m_dateTime; }
+ double getAzimuth() const { return m_azimuth; }
+ double getElevation() const { return m_elevation; }
+ double getCML() const { return m_cml; }
+ double getIoPhase() const { return m_ioPhase; }
+ double getGanymedePhase() const { return m_ganymedePhase; }
+
+ static MsgReportJupiter* create(const QDateTime& dateTime, double azimuth, double elevation, double cml, double ioPhase, double ganymedePhase)
+ {
+ return new MsgReportJupiter(dateTime, azimuth, elevation, cml, ioPhase, ganymedePhase);
+ }
+
+ private:
+ QDateTime m_dateTime;
+ double m_azimuth;
+ double m_elevation;
+ double m_cml;
+ double m_ioPhase;
+ double m_ganymedePhase;
+
+ MsgReportJupiter(const QDateTime& dateTime, double azimuth, double elevation, double cml, double ioPhase, double ganymedePhase) :
+ Message(),
+ m_dateTime(dateTime),
+ m_azimuth(azimuth),
+ m_elevation(elevation),
+ m_cml(cml),
+ m_ioPhase(ioPhase),
+ m_ganymedePhase(ganymedePhase)
+ {
+ }
+
+ };
+
+ class MsgReportJupiterData : public Message {
+ MESSAGE_CLASS_DECLARATION
+
+ public:
+
+ QList getJupiterData() const { return m_jupiterData; }
+ QList getMoonData() const { return m_moonData; }
+
+ static MsgReportJupiterData* create(const QList& jupiterData, const QList& moonData)
+ {
+ return new MsgReportJupiterData(jupiterData, moonData);
+ }
+
+ private:
+ QList m_jupiterData;
+ QList m_moonData;
+
+ MsgReportJupiterData(const QList& jupiterData, const QList& moonData) :
+ Message(),
+ m_jupiterData(jupiterData),
+ m_moonData(moonData)
+ {
+ }
+
+ };
+
public:
StarTrackerReport() {}
~StarTrackerReport() {}
diff --git a/plugins/feature/startracker/startrackersettings.cpp b/plugins/feature/startracker/startrackersettings.cpp
index 98c7c38b5..d7f14a835 100644
--- a/plugins/feature/startracker/startrackersettings.cpp
+++ b/plugins/feature/startracker/startrackersettings.cpp
@@ -1,5 +1,5 @@
///////////////////////////////////////////////////////////////////////////////////
-// Copyright (C) 2021, 2023 Jon Beniston, M7RCE //
+// Copyright (C) 2021-2026 Jon Beniston, M7RCE //
// Copyright (C) 2021-2022 Edouard Griffiths, F4EXB //
// //
// This program is free software; you can redistribute it and/or modify //
@@ -18,11 +18,32 @@
#include
+#include "util/httpdownloadmanager.h"
#include "util/simpleserializer.h"
#include "settings/serializable.h"
#include "startrackersettings.h"
+const QStringList StarTrackerSettings::m_defaultSpiceEphemerides = {
+ "https://naif.jpl.nasa.gov/pub/naif/generic_kernels/lsk/naif0012.tls",
+ "https://naif.jpl.nasa.gov/pub/naif/generic_kernels/pck/pck00011.tpc",
+ "https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/planets/de440.bsp",
+ "https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/satellites/jup365.bsp",
+};
+
+const QStringList StarTrackerSettings::m_defaultSolarSystemBodies = {
+ "SUN",
+ "MERCURY",
+ "VENUS",
+ "EARTH",
+ "MOON",
+ "MARS BARYCENTER",
+ "JUPITER BARYCENTER",
+ "SATURN BARYCENTER",
+ "URANUS BARYCENTER",
+ "NEPTUNE BARYCENTER"
+};
+
StarTrackerSettings::StarTrackerSettings() :
m_rollupState(nullptr)
{
@@ -76,6 +97,14 @@ void StarTrackerSettings::resetToDefaults()
m_drawMoonOnSkyTempChart = true;
m_workspaceIndex = 0;
m_drawRotators = MATCHING_TARGET;
+ m_targetSource = "SDRangel";
+ m_spiceEphemerides = m_defaultSpiceEphemerides;
+ m_solarSystemBodies = m_defaultSolarSystemBodies;
+ m_logScale = true;
+ m_utc = false;
+ m_chartSelect = CHART_ELEVATION_VS_TIME;
+ m_chartSubSelect = 0;
+ m_night = false;
}
QByteArray StarTrackerSettings::serialize() const
@@ -133,6 +162,13 @@ QByteArray StarTrackerSettings::serialize() const
s.writeS32(45, m_workspaceIndex);
s.writeBlob(46, m_geometryBytes);
s.writeS32(47, (int)m_drawRotators);
+ s.writeString(48, m_targetSource);
+ s.writeList(49, m_spiceEphemerides);
+ s.writeList(50, m_solarSystemBodies);
+ s.writeBool(51, m_logScale);
+ s.writeS32(52, (int) m_chartSelect);
+ s.writeS32(53, m_chartSubSelect);
+ s.writeBool(54, m_night);
return s.final();
}
@@ -222,6 +258,13 @@ bool StarTrackerSettings::deserialize(const QByteArray& data)
d.readS32(45, &m_workspaceIndex, 0);
d.readBlob(46, &m_geometryBytes);
d.readS32(47, (int *)&m_drawRotators, MATCHING_TARGET);
+ d.readString(48, &m_targetSource, "SDRangel");
+ d.readList(49, &m_spiceEphemerides, m_defaultSpiceEphemerides);
+ d.readList(50, &m_solarSystemBodies, m_defaultSolarSystemBodies);
+ d.readBool(51, &m_logScale, true);
+ d.readS32(52, (int *)&m_chartSelect);
+ d.readS32(53, &m_chartSubSelect);
+ d.readBool(54, &m_night, false);
return true;
}
@@ -372,6 +415,30 @@ void StarTrackerSettings::applySettings(const QStringList& settingsKeys, const S
if (settingsKeys.contains("drawRotators")) {
m_drawRotators = settings.m_drawRotators;
}
+ if (settingsKeys.contains("targetSource")) {
+ m_targetSource = settings.m_targetSource;
+ }
+ if (settingsKeys.contains("spiceEphemerides")) {
+ m_spiceEphemerides = settings.m_spiceEphemerides;
+ }
+ if (settingsKeys.contains("solarSystemBodies")) {
+ m_solarSystemBodies = settings.m_solarSystemBodies;
+ }
+ if (settingsKeys.contains("logScale")) {
+ m_logScale = settings.m_logScale;
+ }
+ if (settingsKeys.contains("utc")) {
+ m_utc = settings.m_utc;
+ }
+ if (settingsKeys.contains("chartSelect")) {
+ m_chartSelect = settings.m_chartSelect;
+ }
+ if (settingsKeys.contains("chartSubSelect")) {
+ m_chartSubSelect = settings.m_chartSubSelect;
+ }
+ if (settingsKeys.contains("night")) {
+ m_night = settings.m_night;
+ }
}
QString StarTrackerSettings::getDebugString(const QStringList& settingsKeys, bool force) const
@@ -513,7 +580,49 @@ QString StarTrackerSettings::getDebugString(const QStringList& settingsKeys, boo
if (settingsKeys.contains("drawRotators") || force) {
ostr << " m_drawRotators: " << m_drawRotators;
}
+ if (settingsKeys.contains("targetSource") || force) {
+ ostr << " m_targetSource: " << m_targetSource.toStdString();
+ }
+ if (settingsKeys.contains("spiceEphemerides") || force) {
+ ostr << " m_spiceEphemerides: " << m_spiceEphemerides.join(" ").toStdString();
+ }
+ if (settingsKeys.contains("solarSystemBodies") || force) {
+ ostr << " m_solarSystemBodies: " << m_solarSystemBodies.join(" ").toStdString();
+ }
+ if (settingsKeys.contains("logScale") || force) {
+ ostr << " m_logScale: " << m_logScale;
+ }
+ if (settingsKeys.contains("utc") || force) {
+ ostr << " m_utc: " << m_utc;
+ }
+ if (settingsKeys.contains("chartSelect") || force) {
+ ostr << " m_chartSelect: " << m_chartSelect;
+ }
+ if (settingsKeys.contains("chartSubSelect") || force) {
+ ostr << " m_chartSubSelect: " << m_chartSubSelect;
+ }
+ if (settingsKeys.contains("night") || force) {
+ ostr << " m_night: " << m_night;
+ }
return QString(ostr.str().c_str());
}
+QDateTime StarTrackerSettings::getDateTime() const
+{
+ QDateTime dt;
+
+ if (m_dateTime == "")
+ {
+ dt = QDateTime::currentDateTime();
+ }
+ else
+ {
+ dt = QDateTime::fromString(m_dateTime, Qt::ISODateWithMs);
+ if (m_utc) {
+ dt.setTimeZone(QTimeZone::utc());
+ }
+ }
+
+ return dt;
+}
diff --git a/plugins/feature/startracker/startrackersettings.h b/plugins/feature/startracker/startrackersettings.h
index c8bd33222..b2267a8b5 100644
--- a/plugins/feature/startracker/startrackersettings.h
+++ b/plugins/feature/startracker/startrackersettings.h
@@ -2,7 +2,7 @@
// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany //
// written by Christian Daniel //
// Copyright (C) 2015-2022 Edouard Griffiths, F4EXB //
-// Copyright (C) 2020-2024 Jon Beniston, M7RCE //
+// Copyright (C) 2021-2026 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 //
@@ -33,7 +33,7 @@ struct StarTrackerSettings
double m_latitude;
double m_longitude;
QString m_target; // Sun, Moon, Custom
- QString m_dateTime; // Local date/time for observation, or "" for now
+ QString m_dateTime; // Date/time for observation, or "" for now (m_utc is used to determine whether this is in UTC or local time)
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
@@ -75,6 +75,21 @@ struct StarTrackerSettings
int m_workspaceIndex;
QByteArray m_geometryBytes;
enum Rotators {ALL_ROTATORS, NO_ROTATORS, MATCHING_TARGET} m_drawRotators; //!< Which rotators to draw on polar chart
+ QString m_targetSource; // "SDRangel", "SPICE", "Horizons"
+ QStringList m_spiceEphemerides;
+ QStringList m_solarSystemBodies;
+ bool m_logScale; // Log scale rather than linear for planetary orbits
+ bool m_utc; // Custom date time is in UTC rather than local time
+ enum ChartSelect {
+ CHART_ELEVATION_VS_TIME,
+ CHART_SOLAR_FLUX_VS_FREQUENCY,
+ CHART_SKY_TEMPERATURE,
+ CHART_GALACTIC_LINE_OF_SIGHT,
+ CHART_SOLAR_SYSTEM,
+ CHART_JUPITER
+ } m_chartSelect;
+ int m_chartSubSelect;
+ bool m_night; // false=Day, time axis is midnight to midnight. true=Night, time axis is noon to noon.
StarTrackerSettings();
void resetToDefaults();
@@ -83,6 +98,11 @@ struct StarTrackerSettings
void setRollupState(Serializable *rollupState) { m_rollupState = rollupState; }
void applySettings(const QStringList& settingsKeys, const StarTrackerSettings& settings);
QString getDebugString(const QStringList& settingsKeys, bool force=false) const;
+ QDateTime getDateTime() const;
+
+ static const QStringList m_defaultSpiceEphemerides;
+ static const QStringList m_defaultSolarSystemBodies;
+
};
#endif // INCLUDE_FEATURE_STARTRACKERSETTINGS_H_
diff --git a/plugins/feature/startracker/startrackersettingsdialog.cpp b/plugins/feature/startracker/startrackersettingsdialog.cpp
index be9dcd763..2d317ced7 100644
--- a/plugins/feature/startracker/startrackersettingsdialog.cpp
+++ b/plugins/feature/startracker/startrackersettingsdialog.cpp
@@ -1,5 +1,5 @@
///////////////////////////////////////////////////////////////////////////////////
-// Copyright (C) 2021, 2023 Jon Beniston, M7RCE //
+// Copyright (C) 2021-2026 Jon Beniston, M7RCE //
// Copyright (C) 2022 Edouard Griffiths, F4EXB //
// //
// This program is free software; you can redistribute it and/or modify //
@@ -17,7 +17,11 @@
///////////////////////////////////////////////////////////////////////////////////
#include "startrackersettingsdialog.h"
+#include "spice.h"
+
#include
+#include
+#include
StarTrackerSettingsDialog::StarTrackerSettingsDialog(
StarTrackerSettings *settings,
@@ -27,7 +31,8 @@ StarTrackerSettingsDialog::StarTrackerSettingsDialog(
QDialog(parent),
m_settings(settings),
m_settingsKeys(settingsKeys),
- ui(new Ui::StarTrackerSettingsDialog)
+ ui(new Ui::StarTrackerSettingsDialog),
+ m_spiceEphemerides(this)
{
ui->setupUi(this);
ui->epoch->setCurrentIndex(settings->m_jnow ? 1 : 0);
@@ -49,6 +54,16 @@ StarTrackerSettingsDialog::StarTrackerSettingsDialog(
ui->drawSunOnMap->setChecked(settings->m_drawSunOnMap);
ui->drawMoonOnMap->setChecked(settings->m_drawMoonOnMap);
ui->drawStarOnMap->setChecked(settings->m_drawStarOnMap);
+ ui->ephemerides->clear();
+ ui->solarSystemBodies->clear();
+ for (const auto& ephemeris : settings->m_spiceEphemerides) {
+ ui->ephemerides->addItem(ephemeris);
+ }
+ for (const auto& body : settings->m_solarSystemBodies) {
+ ui->solarSystemBodies->addItem(body);
+ }
+
+ connect(&m_spiceEphemerides, &SpiceEphemerides::allDownloadsComplete, this, &StarTrackerSettingsDialog::allDownloadsComplete);
}
StarTrackerSettingsDialog::~StarTrackerSettingsDialog()
@@ -98,5 +113,95 @@ void StarTrackerSettingsDialog::accept()
m_settingsKeys.append("drawMoonOnMap");
m_settingsKeys.append("drawStarOnMap");
+ QStringList ephemerides = getEphemerides();;
+ if (ephemerides != m_settings->m_spiceEphemerides)
+ {
+ m_settings->m_spiceEphemerides = ephemerides;
+ m_settingsKeys.append("spiceEphemerides");
+ }
+
+ QStringList solarSystemBodies;
+ for (int i = 0; i < ui->solarSystemBodies->count(); i++) {
+ solarSystemBodies.append(ui->solarSystemBodies->item(i)->text());
+ }
+ if (solarSystemBodies != m_settings->m_solarSystemBodies)
+ {
+ m_settings->m_solarSystemBodies = solarSystemBodies;
+ m_settingsKeys.append("solarSystemBodies");
+ }
+
QDialog::accept();
}
+
+void StarTrackerSettingsDialog::on_addEphemeris_clicked()
+{
+ QListWidgetItem *item = new QListWidgetItem("https://");
+ item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled);
+ ui->ephemerides->addItem(item);
+}
+
+void StarTrackerSettingsDialog::on_removeEphemeris_clicked()
+{
+ QList items = ui->ephemerides->selectedItems();
+ for (int i = 0; i < items.size(); i++) {
+ delete items[i];
+ }
+}
+
+void StarTrackerSettingsDialog::on_useDefaultEphemeridies_clicked()
+{
+ ui->ephemerides->clear();
+ for (const auto& ephemeris : StarTrackerSettings::m_defaultSpiceEphemerides)
+ {
+ QListWidgetItem *item = new QListWidgetItem(ephemeris);
+ item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled);
+ ui->ephemerides->addItem(item);
+ }
+}
+
+void StarTrackerSettingsDialog::on_addSolarSystemBody_clicked()
+{
+ if (!downloadEphemerides()) {
+ selectSolarSystemBody();
+ }
+}
+
+void StarTrackerSettingsDialog::on_removeSolarSystemBody_clicked()
+{
+ QList items = ui->solarSystemBodies->selectedItems();
+ for (int i = 0; i < items.size(); i++) {
+ delete items[i];
+ }
+}
+
+QStringList StarTrackerSettingsDialog::getEphemerides() const
+{
+ QStringList ephemerides;
+ for (int i = 0; i < ui->ephemerides->count(); i++) {
+ ephemerides.append(ui->ephemerides->item(i)->text());
+ }
+ return ephemerides;
+}
+
+bool StarTrackerSettingsDialog::downloadEphemerides()
+{
+ return m_spiceEphemerides.download(getEphemerides());
+}
+
+void StarTrackerSettingsDialog::allDownloadsComplete()
+{
+ selectSolarSystemBody();
+}
+
+void StarTrackerSettingsDialog::selectSolarSystemBody()
+{
+ QStringList availableSolarSystemBodies = m_spiceEphemerides.getTargets(getEphemerides());
+ bool ok;
+ QString body = QInputDialog::getItem(this, "Select body to add", "Body", availableSolarSystemBodies, 0, false, &ok);
+ if (ok)
+ {
+ QListWidgetItem *item = new QListWidgetItem(body);
+ item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled);
+ ui->solarSystemBodies->addItem(item);
+ }
+}
diff --git a/plugins/feature/startracker/startrackersettingsdialog.h b/plugins/feature/startracker/startrackersettingsdialog.h
index 17498afc3..9ecd732e6 100644
--- a/plugins/feature/startracker/startrackersettingsdialog.h
+++ b/plugins/feature/startracker/startrackersettingsdialog.h
@@ -23,6 +23,7 @@
#include "ui_startrackersettingsdialog.h"
#include "startrackersettings.h"
+#include "spiceephemerides.h"
class StarTrackerSettingsDialog : public QDialog {
Q_OBJECT
@@ -35,10 +36,21 @@ public:
QList& m_settingsKeys;
private slots:
+ void on_addEphemeris_clicked();
+ void on_removeEphemeris_clicked();
+ void on_useDefaultEphemeridies_clicked();
+ void on_addSolarSystemBody_clicked();
+ void on_removeSolarSystemBody_clicked();
void accept();
+ void allDownloadsComplete();
private:
Ui::StarTrackerSettingsDialog* ui;
+ SpiceEphemerides m_spiceEphemerides;
+
+ QStringList getEphemerides() const;
+ bool downloadEphemerides();
+ void selectSolarSystemBody();
};
#endif // INCLUDE_STARTRACKERSETTINGSDIALOG_H
diff --git a/plugins/feature/startracker/startrackersettingsdialog.ui b/plugins/feature/startracker/startrackersettingsdialog.ui
index a2d668912..7337ec82f 100644
--- a/plugins/feature/startracker/startrackersettingsdialog.ui
+++ b/plugins/feature/startracker/startrackersettingsdialog.ui
@@ -6,8 +6,8 @@
0
0
- 569
- 556
+ 642
+ 716
@@ -21,457 +21,664 @@
-
-
-
-
-
-
-
- Enable Stellarium server which allows RA and Dec to be sent to and from Stellarium
-
-
- Stellarium server
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
-
-
-
- -
-
-
- Draw Sun on map
-
-
-
- -
-
-
- Units used for displaying azimuth and elevation. Either degrees, minutes and seconds or decimal degrees.
-
-
- false
-
-
-
-
- ° ' "
+
+
+ 0
+
+
+
+ Settings
+
+
+
-
+
+
+
-
+
+
+ Azimuth and elevation units
+
+
+
+ -
+
+
+ Rotators in polar chart
+
+
+
+ -
+
+
+ Stellarium telescope server IP port number
+
+
+ 1024
+
+
+ 65535
+
+
+ 10001
+
+
+
+ -
+
+
+ Draw Moon on map
+
+
+
+ -
+
+
+ Atmospheric refraction correction
+
+
+ 0
+
+
-
+
+ None
+
+
+ -
+
+ Saemundsson
+
+
+ -
+
+ Positional Astronomy Library
+
+
+
+
+ -
+
+
+ Epoch for RA & Dec
+
+
+
+ -
+
+
+ Qt::Orientation::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
+ Weather update period (min)
+
+
+
+ -
+
+
+ Solar flux density units
+
+
+
+ -
+
+
+ Select which rotators are displayed on the polar chart
+
+
-
+
+ All
+
+
+ -
+
+ None
+
+
+ -
+
+ Matching target
+
+
+
+
+ -
+
+
+ Draw target on map
+
+
+
+ -
+
+
+ Air temperature in degrees Celsius, for use in atmospheric refraction correction
+
+
+ -100
+
+
+ 100
+
+
+ 10
+
+
+
+ -
+
+
+ Units to use for the display of the Solar flux density
+
+
-
+
+ Solar flux units (sfu)
+
+
+ -
+
+ Jansky (Jy)
+
+
+ -
+
+ Watts per square metre per hertz (W m^-2 Hz-1)
+
+
+
+
+ -
+
+
+ Height above sea level (m)
+
+
+
+ -
+
+
+ Enter the time in minutes between each weather update
+
+
+ 100000
+
+
+
+ -
+
+
+ Height of observation/antenna location above sea level in metres
+
+
+ -1000
+
+
+ 20000
+
+
+
+ -
+
+
+ Enter the time in seconds between each calculation of the target's position
+
+
+ 1.000000000000000
+
+
+
+ -
+
+
+ Air pressure (mb)
+
+
+
+ -
+
+
+ Humidity (%)
+
+
+
+ -
+
+
+ 3
+
+
+ 100.000000000000000
+
+
+ 6.490000000000000
+
+
+
+ -
+
+
+ Refraction correction
+
+
+
+ -
+
+
+ Select frequency at which to display Solar flux density data for
+
+
-
+
+ DRAO (2800MHz)
+
+
+ -
+
+ Learmonth (245MHz)
+
+
+ -
+
+ Learmonth (410MHz)
+
+
+ -
+
+ Learmonth (610MHz)
+
+
+ -
+
+ Learmonth (1415MHz)
+
+
+ -
+
+ Learmonth (2695MHz)
+
+
+ -
+
+ Learmonth (4995MHz)
+
+
+ -
+
+ Learmonth (8800MHz)
+
+
+ -
+
+ Learmonth (15400MHz)
+
+
+ -
+
+ Observation frequency
+
+
+
+
+ -
+
+
+ Air pressure in millibars, for use in atmospheric refraction correction
+
+
+ 2000.000000000000000
+
+
+ 1.000000000000000
+
+
+ 1010.000000000000000
+
+
+
+ -
+
+
+ Solar flux density data
+
+
+
+ -
+
+
+ API key from openweathermap.org to download real-time weather
+
+
+
+ -
+
+
+ OpenWeatherMap API Key
+
+
+
+ -
+
+
+ Epoch for custom right ascension and declination
+
+
-
+
+ J2000
+
+
+ -
+
+ JNOW
+
+
+
+
+ -
+
+
+ Units used for displaying azimuth and elevation. Either degrees, minutes and seconds or decimal degrees.
+
+
+ false
+
+
-
+
+ ° ' "
+
+
+ -
+
+ ° '
+
+
+ -
+
+ °
+
+
+ -
+
+ Decimal
+
+
+
+
+ -
+
+
+ Temperature lapse rate (K/m)
+
+
+ Temperature lapse rate (K/km)
+
+
+
+ -
+
+
+ Relative humidity in %
+
+
+ 100
+
+
+ 80
+
+
+
+ -
+
+
+ Update period (s)
+
+
+
+ -
+
+
+ Stellarium server port
+
+
+
+ -
+
+
+ Draw Sun on map
+
+
+
+ -
+
+
+ Air temperature (C)
+
+
+
+ -
+
+
+ Enable Stellarium server which allows RA and Dec to be sent to and from Stellarium
+
+
+ Stellarium server
+
+
+
+
+
+
+
+
+
+
+ Ephermerides
+
+
+ -
+
+
+ Ephemerides
-
- -
-
- ° '
+
+
-
+
+
+ SPICE empemeris files
+
+
-
+
+ https://naif.jpl.nasa.gov/pub/naif/generic_kernels/lsk/naif0012.tls
+
+
+ -
+
+ https://naif.jpl.nasa.gov/pub/naif/generic_kernels/pck/pck00011.tpc
+
+
+ -
+
+ https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/planets/de440.bsp
+
+
+ -
+
+ https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/satellites/jup365.bsp
+
+
+
+
+ -
+
+
-
+
+
+ Add a new ephemeris to list
+
+
+ +
+
+
+
+ -
+
+
+ Remove selected emphemerides from list
+
+
+ -
+
+
+
+ -
+
+
+ Use default ephemerides
+
+
+ Default
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+
+
+ -
+
+
+ Solar System Bodies
-
- -
-
- °
-
-
- -
-
- Decimal
-
-
-
-
- -
-
-
- Air temperature (C)
-
-
-
- -
-
-
- Relative humidity in %
-
-
- 100
-
-
- 80
-
-
-
- -
-
-
- Atmospheric refraction correction
-
-
- 0
-
-
-
-
- None
-
-
- -
-
- Saemundsson
-
-
- -
-
- Positional Astronomy Library
-
-
-
-
- -
-
-
- Temperature lapse rate (K/m)
-
-
- Temperature lapse rate (K/km)
-
-
-
- -
-
-
- Humidity (%)
-
-
-
- -
-
-
- Epoch for RA & Dec
-
-
-
- -
-
-
- Weather update period (min)
-
-
-
- -
-
-
- Air pressure (mb)
-
-
-
- -
-
-
- Draw target star on map
-
-
-
- -
-
-
- OpenWeatherMap API Key
-
-
-
- -
-
-
- Solar flux density units
-
-
-
- -
-
-
- Air pressure in millibars, for use in atmospheric refraction correction
-
-
- 2000.000000000000000
-
-
- 1.000000000000000
-
-
- 1010.000000000000000
-
-
-
- -
-
-
- Height above sea level (m)
-
-
-
- -
-
-
- Select frequency at which to display Solar flux density data for
-
-
-
-
- DRAO (2800MHz)
-
-
- -
-
- Learmonth (245MHz)
-
-
- -
-
- Learmonth (410MHz)
-
-
- -
-
- Learmonth (610MHz)
-
-
- -
-
- Learmonth (1415MHz)
-
-
- -
-
- Learmonth (2695MHz)
-
-
- -
-
- Learmonth (4995MHz)
-
-
- -
-
- Learmonth (8800MHz)
-
-
- -
-
- Learmonth (15400MHz)
-
-
- -
-
- Observation frequency
-
-
-
-
- -
-
-
- 3
-
-
- 100.000000000000000
-
-
- 6.490000000000000
-
-
-
- -
-
-
- API key from openweathermap.org to download real-time weather
-
-
-
- -
-
-
- Air temperature in degrees Celsius, for use in atmospheric refraction correction
-
-
- -100
-
-
- 100
-
-
- 10
-
-
-
- -
-
-
- Draw Moon on map
-
-
-
- -
-
-
- Update period (s)
-
-
-
- -
-
-
- Epoch for custom right ascension and declination
-
-
-
-
- J2000
-
-
- -
-
- JNOW
-
-
-
-
- -
-
-
- Refraction correction
-
-
-
- -
-
-
- Solar flux density data
-
-
-
- -
-
-
- Units to use for the display of the Solar flux density
-
-
-
-
- Solar flux units (sfu)
-
-
- -
-
- Jansky (Jy)
-
-
- -
-
- Watts per square metre per hertz (W m^-2 Hz-1)
-
-
-
-
- -
-
-
- Height of observation/antenna location above sea level in metres
-
-
- -1000
-
-
- 20000
-
-
-
- -
-
-
- Enter the time in seconds between each calculation of the target's position
-
-
- 1.000000000000000
-
-
-
- -
-
-
- Stellarium telescope server IP port number
-
-
- 1024
-
-
- 65535
-
-
- 10001
-
-
-
- -
-
-
- Stellarium server port
-
-
-
- -
-
-
- Enter the time in minutes between each weather update
-
-
- 100000
-
-
-
- -
-
-
- Azimuth and elevation units
-
-
-
- -
-
-
- Rotators in polar chart
-
-
-
- -
-
-
- Select which rotators are displayed on the polar chart
-
-
-
-
- All
-
-
- -
-
- None
-
-
- -
-
- Matching target
-
-
-
-
-
+
+ -
+
+
+ Bodies to display in solar system chart
+
+
-
+
+ SUN
+
+
+ -
+
+ MERCURY
+
+
+ -
+
+ VENUS
+
+
+ -
+
+ EARTH
+
+
+ -
+
+ MOON
+
+
+ -
+
+ MARS BARYCENTER
+
+
+ -
+
+ JUPITER BARYCENTER
+
+
+ -
+
+ SATURN BARYCENTER
+
+
+ -
+
+ URANUS BARYCENTER
+
+
+ -
+
+ NEPTUNE BARYCENTER
+
+
+
+
+ -
+
+
-
+
+
+ Add a new body to list
+
+
+ +
+
+
+
+ -
+
+
+ Remove selected bodies from list
+
+
+ -
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+
+
+
+
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
- QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+ QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok
diff --git a/plugins/feature/startracker/startrackerworker.cpp b/plugins/feature/startracker/startrackerworker.cpp
index f567d65c2..f2f3646bf 100644
--- a/plugins/feature/startracker/startrackerworker.cpp
+++ b/plugins/feature/startracker/startrackerworker.cpp
@@ -1,5 +1,5 @@
///////////////////////////////////////////////////////////////////////////////////
-// Copyright (C) 2021-2023 Jon Beniston, M7RCE //
+// Copyright (C) 2021-2026 Jon Beniston, M7RCE //
// Copyright (C) 2022 Edouard Griffiths, F4EXB //
// Copyright (C) 2022 Jiří Pinkava //
// //
@@ -26,6 +26,7 @@
#include
#include
#include
+#include
#include "SWGTargetAzimuthElevation.h"
#include "SWGMapItem.h"
@@ -36,16 +37,41 @@
#include "channel/channelwebapiutils.h"
#include "util/units.h"
+#include "util/profiler.h"
#include "maincore.h"
#include "startracker.h"
#include "startrackerworker.h"
#include "startrackerreport.h"
+#include "spice.h"
+#include "spiceephemerides.h"
MESSAGE_CLASS_DEFINITION(StarTrackerWorker::MsgConfigureStarTrackerWorker, Message)
MESSAGE_CLASS_DEFINITION(StarTrackerReport::MsgReportAzAl, Message)
MESSAGE_CLASS_DEFINITION(StarTrackerReport::MsgReportRADec, Message)
MESSAGE_CLASS_DEFINITION(StarTrackerReport::MsgReportGalactic, Message)
+MESSAGE_CLASS_DEFINITION(StarTrackerReport::MsgReportAzElVsTime, Message)
+MESSAGE_CLASS_DEFINITION(StarTrackerReport::MsgReportSolarSystemPositions, Message)
+MESSAGE_CLASS_DEFINITION(StarTrackerReport::MsgReportJupiter, Message)
+MESSAGE_CLASS_DEFINITION(StarTrackerReport::MsgReportJupiterData, Message)
+
+static double linearInterpolate(double y1, double y2, double mu)
+{
+ return y1 + mu * (y2 - y1);
+}
+
+static double cubicInterpolate(double y0, double y1, double y2, double y3, double mu)
+{
+ double a0, a1, a2, a3, mu2;
+
+ mu2 = mu * mu;
+ a0 = y3 - y2 - y0 + y1;
+ a1 = y0 - y1 - a0;
+ a2 = y2 - y0;
+ a3 = y1;
+
+ return a0 * mu * mu2 + a1 * mu2 + a2 * mu + a3;
+}
StarTrackerWorker::StarTrackerWorker(StarTracker* starTracker, WebAPIAdapterInterface *webAPIAdapterInterface) :
m_starTracker(starTracker),
@@ -55,15 +81,27 @@ StarTrackerWorker::StarTrackerWorker(StarTracker* starTracker, WebAPIAdapterInte
m_pollTimer(this),
m_tcpServer(nullptr),
m_clientConnection(nullptr),
- m_solarFlux(0.0f)
+ m_solarFlux(0.0f),
+ m_jplHorizons(nullptr),
+ m_requestedEphemeridesLatitude(0.0),
+ m_requestedEphemeridesLongitude(0.0),
+ m_chartLatitude(0.0),
+ m_chartLongitude(0.0),
+ m_chartL(0.0f),
+ m_chartB(0.0f)
{
- connect(&m_pollTimer, SIGNAL(timeout()), this, SLOT(update()));
+ connect(&m_pollTimer, &QTimer::timeout, this, &StarTrackerWorker::update);
}
StarTrackerWorker::~StarTrackerWorker()
{
stopWork();
m_inputMessageQueue.clear();
+ if (m_jplHorizons)
+ {
+ disconnect(m_jplHorizons, &JPLHorizons::ephemeridesUpdated, this, &StarTrackerWorker::horizonsEphemeridesUpdated);
+ delete m_jplHorizons;
+ }
}
void StarTrackerWorker::startWork()
@@ -149,6 +187,11 @@ void StarTrackerWorker::applySettings(const StarTrackerSettings& settings, const
|| settingsKeys.contains("b")
|| settingsKeys.contains("azimuthOffset")
|| settingsKeys.contains("elevationOffset")
+ || settingsKeys.contains("night")
+ || settingsKeys.contains("logScale")
+ || settingsKeys.contains("utc")
+ || settingsKeys.contains("chartSelect")
+ || settingsKeys.contains("chartSubSelect")
|| force)
{
// Recalculate immediately
@@ -175,13 +218,23 @@ void StarTrackerWorker::applySettings(const StarTrackerSettings& settings, const
removeFromMap("Star");
}
-
if (settingsKeys.contains("serverPort") ||
settingsKeys.contains("enableServer") || force)
{
restartServer(settings.m_enableServer, settings.m_serverPort);
}
+ if ((settingsKeys.contains("targetSource") || force) && (settings.m_targetSource == "Horizons"))
+ {
+ if (!m_jplHorizons)
+ {
+ m_jplHorizons = JPLHorizons::create();
+ if (m_jplHorizons) {
+ connect(m_jplHorizons, &JPLHorizons::ephemeridesUpdated, this, &StarTrackerWorker::horizonsEphemeridesUpdated);
+ }
+ }
+ }
+
if (force) {
m_settings = settings;
} else {
@@ -372,31 +425,6 @@ void StarTrackerWorker::writeStellariumTarget(double ra, double dec)
}
}
-
-void StarTrackerWorker::updateRaDec(RADec rd, QDateTime dt, bool lbTarget)
-{
- RADec rdJ2000;
- double jd;
-
- jd = Astronomy::julianDate(dt);
- // Precess to J2000
- rdJ2000 = Astronomy::precess(rd, jd, Astronomy::jd_j2000());
- // 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") || m_settings.m_target.contains("SkyMap"))
- {
- if (getMessageQueueToGUI())
- {
- if (m_settings.m_jnow) {
- getMessageQueueToGUI()->push(StarTrackerReport::MsgReportRADec::create(rd.ra, rd.dec, "target"));
- } else {
- getMessageQueueToGUI()->push(StarTrackerReport::MsgReportRADec::create(rdJ2000.ra, rdJ2000.dec, "target"));
- }
- }
- }
-}
-
void StarTrackerWorker::removeFromMap(QString id)
{
QList mapMessagePipes;
@@ -469,21 +497,139 @@ QString moonPhase(double sunLongitude, double moonLongitude, double observationL
return "full";
}
+bool StarTrackerWorker::getAzElFromSatelliteTracker(double &azimuth, double &elevation)
+{
+ unsigned int satelliteTrackerFeatureSetIndex,satelliteTrackerFeatureIndex;
+
+ if (MainCore::getFeatureIndexFromId(m_settings.m_target, satelliteTrackerFeatureSetIndex, satelliteTrackerFeatureIndex))
+ {
+ if (ChannelWebAPIUtils::getFeatureReportValue(satelliteTrackerFeatureSetIndex, satelliteTrackerFeatureIndex, "targetAzimuth", azimuth)
+ && ChannelWebAPIUtils::getFeatureReportValue(satelliteTrackerFeatureSetIndex, satelliteTrackerFeatureIndex, "targetElevation", elevation))
+ {
+ return true;
+ }
+ else
+ {
+ qDebug() << "StarTrackerWorker::getAzElFromSatelliteTracker - Failed to get feature report value (targetAzimuth/targetElevation) from" << m_settings.m_target;
+ return false;
+ }
+ }
+ else
+ {
+ qDebug() << "StarTrackerWorker::getAzElFromSatelliteTracker - Failed to parse feature name" << m_settings.m_target;
+ return false;
+ }
+}
+
+bool StarTrackerWorker::getRAFromSkyMap(RADec &rdJ2000)
+{
+ 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))
+ {
+ rdJ2000.ra = ra;
+ rdJ2000.dec = dec;
+ return true;
+ }
+ else
+ {
+ qDebug() << "StarTrackerWorker::getRAFromSkyMap - Failed to get feature report value (ra/dec) from" << m_settings.m_target;
+ return false;
+ }
+ }
+ else
+ {
+ qDebug() << "StarTrackerWorker::getRAFromSkyMap - Failed to parse feature name" << m_settings.m_target;
+ return false;
+ }
+}
+
+bool StarTrackerWorker::getRAFromHorizons(const QDateTime &dateTime, RADec &rdJ2000)
+{
+
+ if ( (m_requestedEphemeridesTarget != m_settings.m_target)
+ || (dateTime < m_requestedEphemeridesStartTime)
+ || (dateTime > m_requestedEphemeridesStopTime)
+ || (m_requestedEphemeridesLatitude != m_settings.m_latitude)
+ || (m_requestedEphemeridesLongitude != m_settings.m_longitude)
+ )
+ {
+ // Clear current data
+ m_ephemeridesTarget = "";
+ m_horizonsEphemerides = {};
+ // Request ephemerides
+ m_requestedEphemeridesTarget = m_settings.m_target;
+ m_requestedEphemeridesStartTime = QDateTime(dateTime.date(), QTime(0, 0));
+ m_requestedEphemeridesStopTime = QDateTime(dateTime.date().addDays(1), QTime(0, 0));
+ m_requestedEphemeridesLatitude = m_settings.m_latitude;
+ m_requestedEphemeridesLongitude = m_settings.m_longitude;
+ m_jplHorizons->getData(m_requestedEphemeridesTarget, m_requestedEphemeridesStartTime, m_requestedEphemeridesStopTime, m_requestedEphemeridesLatitude, m_requestedEphemeridesLongitude, 0.0f);
+ }
+ else if ( (m_settings.m_target == m_ephemeridesTarget)
+ && ( !m_horizonsEphemerides.isEmpty()
+ && ( (dateTime >= m_horizonsEphemerides[0].m_dateTime)
+ || (dateTime < m_horizonsEphemerides[m_horizonsEphemerides.size() - 1].m_dateTime)
+ )
+ )
+ )
+ {
+ // Find closest empheris
+ for (int i = 0; i < m_horizonsEphemerides.size() - 1; i++)
+ {
+ if ((dateTime >= m_horizonsEphemerides[i].m_dateTime) && (dateTime < m_horizonsEphemerides[i+1].m_dateTime))
+ {
+ // Interpolate
+
+ qint64 x = dateTime.toMSecsSinceEpoch();
+ qint64 x1 = m_horizonsEphemerides[i].m_dateTime.toMSecsSinceEpoch();
+ qint64 x2 = m_horizonsEphemerides[i+1].m_dateTime.toMSecsSinceEpoch();
+ qint64 diff = x2 - x1;
+ double mu = (x - x1) / (double) diff;
+
+ if ((i == 0) || (i >= m_horizonsEphemerides.size() - 2))
+ {
+ rdJ2000.ra = linearInterpolate(m_horizonsEphemerides[i].m_ra, m_horizonsEphemerides[i+1].m_ra, mu);
+ rdJ2000.dec = linearInterpolate(m_horizonsEphemerides[i].m_dec, m_horizonsEphemerides[i+1].m_dec, mu);
+ }
+ else
+ {
+ rdJ2000.ra = cubicInterpolate(m_horizonsEphemerides[i-1].m_ra, m_horizonsEphemerides[i].m_ra, m_horizonsEphemerides[i+1].m_ra, m_horizonsEphemerides[i+2].m_ra, mu);
+ rdJ2000.dec = cubicInterpolate(m_horizonsEphemerides[i-1].m_dec, m_horizonsEphemerides[i].m_dec, m_horizonsEphemerides[i+1].m_dec, m_horizonsEphemerides[i+2].m_dec, mu);
+ }
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
void StarTrackerWorker::update()
{
AzAlt aa, sunAA, moonAA;
- RADec rd, sunRD, moonRD;
+ RADec rdJnow, sunRD, moonRD;
+ bool rdJnowValid = false;
double l, b;
bool lbTarget = false;
-
+ bool horizonsTarget = false;
QDateTime dt;
+ const QStringList raDecTargets = {
+ "Custom RA/Dec", "PSR B0329+54", "PSR B0833-45", "Sagittarius A", "Cassiopeia A", "Cygnus A", "Taurus A (M1)", "Virgo A (M87)"
+ };
+ const QStringList lbTargets = {
+ "Custom l/b", "S7", "S8", "S9"
+ };
+
+ PROFILER_START();
+
+ spiceLock(SpiceEphemerides::getEphemeridesFiles(m_settings.m_spiceEphemerides));
+
// Get date and time to calculate position at
- if (m_settings.m_dateTime == "") {
- dt = QDateTime::currentDateTime();
- } else {
- dt = QDateTime::fromString(m_settings.m_dateTime, Qt::ISODateWithMs);
- }
+ dt = m_settings.getDateTime();
// Calculate position
if ((m_settings.m_target == "Sun") || m_settings.m_drawSunOnMap || m_settings.m_drawSunOnSkyTempChart)
@@ -497,246 +643,608 @@ void StarTrackerWorker::update()
getMessageQueueToGUI()->push(StarTrackerReport::MsgReportRADec::create(moonRD.ra, moonRD.dec, "moon"));
}
- if (m_settings.m_target.contains("SatelliteTracker"))
+ if ((m_settings.m_target == "Sun") && (m_settings.m_targetSource == "SDRangel"))
+ {
+ rdJnow = sunRD;
+ rdJnowValid = true;
+ aa = sunAA;
+ RADec rdJ2000 = Astronomy::precess(rdJnow, Astronomy::julianDate(dt), Astronomy::jd_j2000());
+ Astronomy::equatorialToGalactic(rdJ2000.ra, rdJ2000.dec, l, b);
+ }
+ else if ((m_settings.m_target == "Moon") && (m_settings.m_targetSource == "SDRangel"))
+ {
+ rdJnow = moonRD;
+ rdJnowValid = true;
+ aa = moonAA;
+ RADec rdJ2000 = Astronomy::precess(rdJnow, Astronomy::julianDate(dt), Astronomy::jd_j2000());
+ Astronomy::equatorialToGalactic(rdJ2000.ra, rdJ2000.dec, l, b);
+ }
+ else if (m_settings.m_target.contains("SatelliteTracker"))
{
// Get Az/El from Satellite Tracker
double azimuth, elevation;
- unsigned int satelliteTrackerFeatureSetIndex,satelliteTrackerFeatureIndex;
-
- if (MainCore::getFeatureIndexFromId(m_settings.m_target, satelliteTrackerFeatureSetIndex, satelliteTrackerFeatureIndex))
+ if (getAzElFromSatelliteTracker(azimuth, elevation))
{
- if (ChannelWebAPIUtils::getFeatureReportValue(satelliteTrackerFeatureSetIndex, satelliteTrackerFeatureIndex, "targetAzimuth", azimuth)
- && ChannelWebAPIUtils::getFeatureReportValue(satelliteTrackerFeatureSetIndex, satelliteTrackerFeatureIndex, "targetElevation", elevation))
- {
- m_settings.m_el = elevation;
- m_settings.m_az = azimuth;
- }
- else
- qDebug() << "StarTrackerWorker::update - Failed to target from feature " << m_settings.m_target;
+ m_settings.m_el = elevation;
+ m_settings.m_az = azimuth;
+ // Convert Alt/Az to RA/Dec and l/b
+ aa.alt = m_settings.m_el;
+ aa.az = m_settings.m_az;
+ rdJnow = Astronomy::azAltToRaDec(aa, m_settings.m_latitude, m_settings.m_longitude, dt);
+ rdJnowValid = true;
+ RADec rdJ2000 = Astronomy::precess(rdJnow, Astronomy::julianDate(dt), Astronomy::jd_j2000());
+ Astronomy::equatorialToGalactic(rdJ2000.ra, rdJ2000.dec, l, b);
}
- else
- qDebug() << "StarTrackerWorker::update - Failed to parse feature name " << m_settings.m_target;
}
- if (m_settings.m_target.contains("SkyMap"))
+ else if (m_settings.m_target == "Custom Az/El")
{
- // 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")
- {
- rd = sunRD;
- aa = sunAA;
- Astronomy::equatorialToGalactic(rd.ra, rd.dec, l, b);
- }
- else if (m_settings.m_target == "Moon")
- {
- rd = moonRD;
- aa = moonAA;
- Astronomy::equatorialToGalactic(rd.ra, rd.dec, l, b);
- }
- else if ((m_settings.m_target == "Custom Az/El") || m_settings.m_target.contains("SatelliteTracker"))
- {
- // Convert Alt/Az to RA/Dec
+ // Convert Alt/Az to RA/Dec and l/b
aa.alt = m_settings.m_el;
aa.az = m_settings.m_az;
- rd = Astronomy::azAltToRaDec(aa, m_settings.m_latitude, m_settings.m_longitude, dt);
- // Precess RA/DEC from Jnow to J2000
- RADec rd2000 = Astronomy::precess(rd, Astronomy::julianDate(dt), Astronomy::jd_j2000());
- // Convert to l/b
- Astronomy::equatorialToGalactic(rd2000.ra, rd2000.dec, l, b);
- if (!m_settings.m_jnow) {
- rd = rd2000;
- }
+ rdJnow = Astronomy::azAltToRaDec(aa, m_settings.m_latitude, m_settings.m_longitude, dt);
+ rdJnowValid = true;
+ RADec rdJ2000 = Astronomy::precess(rdJnow, Astronomy::julianDate(dt), Astronomy::jd_j2000());
+ Astronomy::equatorialToGalactic(rdJ2000.ra, rdJ2000.dec, l, b);
}
- else if ( (m_settings.m_target == "Custom l/b")
- || (m_settings.m_target == "S7")
- || (m_settings.m_target == "S8")
- || (m_settings.m_target == "S9")
- )
+ else if (lbTargets.contains(m_settings.m_target))
{
// Convert l/b to RA/Dec, then Alt/Az
l = m_settings.m_l;
b = m_settings.m_b;
- Astronomy::galacticToEquatorial(l, b, rd.ra, rd.dec);
- aa = Astronomy::raDecToAzAlt(rd, m_settings.m_latitude, m_settings.m_longitude, dt, !m_settings.m_jnow);
+ RADec rdJ2000;
+ Astronomy::galacticToEquatorial(l, b, rdJ2000.ra, rdJ2000.dec);
+ aa = Astronomy::raDecToAzAlt(rdJ2000, m_settings.m_latitude, m_settings.m_longitude, dt, true);
lbTarget = true;
+ rdJnow = Astronomy::precess(rdJ2000, Astronomy::jd_j2000(), Astronomy::julianDate(dt));
+ rdJnowValid = true;
+ }
+ else if (m_settings.m_targetSource == "SPICE")
+ {
+ // Calculate Alt/Az and RA/Dec with SPICE, then convert to l/b
+ double et;
+
+ if (dateTimeToET(dt, et))
+ {
+ getAzElFromSPICE(m_settings.m_target, et, m_settings.m_latitude, m_settings.m_longitude, 0.0, aa.az, aa.alt);
+
+ RADec rdJ2000;
+ if (getRADecFromSPICE(m_settings.m_target, et, rdJ2000.ra, rdJ2000.dec))
+ {
+ rdJnow = Astronomy::precess(rdJ2000, Astronomy::jd_j2000(), Astronomy::julianDate(dt));
+ rdJnowValid = true;
+ Astronomy::equatorialToGalactic(rdJ2000.ra, rdJ2000.dec, l, b);
+ }
+ }
}
else
{
- // Convert RA/Dec to Alt/Az
- rd.ra = Units::raToDecimal(m_settings.m_ra);
- rd.dec = Units::decToDecimal(m_settings.m_dec);
- aa = Astronomy::raDecToAzAlt(rd, m_settings.m_latitude, m_settings.m_longitude, dt, !m_settings.m_jnow);
- Astronomy::equatorialToGalactic(rd.ra, rd.dec, l, b);
- }
- updateRaDec(rd, dt, lbTarget);
-
- // Adjust for refraction
- if (m_settings.m_refraction == "Positional Astronomy Library")
- {
- aa.alt += Astronomy::refractionPAL(aa.alt, m_settings.m_pressure, m_settings.m_temperature, m_settings.m_humidity,
- m_settings.m_frequency, m_settings.m_latitude, m_settings.m_heightAboveSeaLevel,
- m_settings.m_temperatureLapseRate);
- if (aa.alt > 90.0) {
- aa.alt = 90.0f;
- }
- }
- else if (m_settings.m_refraction == "Saemundsson")
- {
- aa.alt += Astronomy::refractionSaemundsson(aa.alt, m_settings.m_pressure, m_settings.m_temperature);
- if (aa.alt > 90.0) {
- aa.alt = 90.0f;
- }
- }
-
- // Add user-adjustment
- aa.alt += m_settings.m_elevationOffset;
- aa.az += m_settings.m_azimuthOffset;
-
- // Send to GUI
- if (getMessageQueueToGUI())
- {
- // MsgReportRADec sent in updateRaDec()
- if (m_settings.m_target != "Custom Az/El") {
- getMessageQueueToGUI()->push(StarTrackerReport::MsgReportAzAl::create(aa.az, aa.alt));
- }
- if (!lbTarget) {
- getMessageQueueToGUI()->push(StarTrackerReport::MsgReportGalactic::create(l, b));
- }
- }
-
- // Send Az/El to Rotator Controllers
- // Unless we're receiving settings to display from a Radio Astronomy plugins
- if (!m_settings.m_link)
- {
- QList rotatorPipes;
- MainCore::instance()->getMessagePipes().getMessagePipes(m_starTracker, "target", rotatorPipes);
-
- for (const auto& pipe : rotatorPipes)
+ RADec rdJ2000;
+ if (m_settings.m_target.contains("SkyMap"))
{
- MessageQueue *messageQueue = qobject_cast(pipe->m_element);
- SWGSDRangel::SWGTargetAzimuthElevation *swgTarget = new SWGSDRangel::SWGTargetAzimuthElevation();
- swgTarget->setName(new QString(m_settings.m_target));
- swgTarget->setAzimuth(aa.az);
- swgTarget->setElevation(aa.alt);
- messageQueue->push(MainCore::MsgTargetAzimuthElevation::create(m_starTracker, swgTarget));
+ // Get RA/Dec from Sky Map
+ if (getRAFromSkyMap(rdJ2000))
+ {
+ m_settings.m_ra = QString::number(rdJ2000.ra, 'f', 10);;
+ m_settings.m_dec = QString::number(rdJ2000.dec, 'f', 10);;
+ rdJnow = Astronomy::precess(rdJ2000, Astronomy::jd_j2000(), Astronomy::julianDate(dt));
+ rdJnowValid = true;
+ }
}
- }
-
- // Send Az/El, RA/Dec and Galactic b/l to Radio Astronomy plugins
- // Unless we're receiving settings to display from a Radio Astronomy plugins
- if (!m_settings.m_link)
- {
- QList starTrackerPipes;
- MainCore::instance()->getMessagePipes().getMessagePipes(m_starTracker, "startracker.target", starTrackerPipes);
-
- for (const auto& pipe : starTrackerPipes)
+ else if ((m_settings.m_targetSource == "Horizons") && !raDecTargets.contains(m_settings.m_target))
{
- MessageQueue *messageQueue = qobject_cast(pipe->m_element);
- SWGSDRangel::SWGStarTrackerTarget *swgTarget = new SWGSDRangel::SWGStarTrackerTarget();
- swgTarget->setName(new QString(m_settings.m_target));
- swgTarget->setAzimuth(aa.az);
- swgTarget->setElevation(aa.alt);
- swgTarget->setRa(rd.ra);
- swgTarget->setDec(rd.dec);
- swgTarget->setB(b);
- swgTarget->setL(l);
- swgTarget->setSolarFlux(m_solarFlux);
- swgTarget->setAirTemperature(m_settings.m_temperature);
- double temp;
- m_starTracker->calcSkyTemperature(m_settings.m_frequency, m_settings.m_beamwidth, rd.ra, rd.dec, temp);
- swgTarget->setSkyTemperature(temp);
- swgTarget->setHpbw(m_settings.m_beamwidth);
- // Calculate velocities
- double vRot = Astronomy::earthRotationVelocity(rd, m_settings.m_latitude, m_settings.m_longitude, dt);
- swgTarget->setEarthRotationVelocity(vRot);
- double vOrbit = Astronomy::earthOrbitVelocityBCRS(rd,dt);
- swgTarget->setEarthOrbitVelocityBcrs(vOrbit);
- double vLSRK = Astronomy::sunVelocityLSRK(rd);
- swgTarget->setSunVelocityLsr(vLSRK);
- messageQueue->push(MainCore::MsgStarTrackerTarget::create(m_starTracker, swgTarget));
+ // Get RA/Dec from Horizons
+ if (getRAFromHorizons(dt, rdJ2000))
+ {
+ m_settings.m_ra = QString::number(rdJ2000.ra, 'f', 10);;
+ m_settings.m_dec = QString::number(rdJ2000.dec, 'f', 10);;
+ rdJnow = Astronomy::precess(rdJ2000, Astronomy::jd_j2000(), Astronomy::julianDate(dt));
+ rdJnowValid = true;
+ horizonsTarget = true;
+ }
}
- }
-
- // 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)
+ else if ((m_settings.m_target == "Custom RA/Dec") && 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);
+ rdJnow.ra = Units::raToDecimal(m_settings.m_ra);
+ rdJnow.dec = Units::decToDecimal(m_settings.m_dec);
+ rdJnowValid = true;
+ rdJ2000 = Astronomy::precess(rdJnow, Astronomy::julianDate(dt), Astronomy::jd_j2000());
}
else
{
- swgTarget->setRa(rd.ra);
- swgTarget->setDec(rd.dec);
+ rdJ2000.ra = Units::raToDecimal(m_settings.m_ra);
+ rdJ2000.dec = Units::decToDecimal(m_settings.m_dec);
+ rdJnowValid = true;
+ rdJnow = Astronomy::precess(rdJ2000, Astronomy::jd_j2000(), Astronomy::julianDate(dt));
}
- 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)
- {
- QList mapMessagePipes;
- MainCore::instance()->getMessagePipes().getMessagePipes(m_starTracker, "mapitems", mapMessagePipes);
-
- if (mapMessagePipes.size() > 0)
+ if (rdJnowValid)
{
- // Different between GMST(Lst at Greenwich) and RA
- double lst = Astronomy::localSiderealTime(dt, 0.0);
- double sunLongitude;
- double sunLatitude;
+ aa = Astronomy::raDecToAzAlt(rdJnow, m_settings.m_latitude, m_settings.m_longitude, dt, false);
+ Astronomy::equatorialToGalactic(rdJ2000.ra, rdJ2000.dec, l, b);
+ }
+ }
- if (m_settings.m_drawSunOnMap || m_settings.m_drawMoonOnMap)
+ if (rdJnowValid)
+ {
+ // Precess to J2000
+ RADec rdJ2000 = Astronomy::precess(rdJnow, Astronomy::julianDate(dt), Astronomy::jd_j2000());
+
+ // 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")
+ || m_settings.m_target.contains("SatelliteTracker") || m_settings.m_target.contains("SkyMap")
+ || lbTarget || horizonsTarget || (m_settings.m_targetSource == "SPICE"))
+ {
+ if (getMessageQueueToGUI())
{
- sunLongitude = Astronomy::lstAndRAToLongitude(lst, sunRD.ra);
- sunLatitude = sunRD.dec;
- sendToMap(mapMessagePipes, "Sun", "qrc:///startracker/startracker/sun-40.png", "Sun", sunLatitude, sunLongitude);
+ if (m_settings.m_jnow) {
+ getMessageQueueToGUI()->push(StarTrackerReport::MsgReportRADec::create(rdJnow.ra, rdJnow.dec, "target"));
+ } else {
+ getMessageQueueToGUI()->push(StarTrackerReport::MsgReportRADec::create(rdJ2000.ra, rdJ2000.dec, "target"));
+ }
}
- if (m_settings.m_drawMoonOnMap)
- {
- double moonLongitude = Astronomy::lstAndRAToLongitude(lst, moonRD.ra);
- double moonLatitude = moonRD.dec;
- double moonRotation;
- QString phase = moonPhase(sunLongitude, moonLongitude, m_settings.m_latitude, moonRotation);
- sendToMap(mapMessagePipes, "Moon", QString("qrc:///startracker/startracker/moon-%1-32").arg(phase), "Moon",
- moonLatitude, moonLongitude, moonRotation);
+ }
+
+ // Adjust for refraction
+ if (m_settings.m_refraction == "Positional Astronomy Library")
+ {
+ aa.alt += Astronomy::refractionPAL(aa.alt, m_settings.m_pressure, m_settings.m_temperature, m_settings.m_humidity,
+ m_settings.m_frequency, m_settings.m_latitude, m_settings.m_heightAboveSeaLevel,
+ m_settings.m_temperatureLapseRate);
+ if (aa.alt > 90.0) {
+ aa.alt = 90.0f;
}
- if ((m_settings.m_drawStarOnMap) && (m_settings.m_target != "Sun") && (m_settings.m_target != "Moon"))
+ }
+ else if (m_settings.m_refraction == "Saemundsson")
+ {
+ aa.alt += Astronomy::refractionSaemundsson(aa.alt, m_settings.m_pressure, m_settings.m_temperature);
+ if (aa.alt > 90.0) {
+ aa.alt = 90.0f;
+ }
+ }
+
+ // Add user-adjustment
+ aa.alt += m_settings.m_elevationOffset;
+ aa.az += m_settings.m_azimuthOffset;
+
+ // Send to GUI
+ if (getMessageQueueToGUI())
+ {
+ // MsgReportRADec sent in updateRaDec()
+ if (m_settings.m_target != "Custom Az/El") {
+ getMessageQueueToGUI()->push(StarTrackerReport::MsgReportAzAl::create(aa.az, aa.alt));
+ }
+ if (!lbTarget) {
+ getMessageQueueToGUI()->push(StarTrackerReport::MsgReportGalactic::create(l, b));
+ }
+ }
+
+ // Send Az/El to Rotator Controllers
+ // Unless we're receiving settings to display from a Radio Astronomy plugins
+ if (!m_settings.m_link)
+ {
+ QList rotatorPipes;
+ MainCore::instance()->getMessagePipes().getMessagePipes(m_starTracker, "target", rotatorPipes);
+
+ for (const auto& pipe : rotatorPipes)
{
- double starLongitude = Astronomy::lstAndRAToLongitude(lst, rd.ra);
- double starLatitude = rd.dec;
- QString text = m_settings.m_target.startsWith("Custom") ? "Star" : m_settings.m_target;
- sendToMap(mapMessagePipes, "Star", "qrc:///startracker/startracker/pulsar-32.png", text, starLatitude, starLongitude);
+ MessageQueue *messageQueue = qobject_cast(pipe->m_element);
+ SWGSDRangel::SWGTargetAzimuthElevation *swgTarget = new SWGSDRangel::SWGTargetAzimuthElevation();
+ swgTarget->setName(new QString(m_settings.m_target));
+ swgTarget->setAzimuth(aa.az);
+ swgTarget->setElevation(aa.alt);
+ messageQueue->push(MainCore::MsgTargetAzimuthElevation::create(m_starTracker, swgTarget));
+ }
+ }
+
+ // Send Az/El, RA/Dec and Galactic b/l to Radio Astronomy plugins
+ // Unless we're receiving settings to display from a Radio Astronomy plugins
+ if (!m_settings.m_link)
+ {
+ QList starTrackerPipes;
+ MainCore::instance()->getMessagePipes().getMessagePipes(m_starTracker, "startracker.target", starTrackerPipes);
+
+ for (const auto& pipe : starTrackerPipes)
+ {
+ MessageQueue *messageQueue = qobject_cast(pipe->m_element);
+ SWGSDRangel::SWGStarTrackerTarget *swgTarget = new SWGSDRangel::SWGStarTrackerTarget();
+ swgTarget->setName(new QString(m_settings.m_target));
+ swgTarget->setAzimuth(aa.az);
+ swgTarget->setElevation(aa.alt);
+ swgTarget->setRa(rdJnow.ra);
+ swgTarget->setDec(rdJnow.dec);
+ swgTarget->setB(b);
+ swgTarget->setL(l);
+ swgTarget->setSolarFlux(m_solarFlux);
+ swgTarget->setAirTemperature(m_settings.m_temperature);
+ double temp;
+ m_starTracker->calcSkyTemperature(m_settings.m_frequency, m_settings.m_beamwidth, rdJnow.ra, rdJnow.dec, temp);
+ swgTarget->setSkyTemperature(temp);
+ swgTarget->setHpbw(m_settings.m_beamwidth);
+ // Calculate velocities
+ double vRot = Astronomy::earthRotationVelocity(rdJnow, m_settings.m_latitude, m_settings.m_longitude, dt);
+ swgTarget->setEarthRotationVelocity(vRot);
+ double vOrbit = Astronomy::earthOrbitVelocityBCRS(rdJnow, dt);
+ swgTarget->setEarthOrbitVelocityBcrs(vOrbit);
+ double vLSRK = Astronomy::sunVelocityLSRK(rdJnow);
+ swgTarget->setSunVelocityLsr(vLSRK);
+ messageQueue->push(MainCore::MsgStarTrackerTarget::create(m_starTracker, swgTarget));
+ }
+ }
+
+ // 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();
+ RADec rdJ2000 = Astronomy::precess(rdJnow, Astronomy::julianDate(dt), Astronomy::jd_j2000());
+ swgTarget->setRa(rdJ2000.ra);
+ swgTarget->setDec(rdJ2000.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)
+ {
+ QList mapMessagePipes;
+ MainCore::instance()->getMessagePipes().getMessagePipes(m_starTracker, "mapitems", mapMessagePipes);
+
+ if (mapMessagePipes.size() > 0)
+ {
+ // Different between GMST(Lst at Greenwich) and RA
+ double lst = Astronomy::localSiderealTime(dt, 0.0);
+ double sunLongitude;
+ double sunLatitude;
+
+ if (m_settings.m_drawSunOnMap || m_settings.m_drawMoonOnMap)
+ {
+ sunLongitude = Astronomy::lstAndRAToLongitude(lst, sunRD.ra);
+ sunLatitude = sunRD.dec;
+ sendToMap(mapMessagePipes, "Sun", "qrc:///startracker/startracker/sun-40.png", "Sun", sunLatitude, sunLongitude);
+ }
+ if (m_settings.m_drawMoonOnMap)
+ {
+ double moonLongitude = Astronomy::lstAndRAToLongitude(lst, moonRD.ra);
+ double moonLatitude = moonRD.dec;
+ double moonRotation;
+ QString phase = moonPhase(sunLongitude, moonLongitude, m_settings.m_latitude, moonRotation);
+ sendToMap(mapMessagePipes, "Moon", QString("qrc:///startracker/startracker/moon-%1-32").arg(phase), "Moon",
+ moonLatitude, moonLongitude, moonRotation);
+ }
+ if ((m_settings.m_drawStarOnMap) && (m_settings.m_target != "Sun") && (m_settings.m_target != "Moon"))
+ {
+ double starLongitude = Astronomy::lstAndRAToLongitude(lst, rdJnow.ra);
+ double starLatitude = rdJnow.dec;
+ QString text = m_settings.m_target.startsWith("Custom") ? "Star" : m_settings.m_target;
+ sendToMap(mapMessagePipes, "Star", "qrc:///startracker/startracker/pulsar-32.png", text, starLatitude, starLongitude);
+ }
+ }
+ }
+
+ if (m_settings.m_chartSelect == StarTrackerSettings::CHART_SOLAR_SYSTEM) {
+ calculateSolarSystemPositions(dt);
+ }
+ if (m_settings.m_chartSelect == StarTrackerSettings::CHART_JUPITER) {
+ calculateJupiterParameters(dt);
+ }
+
+ // Calculate az/el of target over the current day for plotting on chart
+ // Can't do this for satellite tracker, as we only have current az/el
+ // Values from SkyMap map not be valid if object is moving (E.g. planets), as ra/dec is for one point in time
+
+ // Start at midnight or noon
+ QDateTime lt = dt.toLocalTime();
+ if (m_settings.m_night)
+ {
+ if (lt.time() < QTime(12, 0)) {
+ dt = QDateTime(lt.date().addDays(-1), QTime(12, 0));
+ } else {
+ dt = QDateTime(lt.date(), QTime(12, 0));
+ }
+ }
+ else
+ {
+ dt = QDateTime(lt.date(), QTime(0, 0));
+ }
+ //dt.setTimeZone(QTimeZone::LocalTime());
+
+ if ( (m_settings.m_target != m_chartTarget)
+ || (m_settings.m_latitude != m_chartLatitude) || (m_settings.m_longitude != m_chartLongitude)
+ || (dt != m_chartStartTime)
+ || (m_settings.m_target.startsWith("Custom"))
+ || ( (raDecTargets.contains(m_settings.m_target) || m_settings.m_target.contains("SkyMap")) // When switching these targets, we get separate settings updates for RA and Dec, so need to redraw
+ && ((m_settings.m_ra != m_chartRA) || (m_settings.m_dec != m_chartDec)) // We don't want to redraw when RA/Dec for Sun/Moon changes though, as that happens continuously
+ )
+ || ( lbTargets.contains(m_settings.m_target)
+ && ((m_settings.m_l != m_chartL) || (m_settings.m_b != m_chartB))
+ )
+ )
+ {
+ m_chartTarget = m_settings.m_target;
+ m_chartLatitude = m_settings.m_latitude;
+ m_chartLongitude = m_settings.m_longitude;
+ m_chartStartTime = dt;
+ m_chartRA = m_settings.m_ra;
+ m_chartDec = m_settings.m_dec;
+ m_chartL = m_settings.m_l;
+ m_chartB = m_settings.m_b;
+
+ if (!m_settings.m_target.contains("SatelliteTracker"))
+ {
+ int timestep = 5*60; // 5 mins
+
+ QList azimuths;
+ QList elevations;
+ QList dateTimes;
+
+ for (int step = 0; step <= 24*60*60/timestep; step++)
+ {
+ AzAlt aa;
+ RADec rdJnow, rdJ2000;
+
+ // Calculate elevation of desired object
+ if ((m_settings.m_target == "Sun") && (m_settings.m_targetSource == "SDRangel"))
+ {
+ Astronomy::sunPosition(aa, rdJnow, m_settings.m_latitude, m_settings.m_longitude, dt);
+ }
+ else if ((m_settings.m_target == "Moon") && (m_settings.m_targetSource == "SDRangel"))
+ {
+ Astronomy::moonPosition(aa, rdJnow, m_settings.m_latitude, m_settings.m_longitude, dt);
+ }
+ else if (m_settings.m_target == "Custom Az/El")
+ {
+ aa.alt = m_settings.m_el;
+ aa.az = m_settings.m_az;
+ }
+ else if (lbTargets.contains(m_settings.m_target))
+ {
+ Astronomy::galacticToEquatorial(m_settings.m_l, m_settings.m_b, rdJ2000.ra, rdJ2000.dec);
+ aa = Astronomy::raDecToAzAlt(rdJ2000, m_settings.m_latitude, m_settings.m_longitude, dt, true);
+ }
+ else if (m_settings.m_targetSource == "SPICE")
+ {
+ double et;
+
+ if (dateTimeToET(dt, et)) {
+ getAzElFromSPICE(m_settings.m_target, et, m_settings.m_latitude, m_settings.m_longitude, 0.0, aa.az, aa.alt);
+ }
+ }
+ else
+ {
+ if (m_settings.m_target.contains("SkyMap"))
+ {
+ getRAFromSkyMap(rdJ2000);
+ }
+ else if ((m_settings.m_targetSource == "Horizons") && !raDecTargets.contains(m_settings.m_target))
+ {
+ getRAFromHorizons(dt, rdJ2000);
+ }
+ else if ((m_settings.m_target == "Custom RA/Dec") && m_settings.m_jnow)
+ {
+ rdJnow.ra = Units::raToDecimal(m_settings.m_ra);
+ rdJnow.dec = Units::decToDecimal(m_settings.m_dec);
+ rdJ2000 = Astronomy::precess(rdJnow, Astronomy::julianDate(dt), Astronomy::jd_j2000());
+ }
+ else
+ {
+ rdJ2000.ra = Units::raToDecimal(m_settings.m_ra);
+ rdJ2000.dec = Units::decToDecimal(m_settings.m_dec);
+ }
+ aa = Astronomy::raDecToAzAlt(rdJ2000, m_settings.m_latitude, m_settings.m_longitude, dt, !m_settings.m_jnow);
+ }
+
+ azimuths.append(aa.az);
+ elevations.append(aa.alt);
+ dateTimes.append(dt);
+
+ dt = dt.addSecs(timestep); // addSecs accounts for daylight savings jumps
+ }
+
+ getMessageQueueToGUI()->push(StarTrackerReport::MsgReportAzElVsTime::create(m_settings.m_target, azimuths, elevations, dateTimes));
}
}
}
+ spiceUnlock();
+
+ PROFILER_STOP("StarTrackerWorker");
+}
+
+void StarTrackerWorker::calculateSolarSystemPositions(const QDateTime& dateTime)
+{
+ QStringList bodyNames;
+ QList bodyPositions;
+
+ double et;
+ if (!dateTimeToET(dateTime, et)) {
+ return;
+ }
+
+ for (const auto& body : m_settings.m_solarSystemBodies)
+ {
+ QVector3D positionKm;
+
+ if (getSSBPositionFromSPICE(body, et, positionKm))
+ {
+ QVector3D position(positionKm[0], positionKm[1], positionKm[2]);
+
+ bodyNames.append(body);
+ bodyPositions.append(position);
+ }
+ }
+
+ if (getMessageQueueToGUI()) {
+ getMessageQueueToGUI()->push(StarTrackerReport::MsgReportSolarSystemPositions::create(dateTime, bodyNames, bodyPositions));
+ }
+}
+
+void StarTrackerWorker::calculateJupiterParameters(const QDateTime& dateTime)
+{
+ double cml;
+ double ioPhase;
+ double ganymedePhase;
+ double et;
+ double az, el;
+ bool jupiterVisibleNow = false;
+ QDateTime dt;
+ bool done;
+
+ QString moon = m_settings.m_chartSubSelect == 0 ? "IO" : "GANYMEDE";
+ double moonPhase;
+
+ QList jupiterData;
+ QList moonData;
+
+ PROFILER_START();
+
+ if (dateTimeToET(dateTime, et))
+ {
+ // Calculate Alt/El of Jupiter
+ if (getAzElFromSPICE("JUPITER", et, m_settings.m_latitude, m_settings.m_longitude, 0.0, az, el))
+ {
+ // Calculate Jupiter CML and phase of Moons
+ if (calculateJupiterMoonPhase("IO", et, m_settings.m_latitude, m_settings.m_longitude, 0.0, cml, ioPhase))
+ {
+ if (calculateJupiterMoonPhase("GANYMEDE", et, m_settings.m_latitude, m_settings.m_longitude, 0.0, cml, ganymedePhase))
+ {
+ if (getMessageQueueToGUI()) {
+ getMessageQueueToGUI()->push(StarTrackerReport::MsgReportJupiter::create(dateTime, az, el, cml, ioPhase, ganymedePhase));
+ }
+ }
+ }
+ jupiterVisibleNow = el > 0.0;
+ }
+ }
+
+ if (jupiterVisibleNow)
+ {
+ // Step backwards in time
+ dt = dateTime;
+ dt.setTime(QTime(dateTime.time().hour(), 0));
+ done = false;
+ while (!done)
+ {
+ if (dateTimeToET(dt, et))
+ {
+ if (getAzElFromSPICE("JUPITER", et, m_settings.m_latitude, m_settings.m_longitude, 0.0, az, el))
+ {
+ if (el > 0.0)
+ {
+ if (calculateJupiterMoonPhase(moon, et, m_settings.m_latitude, m_settings.m_longitude, 0.0, cml, moonPhase))
+ {
+ StarTrackerReport::JupiterData jd = {dt, az, el};
+ StarTrackerReport::JupiterMoonData md = {cml, moonPhase};
+
+ jupiterData.push_front(jd);
+ moonData.push_front(md);
+ }
+ }
+ else
+ {
+ done = true;
+ }
+ }
+ }
+ dt = dt.addSecs(-60*60);
+ }
+
+ // Step forwards in time
+ dt = dateTime;
+ dt.setTime(QTime(dateTime.time().hour(), 0));
+ done = false;
+ while (!done)
+ {
+ dt = dt.addSecs(60*60);
+ if (dateTimeToET(dt, et))
+ {
+ if (getAzElFromSPICE("JUPITER", et, m_settings.m_latitude, m_settings.m_longitude, 0.0, az, el))
+ {
+ if (el > 0.0)
+ {
+ if (calculateJupiterMoonPhase(moon, et, m_settings.m_latitude, m_settings.m_longitude, 0.0, cml, moonPhase))
+ {
+ StarTrackerReport::JupiterData jd = {dt, az, el};
+ StarTrackerReport::JupiterMoonData id = {cml, moonPhase};
+
+ jupiterData.push_back(jd);
+ moonData.push_back(id);
+ }
+ }
+ else
+ {
+ done = true;
+ }
+ }
+ }
+ }
+ }
+ else
+ {
+ // Step forwards in time until Jupiter is visible
+ dt = dateTime;
+ dt.setTime(QTime(dateTime.time().hour(), 0));
+ done = false;
+ while (!done)
+ {
+ dt = dt.addSecs(60*60);
+ if (dateTimeToET(dt, et))
+ {
+ if (getAzElFromSPICE("JUPITER", et, m_settings.m_latitude, m_settings.m_longitude, 0.0, az, el))
+ {
+ if (el > 0.0) {
+ done = true;
+ }
+ }
+ }
+ }
+
+ // Continue stepping forward until no longer visible
+ done = false;
+ while (!done)
+ {
+ if (dateTimeToET(dt, et))
+ {
+ if (getAzElFromSPICE("JUPITER", et, m_settings.m_latitude, m_settings.m_longitude, 0.0, az, el))
+ {
+ if (el > 0.0)
+ {
+ if (calculateJupiterMoonPhase(moon, et, m_settings.m_latitude, m_settings.m_longitude, 0.0, cml, moonPhase))
+ {
+ StarTrackerReport::JupiterData jd = {dt, az, el};
+ StarTrackerReport::JupiterMoonData id = {cml, moonPhase};
+
+ jupiterData.push_back(jd);
+ moonData.push_back(id);
+ }
+ }
+ else
+ {
+ done = true;
+ }
+ }
+ }
+ dt = dt.addSecs(60*60);
+ }
+
+ }
+
+ if (getMessageQueueToGUI()) {
+ getMessageQueueToGUI()->push(StarTrackerReport::MsgReportJupiterData::create(jupiterData, moonData));
+ }
+
+ PROFILER_STOP("calculateJupiterParameters");
+}
+
+void StarTrackerWorker::horizonsEphemeridesUpdated(const QString &target, const QList& ephemerides)
+{
+ m_ephemeridesTarget = target;
+ m_horizonsEphemerides = ephemerides;
}
diff --git a/plugins/feature/startracker/startrackerworker.h b/plugins/feature/startracker/startrackerworker.h
index 0f3ef0b98..a9e13726e 100644
--- a/plugins/feature/startracker/startrackerworker.h
+++ b/plugins/feature/startracker/startrackerworker.h
@@ -1,6 +1,6 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019-2020, 2022 Edouard Griffiths, F4EXB //
-// Copyright (C) 2020-2022 Jon Beniston, M7RCE //
+// Copyright (C) 2021-2026 Jon Beniston, M7RCE //
// Copyright (C) 2020 Vort //
// Copyright (C) 2022 Jiří Pinkava //
// //
@@ -28,6 +28,7 @@
#include "util/message.h"
#include "util/messagequeue.h"
#include "util/astronomy.h"
+#include "util/jplhorizons.h"
#include "startrackersettings.h"
@@ -90,11 +91,28 @@ private:
QTcpSocket *m_clientConnection;
float m_solarFlux;
+ JPLHorizons *m_jplHorizons;
+ QString m_ephemeridesTarget;
+ QList m_horizonsEphemerides;
+ QString m_requestedEphemeridesTarget;
+ QDateTime m_requestedEphemeridesStartTime;
+ QDateTime m_requestedEphemeridesStopTime;
+ double m_requestedEphemeridesLatitude;
+ double m_requestedEphemeridesLongitude;
+
+ QString m_chartTarget;
+ double m_chartLatitude;
+ double m_chartLongitude;
+ QDateTime m_chartStartTime;
+ QString m_chartRA;
+ QString m_chartDec;
+ float m_chartL;
+ float m_chartB;
+
bool handleMessage(const Message& cmd);
void applySettings(const StarTrackerSettings& settings, const QList& settingsKeys, bool force = false);
void restartServer(bool enabled, uint32_t port);
MessageQueue *getMessageQueueToGUI() { return m_msgQueueToGUI; }
- void updateRaDec(RADec rd, QDateTime dt, bool lbTarget);
void writeStellariumTarget(double ra, double dec);
void removeFromMap(QString id);
void sendToMap(
@@ -106,6 +124,11 @@ private:
double lon,
double rotation = 0.0
);
+ bool getAzElFromSatelliteTracker(double &azimuth, double &elevation);
+ bool getRAFromSkyMap(RADec &rdJ2000);
+ bool getRAFromHorizons(const QDateTime &dateTime, RADec &rdJ2000);
+ void calculateSolarSystemPositions(const QDateTime& dateTime);
+ void calculateJupiterParameters(const QDateTime& dateTime);
private slots:
void handleInputMessages();
@@ -114,6 +137,7 @@ private slots:
void disconnected();
void errorOccurred(QAbstractSocket::SocketError socketError);
void readStellariumCommand();
+ void horizonsEphemeridesUpdated(const QString &target, const QList& ephemerides);
};
#endif // INCLUDE_FEATURE_STARTRACKERWORKER_H_
diff --git a/sdrbase/CMakeLists.txt b/sdrbase/CMakeLists.txt
index 923629827..eabe38a37 100644
--- a/sdrbase/CMakeLists.txt
+++ b/sdrbase/CMakeLists.txt
@@ -246,6 +246,7 @@ set(sdrbase_SOURCES
util/grb.cpp
util/httpdownloadmanager.cpp
util/interpolation.cpp
+ util/jplhorizons.cpp
util/kiwisdrlist.cpp
util/lfsr.cpp
util/maidenhead.cpp
@@ -511,6 +512,7 @@ set(sdrbase_HEADERS
util/incrementalarray.h
util/incrementalvector.h
util/interpolation.h
+ util/jplhorizons.h
util/kiwisdrlist.h
util/lfsr.h
util/maidenhead.h
diff --git a/sdrbase/util/astronomy.cpp b/sdrbase/util/astronomy.cpp
index b64a69bb4..d588b50a8 100644
--- a/sdrbase/util/astronomy.cpp
+++ b/sdrbase/util/astronomy.cpp
@@ -190,7 +190,7 @@ double Astronomy::localSiderealTime(QDateTime dateTime, double longitude)
return fmod(100.46 + 0.985647 * d + longitude + (360/24) * ut, 360.0);
}
-// Convert from J2000 right ascension (decimal hours) and declination (decimal degrees) to altitude and azimuth, for location (decimal degrees) and time
+// Convert from right ascension (decimal hours) and declination (decimal degrees) to altitude and azimuth, for location (decimal degrees) and time
AzAlt Astronomy::raDecToAzAlt(RADec rd, double latitude, double longitude, QDateTime dt, bool j2000)
{
AzAlt aa;
@@ -475,6 +475,7 @@ double Astronomy::modulo(double a, double b)
}
// Calculate azimuth and altitude angles to the sun from the given latitude and longitude at the given time
+// Jnow RADec
// Refer to:
// https://en.wikipedia.org/wiki/Position_of_the_Sun
// https://www.aa.quae.nl/en/reken/zonpositie.html
@@ -530,6 +531,7 @@ double Astronomy::moonDays(QDateTime dt)
}
// Calculate azimuth and altitude angles to the moon from the given latitude and longitude at the given time
+// Jnow RADec
// Refer to: https://stjarnhimlen.se/comp/ppcomp.html
// Accurate to 4 arcminute
void Astronomy::moonPosition(AzAlt& aa, RADec& rd, double latitude, double longitude, QDateTime dt)
diff --git a/sdrbase/util/aurora.cpp b/sdrbase/util/aurora.cpp
index 21112f674..94326d216 100644
--- a/sdrbase/util/aurora.cpp
+++ b/sdrbase/util/aurora.cpp
@@ -295,7 +295,7 @@ Aurora::Aurora()
}
m_cache = new QNetworkDiskCache();
- m_cache->setCacheDirectory(locations[0] + QDir::separator() + QStringLiteral("cache") + QDir::separator() + QStringLiteral("giro"));
+ m_cache->setCacheDirectory(locations[0] + QDir::separator() + QStringLiteral("cache") + QDir::separator() + QStringLiteral("aurora"));
m_cache->setMaximumCacheSize(100000000);
m_networkManager->setCache(m_cache);
}
diff --git a/sdrbase/util/jplhorizons.cpp b/sdrbase/util/jplhorizons.cpp
new file mode 100644
index 000000000..923f68c0c
--- /dev/null
+++ b/sdrbase/util/jplhorizons.cpp
@@ -0,0 +1,230 @@
+///////////////////////////////////////////////////////////////////////////////////
+// Copyright (C) 2026 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 "jplhorizons.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "util/units.h"
+
+const QString JPLHorizons::m_majorBodiesURL = QStringLiteral("https://ssd.jpl.nasa.gov/api/horizons.api?COMMAND=MB&format=text");
+QMutex JPLHorizons::m_mutex;
+QHash JPLHorizons::m_bodies;
+
+JPLHorizons::JPLHorizons()
+{
+ m_networkManager = new QNetworkAccessManager();
+ connect(m_networkManager, &QNetworkAccessManager::finished, this, &JPLHorizons::handleReply);
+
+ QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
+ QDir writeableDir(locations[0]);
+ if (!writeableDir.mkpath(QStringLiteral("cache") + QDir::separator() + QStringLiteral("jplhorizons"))) {
+ qDebug() << "Failed to create cache/jplhorizons";
+ }
+
+ m_cache = new QNetworkDiskCache();
+ m_cache->setCacheDirectory(locations[0] + QDir::separator() + QStringLiteral("cache") + QDir::separator() + QStringLiteral("jplhorizons"));
+ m_cache->setMaximumCacheSize(10000000);
+ m_networkManager->setCache(m_cache);
+}
+
+JPLHorizons::~JPLHorizons()
+{
+ disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &JPLHorizons::handleReply);
+ delete m_networkManager;
+}
+
+JPLHorizons* JPLHorizons::create()
+{
+ return new JPLHorizons();
+}
+
+void JPLHorizons::getData(const QString& target, QDateTime startDateTime, QDateTime stopDateTime, double latitude, double longitude, double altitudeKM)
+{
+ QMutexLocker locker(&m_mutex);
+
+ QString id;
+ if (m_bodies.contains(target)) {
+ id = QString::number(m_bodies.value(target).m_id);
+ } else {
+ id = target;
+ }
+
+ QString urlString = QString("https://ssd.jpl.nasa.gov/api/horizons.api?format=text&COMMAND='%1'&OBJ_DATA='NO'&MAKE_EPHEM='YES'&EPHEM_TYPE='OBSERVER'&START_TIME='%2'&STOP_TIME='%3'&STEP_SIZE='%4'&EMAIL_ADDR=sdrangel@sdrangel.org&CSV_FORMAT=NO&CENTER='COORD'&SITE_COORD='%5,%6,%7'")
+ .arg(id)
+ .arg(startDateTime.toString("yyyy-MM-dd hh:mm"))
+ .arg(stopDateTime.toString("yyyy-MM-dd hh:mm"))
+ .arg("10 min")
+ .arg(longitude)
+ .arg(latitude)
+ .arg(altitudeKM)
+ ;
+ qDebug() << urlString;
+ QUrl url(urlString);
+ m_networkManager->get(QNetworkRequest(url));
+}
+
+void JPLHorizons::getMajorBodiesList()
+{
+ QMutexLocker locker(&m_mutex);
+
+ if (m_bodies.isEmpty())
+ {
+ QUrl url(m_majorBodiesURL);
+ m_networkManager->get(QNetworkRequest(url));
+ }
+ else
+ {
+ emit majorBodiesUpdated(m_bodies);
+ }
+}
+
+bool JPLHorizons::getBodyId(const QString& body, int &id)
+{
+ QMutexLocker locked(&m_mutex);
+
+ if (m_bodies.contains(body))
+ {
+ id = m_bodies.value(body).m_id;
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+}
+
+void JPLHorizons::handleReply(QNetworkReply* reply)
+{
+ if (reply)
+ {
+ if (!reply->error())
+ {
+ QByteArray bytes = reply->readAll();
+
+ if (reply->url() == m_majorBodiesURL) {
+ handleMajorBodiesList(bytes);
+ } else {
+ handleEphemerides(bytes);
+ }
+ }
+ else
+ {
+ qDebug() << "JPLHorizons::handleReply: error: " << reply->error();
+ }
+ reply->deleteLater();
+ }
+ else
+ {
+ qDebug() << "JPLHorizons::handleReply: reply is null";
+ }
+}
+
+void JPLHorizons::handleEphemerides(const QByteArray& bytes)
+{
+ QString file = QString::fromLatin1(bytes);
+
+ QRegularExpression targetRE(R"(Target body name: ([A-Za-z\d' \(\)]+)\(-?\d+\))");
+ QRegularExpressionMatch match = targetRE.match(file);
+ int soe = file.indexOf("$$SOE");
+ int eoe = file.indexOf("$$EOE");
+
+ if (match.hasMatch() && (soe >= 0) && (eoe >= soe))
+ {
+ QString target = match.captured(1).trimmed();
+
+ QList ephemerides;
+ soe += 6;
+ QString text = file.mid(soe, eoe - soe - 1);
+ QStringList lines = text.split("\n");
+
+ for (int i = 0; i < lines.size(); i++)
+ {
+ QString& line = lines[i];
+
+ QDate date = QDate::fromString(line.mid(1, 11), "yyyy-MMM-dd");
+ QTime time = QTime::fromString(line.mid(13, 5), "hh:mm");
+
+ float ra;
+ float dec;
+ bool ok = Units::stringToRADec(line.mid(23, 23), ra, dec);
+
+ if (date.isValid() && time.isValid() && ok)
+ {
+ Ephemeris ephemeris;
+ ephemeris.m_dateTime = QDateTime(date, time);
+ ephemeris.m_ra = ra;
+ ephemeris.m_dec = dec;
+ ephemerides.append(ephemeris);
+ }
+ else
+ {
+ qDebug() << "JPLHorizons::handleEphemerides: Failed to parse line:" << line << date.toString() << time.toString();
+ }
+ }
+
+ emit ephemeridesUpdated(target, ephemerides);
+ }
+ else
+ {
+ qDebug() << "JPLHorizons::handleEphemerides: Failed to parse file" << file;
+ }
+}
+
+void JPLHorizons::handleMajorBodiesList(const QByteArray& bytes)
+{
+ QHash bodies;
+
+ QString file = QString::fromLatin1(bytes);
+ QString separator = "------- ---------------------------------- ----------- -------------------";
+ file = file.mid(file.indexOf(separator) + separator.size() + 1);
+
+ QStringList lines = file.split("\n");
+
+ for (int i = 0; i < lines.size(); i++)
+ {
+ QString& line = lines[i];
+ if (line.size() == 80)
+ {
+ bool ok;
+ int id = line.mid(0, 9).trimmed().toInt(&ok);
+ QString name = line.mid(11, 34).trimmed();
+ QString designation = line.mid(46, 11).trimmed();
+ if (ok && !name.isEmpty())
+ {
+ BodyID body;
+ body.m_id = id;
+ body.m_name = name;
+ body.m_designation = designation;
+ bodies.insert(body.m_name, body);
+ }
+ }
+ }
+ if (bodies.isEmpty()) {
+ qDebug() << "JPLHorizons::handleMajorBodiesList: Failed to parse" << file;
+ }
+
+ QMutexLocker locker(&m_mutex);
+ m_bodies = bodies;
+ emit majorBodiesUpdated(bodies);
+}
diff --git a/sdrbase/util/jplhorizons.h b/sdrbase/util/jplhorizons.h
new file mode 100644
index 000000000..d0e809f1a
--- /dev/null
+++ b/sdrbase/util/jplhorizons.h
@@ -0,0 +1,79 @@
+///////////////////////////////////////////////////////////////////////////////////
+// Copyright (C) 2026 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_JPLHORIZONS_H
+#define INCLUDE_JPLHORIZONS_H
+
+#include
+
+#include "export.h"
+
+class QNetworkAccessManager;
+class QNetworkReply;
+class QNetworkDiskCache;
+
+// NASA JPL Horizons Ephemeris
+class SDRBASE_API JPLHorizons : public QObject
+{
+ Q_OBJECT
+protected:
+ JPLHorizons();
+
+public:
+ struct Ephemeris {
+ QDateTime m_dateTime;
+ float m_ra;
+ float m_dec;
+ };
+
+ struct BodyID {
+ int m_id;
+ QString m_name;
+ QString m_designation;
+ };
+
+ static JPLHorizons* create();
+
+ ~JPLHorizons();
+ void getMajorBodiesList();
+ bool getBodyId(const QString& body, int &id);
+
+public slots:
+ void getData(const QString& target, QDateTime startDateTime, QDateTime stopDateTime, double latitude, double longitude, double altitudeKM);
+
+private slots:
+ void handleReply(QNetworkReply* reply);
+
+signals:
+ void ephemeridesUpdated(const QString &target, const QList& ephemerides); // Called when new data available.
+ void majorBodiesUpdated(const QHash& bodies);
+
+private:
+ void handleEphemerides(const QByteArray& bytes);
+ void handleMajorBodiesList(const QByteArray& bytes);
+
+ QNetworkAccessManager *m_networkManager;
+ QNetworkDiskCache *m_cache;
+
+ static QMutex m_mutex;
+ static QHash m_bodies;
+ static const QString m_majorBodiesURL;
+
+};
+
+#endif /* INCLUDE_JPLHORIZONS_H */
+
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index ca16a94d8..5cdfea82b 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -150,7 +150,7 @@ parts:
override-pull: |
snapcraftctl pull
snapcraftctl set-version "$(git describe --tags --abbrev=0 | sed 's/v//')"
- after: [apt, libdab, mbelib, serialdv, dsdcc, codec2, sgp4, inmarsatc, cm265cc, libsigmf, airspy, rtlsdr, pluto, bladerf, hackrf, limesuite, airspyhf, uhd, uhdfpga, soapysdr, soapyremote]
+ after: [apt, libdab, mbelib, serialdv, dsdcc, codec2, sgp4, cspice, inmarsatc, cm265cc, libsigmf, airspy, rtlsdr, pluto, bladerf, hackrf, limesuite, airspyhf, uhd, uhdfpga, soapysdr, soapyremote]
cmake-parameters:
- -DDEBUG_OUTPUT=OFF
- -DBUILD_TYPE=RELEASE
@@ -175,6 +175,7 @@ parts:
- -DMBE_DIR=$SNAPCRAFT_STAGE/opt/install/sdrangel
- -DCODEC2_DIR=$SNAPCRAFT_STAGE/opt/install/sdrangel
- -DSGP4_DIR=$SNAPCRAFT_STAGE/opt/install/sdrangel
+ - -DCSPICE_DIR=$SNAPCRAFT_STAGE/opt/install/sdrangel
- -DINMARSATC_DIR=$SNAPCRAFT_STAGE/opt/install/sdrangel
- -DLIBSIGMF_DIR=$SNAPCRAFT_STAGE/opt/install/sdrangel
- -DDAB_DIR=$SNAPCRAFT_STAGE/opt/install/sdrangel
@@ -476,6 +477,14 @@ parts:
cmake-parameters:
- -DCMAKE_INSTALL_PREFIX=/opt/install/sdrangel
+ cspice:
+ plugin: cmake
+ source: https://github.com/srcejon/cspice-cmake
+ source-type: git
+ source-tag: msvc
+ cmake-parameters:
+ - -DCMAKE_INSTALL_PREFIX=/opt/install/sdrangel
+
inmarsatc:
plugin: cmake
source: https://github.com/srcejon/inmarsatc.git