From 200faad519f2ca857045c57fa8b02b7124bf4df4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Zara=C5=9B?= Date: Thu, 21 May 2026 12:04:44 +0200 Subject: [PATCH 1/5] Wire in built-in Planes plugin --- CMakeLists.txt | 1 + cmake/default_cfg.ini.cmake | 1 + src/core/StelApp.cpp | 4 ++++ 3 files changed, 6 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 29ba1e88e8a51..544a1dbc9a64f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -571,6 +571,7 @@ ADD_PLUGIN(Observability 1) ADD_PLUGIN(Oculars 1) ADD_PLUGIN(Oculus 0) ADD_PLUGIN(OnlineQueries 1) +ADD_PLUGIN(Planes 1) ADD_PLUGIN(PointerCoordinates 1) ADD_PLUGIN(Pulsars 1) ADD_PLUGIN(Quasars 1) diff --git a/cmake/default_cfg.ini.cmake b/cmake/default_cfg.ini.cmake index f54d11b1ce658..4a20c2c76ffa4 100644 --- a/cmake/default_cfg.ini.cmake +++ b/cmake/default_cfg.ini.cmake @@ -10,6 +10,7 @@ Exoplanets = true MeteorShowers = true Novae = true FOV = true +Planes = false [video] fullscreen = true diff --git a/src/core/StelApp.cpp b/src/core/StelApp.cpp index 7d72a781314d8..154c4bd8360f6 100644 --- a/src/core/StelApp.cpp +++ b/src/core/StelApp.cpp @@ -224,6 +224,10 @@ Q_IMPORT_PLUGIN(VtsStelPluginInterface) Q_IMPORT_PLUGIN(OnlineQueriesPluginInterface) #endif +#ifdef USE_STATIC_PLUGIN_PLANES +Q_IMPORT_PLUGIN(PlanesStelPluginInterface) +#endif + #ifdef USE_STATIC_PLUGIN_NEBULATEXTURES Q_IMPORT_PLUGIN(NebulaTexturesStelPluginInterface) #endif From 97cc08901f9f2c6e6cb8e4330e5bfdb5fe236002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Zara=C5=9B?= Date: Thu, 21 May 2026 12:06:23 +0200 Subject: [PATCH 2/5] Add live ADS-B aircraft plugin --- plugins/Planes/CMakeLists.txt | 1 + plugins/Planes/module.ini | 4 + plugins/Planes/resources/Planes.qrc | 7 + plugins/Planes/resources/plane.png | Bin 0 -> 19024 bytes plugins/Planes/resources/planes_off_160.png | Bin 0 -> 4407 bytes plugins/Planes/resources/planes_on_160.png | Bin 0 -> 4407 bytes plugins/Planes/src/AircraftObject.cpp | 299 ++++++++ plugins/Planes/src/AircraftObject.hpp | 53 ++ plugins/Planes/src/AircraftRecord.hpp | 20 + plugins/Planes/src/CMakeLists.txt | 57 ++ plugins/Planes/src/Planes.cpp | 718 ++++++++++++++++++++ plugins/Planes/src/Planes.hpp | 136 ++++ plugins/Planes/src/gui/PlanesDialog.cpp | 241 +++++++ plugins/Planes/src/gui/PlanesDialog.hpp | 44 ++ plugins/Planes/src/gui/planesDialog.ui | 245 +++++++ 15 files changed, 1825 insertions(+) create mode 100644 plugins/Planes/CMakeLists.txt create mode 100644 plugins/Planes/module.ini create mode 100644 plugins/Planes/resources/Planes.qrc create mode 100644 plugins/Planes/resources/plane.png create mode 100644 plugins/Planes/resources/planes_off_160.png create mode 100644 plugins/Planes/resources/planes_on_160.png create mode 100644 plugins/Planes/src/AircraftObject.cpp create mode 100644 plugins/Planes/src/AircraftObject.hpp create mode 100644 plugins/Planes/src/AircraftRecord.hpp create mode 100644 plugins/Planes/src/CMakeLists.txt create mode 100644 plugins/Planes/src/Planes.cpp create mode 100644 plugins/Planes/src/Planes.hpp create mode 100644 plugins/Planes/src/gui/PlanesDialog.cpp create mode 100644 plugins/Planes/src/gui/PlanesDialog.hpp create mode 100644 plugins/Planes/src/gui/planesDialog.ui diff --git a/plugins/Planes/CMakeLists.txt b/plugins/Planes/CMakeLists.txt new file mode 100644 index 0000000000000..4b7537b554d72 --- /dev/null +++ b/plugins/Planes/CMakeLists.txt @@ -0,0 +1 @@ +ADD_SUBDIRECTORY(src) diff --git a/plugins/Planes/module.ini b/plugins/Planes/module.ini new file mode 100644 index 0000000000000..1bf1382346d4a --- /dev/null +++ b/plugins/Planes/module.ini @@ -0,0 +1,4 @@ +[module] +id=Planes +version=0.1.0 +stellarium-minimum-version=26.1.0 diff --git a/plugins/Planes/resources/Planes.qrc b/plugins/Planes/resources/Planes.qrc new file mode 100644 index 0000000000000..bba4dbcd2cd29 --- /dev/null +++ b/plugins/Planes/resources/Planes.qrc @@ -0,0 +1,7 @@ + + + plane.png + planes_off_160.png + planes_on_160.png + + diff --git a/plugins/Planes/resources/plane.png b/plugins/Planes/resources/plane.png new file mode 100644 index 0000000000000000000000000000000000000000..321e38716b9e53429fd276705c5c28ce4c2478d6 GIT binary patch literal 19024 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 a%Bl#={}A-#Oz)o&0KKb5I`vx4@&5(;Z_uy+ literal 0 HcmV?d00001 diff --git a/plugins/Planes/resources/planes_on_160.png b/plugins/Planes/resources/planes_on_160.png new file mode 100644 index 0000000000000000000000000000000000000000..8aa3f4e16bb359d71c76ac6c0b2fb12e38bc7128 GIT binary patch literal 4407 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 +#include + +namespace +{ +constexpr double kEarthFlattening = 1.0 / 298.257223563; +constexpr double kEarthRadiusMeters = 6378137.0; +constexpr double kSecondsPerDay = 86400.0; +constexpr double kMaxDeadReckoningSeconds = 30.0; +constexpr double kHeadingProbeSeconds = 1.0; +constexpr double kMetersToFeet = 3.280839895; +constexpr double kMetersPerSecondToKnots = 1.943844492; +constexpr float kPlaneSpriteSize = 16.0f; +constexpr float kSpriteHeadingOffsetDegrees = -180.0f; + +Vec3d toEcef(double latitudeRad, double longitudeRad, double altitudeMeters) +{ + const double sinLat = std::sin(latitudeRad); + const double c = 1.0 / std::sqrt(1.0 + kEarthFlattening * (kEarthFlattening - 2.0) * (sinLat * sinLat)); + const double sq = (1.0 - kEarthFlattening) * (1.0 - kEarthFlattening) * c; + const double radius = (kEarthRadiusMeters * c + altitudeMeters) * std::cos(latitudeRad); + + return Vec3d(radius * std::cos(longitudeRad), + radius * std::sin(longitudeRad), + (kEarthRadiusMeters * sq + altitudeMeters) * sinLat); +} + +double normalizeLongitudeRadians(double longitudeRad) +{ + while (longitudeRad > M_PI) + longitudeRad -= 2.0 * M_PI; + while (longitudeRad < -M_PI) + longitudeRad += 2.0 * M_PI; + return longitudeRad; +} + +double normalizeDegrees(double degrees) +{ + while (degrees >= 360.0) + degrees -= 360.0; + while (degrees < 0.0) + degrees += 360.0; + return degrees; +} + +QString headingToCompass(double degrees) +{ + static const char* labels[] = {"N", "NE", "E", "SE", "S", "SW", "W", "NW"}; + const int index = static_cast(std::floor((normalizeDegrees(degrees) + 22.5) / 45.0)) % 8; + return QString::fromLatin1(labels[index]); +} +} + +const QString AircraftObject::STEL_TYPE = QStringLiteral("Flight"); + +AircraftObject::AircraftObject(const AircraftRecord& record) + : aircraftRecord(record) +{ +} + +void AircraftObject::updateRecord(const AircraftRecord& record) +{ + aircraftRecord = record; +} + +QString AircraftObject::getObjectTypeI18n() const +{ + return q_(getObjectType()); +} + +QString AircraftObject::getID() const +{ + return aircraftRecord.icao24; +} + +QString AircraftObject::getEnglishName() const +{ + if (!aircraftRecord.callsign.isEmpty()) + return aircraftRecord.callsign; + return aircraftRecord.icao24; +} + +QString AircraftObject::getNameI18n() const +{ + return getEnglishName(); +} + +QString AircraftObject::labelText() const +{ + if (!aircraftRecord.callsign.isEmpty()) + return aircraftRecord.callsign; + if (!aircraftRecord.aircraftType.isEmpty()) + return QString("%1 (%2)").arg(aircraftRecord.icao24, aircraftRecord.aircraftType); + return aircraftRecord.icao24; +} + +QString AircraftObject::displayLabelText(int labelMode) const +{ + if (labelMode == 1 && !aircraftRecord.aircraftType.isEmpty()) + return aircraftRecord.aircraftType; + return labelText(); +} + +double AircraftObject::getElapsedSeconds() const +{ + if (aircraftRecord.snapshotJd <= 0.0) + return 0.0; + + const double elapsedSeconds = (StelUtils::getJDFromSystem() - aircraftRecord.snapshotJd) * kSecondsPerDay; + if (elapsedSeconds <= 0.0) + return 0.0; + return qMin(elapsedSeconds, kMaxDeadReckoningSeconds); +} + +AircraftRecord AircraftObject::getExtrapolatedRecord(double elapsedSeconds) const +{ + AircraftRecord record = aircraftRecord; + if (elapsedSeconds <= 0.0 || record.groundSpeedMs <= 0.0) + { + record.altitudeMeters = qMax(0.0, record.altitudeMeters + record.verticalRateMs * elapsedSeconds); + return record; + } + + const double lat1 = record.latitude * M_PI / 180.0; + const double lon1 = record.longitude * M_PI / 180.0; + const double bearing = normalizeDegrees(record.trackDegrees) * M_PI / 180.0; + const double angularDistance = (record.groundSpeedMs * elapsedSeconds) / kEarthRadiusMeters; + + const double sinLat1 = std::sin(lat1); + const double cosLat1 = std::cos(lat1); + const double sinAngularDistance = std::sin(angularDistance); + const double cosAngularDistance = std::cos(angularDistance); + + const double lat2 = std::asin(sinLat1 * cosAngularDistance + + cosLat1 * sinAngularDistance * std::cos(bearing)); + const double lon2 = lon1 + std::atan2(std::sin(bearing) * sinAngularDistance * cosLat1, + cosAngularDistance - sinLat1 * std::sin(lat2)); + + record.latitude = lat2 * 180.0 / M_PI; + record.longitude = normalizeLongitudeRadians(lon2) * 180.0 / M_PI; + record.altitudeMeters = qMax(0.0, record.altitudeMeters + record.verticalRateMs * elapsedSeconds); + return record; +} + +QString AircraftObject::getInfoString(const StelCore* core, const InfoStringGroup& flags) const +{ + QString str; + QTextStream stream(&str); + const AircraftRecord currentRecord = getExtrapolatedRecord(getElapsedSeconds()); + + if (flags & Name) + stream << QString("

%1

").arg(labelText().toHtmlEscaped()); + if (flags & ObjectType) + stream << QString("%1: %2
").arg(q_("Type"), getObjectTypeI18n()); + if (flags & Extra) + { + const double dataAgeSeconds = getElapsedSeconds(); + const QString heading = QString("%1° (%2)") + .arg(QString::number(normalizeDegrees(currentRecord.trackDegrees), 'f', 0), headingToCompass(currentRecord.trackDegrees)); + const QString altitude = QString("%1 m / %2 ft") + .arg(QString::number(currentRecord.altitudeMeters, 'f', 0), + QString::number(currentRecord.altitudeMeters * kMetersToFeet, 'f', 0)); + const QString groundSpeed = QString("%1 m/s / %2 kt") + .arg(QString::number(currentRecord.groundSpeedMs, 'f', 0), + QString::number(currentRecord.groundSpeedMs * kMetersPerSecondToKnots, 'f', 0)); + const QString verticalRate = QString("%1 m/s") + .arg(QString::number(currentRecord.verticalRateMs, 'f', 1)); + + stream << QString("%1: %2
").arg(q_("Identifier"), aircraftRecord.icao24.toHtmlEscaped()); + if (!aircraftRecord.callsign.isEmpty()) + stream << QString("%1: %2
").arg(q_("Flight"), aircraftRecord.callsign.toHtmlEscaped()); + if (!aircraftRecord.aircraftType.isEmpty()) + stream << QString("%1: %2
").arg(q_("Model"), aircraftRecord.aircraftType.toHtmlEscaped()); + stream << "
"; + stream << ""; + stream << QString("").arg(q_("Altitude"), altitude); + stream << QString("").arg(q_("Ground speed"), groundSpeed); + stream << QString("").arg(q_("Vertical rate"), verticalRate); + stream << QString("").arg(q_("Track"), heading); + stream << QString("") + .arg(q_("Data age"), QString::number(dataAgeSeconds, 'f', dataAgeSeconds < 10.0 ? 1 : 0)); + stream << "
%1:%2
%1:%2
%1:%2
%1:%2
%1:%2 s
"; + } + + stream << getCommonInfoString(core, flags); + postProcessInfoString(str, flags); + return str; +} + +Vec3f AircraftObject::getInfoColor() const +{ + return Vec3f(0.30f, 0.86f, 1.0f); +} + +Vec3d AircraftObject::getAltAzPos(const StelCore* core, double elapsedSeconds) const +{ + if (!core) + return Vec3d(0.0, 0.0, 1.0); + + const AircraftRecord record = getExtrapolatedRecord(elapsedSeconds); + const StelLocation& location = core->getCurrentLocation(); + const double obsLat = static_cast(location.getLatitude()) * M_PI / 180.0; + const double obsLon = static_cast(location.getLongitude()) * M_PI / 180.0; + const double tgtLat = record.latitude * M_PI / 180.0; + const double tgtLon = record.longitude * M_PI / 180.0; + + const Vec3d observer = toEcef(obsLat, obsLon, static_cast(location.altitude)); + const Vec3d aircraft = toEcef(tgtLat, tgtLon, record.altitudeMeters); + const Vec3d toPoint = aircraft - observer; + + const double sla = std::sin(obsLat); + const double cla = std::cos(obsLat); + const double slo = std::sin(obsLon); + const double clo = std::cos(obsLon); + + Vec3d altAz; + altAz[0] = sla * clo * toPoint[0] + sla * slo * toPoint[1] - cla * toPoint[2]; + altAz[1] = -slo * toPoint[0] + clo * toPoint[1]; + altAz[2] = cla * clo * toPoint[0] + cla * slo * toPoint[1] + sla * toPoint[2]; + altAz.normalize(); + return altAz; +} + +Vec3d AircraftObject::getJ2000EquatorialPos(const StelCore* core) const +{ + return core ? core->altAzToJ2000(getAltAzPos(core, getElapsedSeconds()), StelCore::RefractionOff) : Vec3d(1.0, 0.0, 0.0); +} + +float AircraftObject::getVMagnitude(const StelCore* core) const +{ + Q_UNUSED(core) + return 1.5f; +} + +double AircraftObject::getAngularRadius(const StelCore* core) const +{ + Q_UNUSED(core) + return 0.0002; +} + +float AircraftObject::getSelectPriority(const StelCore* core) const +{ + Q_UNUSED(core) + return -10.0f; +} + +bool AircraftObject::isAboveHorizon(const StelCore* core) const +{ + return getAltAzPos(core, getElapsedSeconds())[2] > 0.0; +} + +float AircraftObject::getScreenRotationDegrees(StelCore* core, const StelProjectorP& projector, const Vec3d& currentScreenPos) const +{ + if (!projector) + return static_cast(normalizeDegrees(aircraftRecord.trackDegrees) + kSpriteHeadingOffsetDegrees); + + Vec3d futureScreenPos; + if (!projector->project(getAltAzPos(core, getElapsedSeconds() + kHeadingProbeSeconds), futureScreenPos)) + return static_cast(normalizeDegrees(aircraftRecord.trackDegrees) + kSpriteHeadingOffsetDegrees); + + const double dx = futureScreenPos[0] - currentScreenPos[0]; + const double dy = futureScreenPos[1] - currentScreenPos[1]; + if (std::abs(dx) < 0.01 && std::abs(dy) < 0.01) + return static_cast(normalizeDegrees(aircraftRecord.trackDegrees) + kSpriteHeadingOffsetDegrees); + + const double angleDeg = std::atan2(dy, dx) * 180.0 / M_PI; + return static_cast(angleDeg + kSpriteHeadingOffsetDegrees); +} + +void AircraftObject::draw(StelCore* core, StelPainter* painter, bool drawLabels, int labelMode) const +{ + if (!core || !painter || !isAboveHorizon(core)) + return; + + StelProjectorP projector = core->getProjection(StelCore::FrameAltAz, StelCore::RefractionOff); + const double elapsedSeconds = getElapsedSeconds(); + Vec3d screenPos; + if (!projector->project(getAltAzPos(core, elapsedSeconds), screenPos) || !projector->checkInViewport(screenPos)) + return; + + const Vec3f color = getInfoColor(); + painter->setColor(color, 1.0f); + painter->drawSprite2dMode(static_cast(screenPos[0]), static_cast(screenPos[1]), + kPlaneSpriteSize, getScreenRotationDegrees(core, projector, screenPos)); + + if (drawLabels) + { + painter->drawText(static_cast(screenPos[0]), static_cast(screenPos[1]), displayLabelText(labelMode), 0, 10.0f, 10.0f, false); + } +} diff --git a/plugins/Planes/src/AircraftObject.hpp b/plugins/Planes/src/AircraftObject.hpp new file mode 100644 index 0000000000000..f3447b811cc86 --- /dev/null +++ b/plugins/Planes/src/AircraftObject.hpp @@ -0,0 +1,53 @@ +#ifndef PLANES_AIRCRAFTOBJECT_HPP +#define PLANES_AIRCRAFTOBJECT_HPP + +#include "AircraftRecord.hpp" +#include "StelObject.hpp" +#include "StelProjectorType.hpp" +#include "StelTranslator.hpp" + +#include +#include + +class StelPainter; + +class AircraftObject : public StelObject +{ +public: + static const QString STEL_TYPE; + + explicit AircraftObject(const AircraftRecord& record); + void updateRecord(const AircraftRecord& record); + + QString getType() const override { return STEL_TYPE; } + QString getObjectType() const override { return N_("plane"); } + QString getObjectTypeI18n() const override; + QString getID() const override; + QString getEnglishName() const override; + QString getNameI18n() const override; + QString getInfoString(const StelCore* core, const InfoStringGroup& flags) const override; + Vec3f getInfoColor() const override; + Vec3d getJ2000EquatorialPos(const StelCore* core) const override; + float getVMagnitude(const StelCore* core) const override; + double getAngularRadius(const StelCore* core) const override; + float getSelectPriority(const StelCore* core) const override; + + bool isAboveHorizon(const StelCore* core) const; + void draw(StelCore* core, StelPainter* painter, bool drawLabels, int labelMode) const; + + const AircraftRecord& record() const { return aircraftRecord; } + +private: + AircraftRecord getExtrapolatedRecord(double elapsedSeconds) const; + double getElapsedSeconds() const; + Vec3d getAltAzPos(const StelCore* core, double elapsedSeconds=0.0) const; + float getScreenRotationDegrees(StelCore* core, const StelProjectorP& projector, const Vec3d& currentScreenPos) const; + QString labelText() const; + QString displayLabelText(int labelMode) const; + + AircraftRecord aircraftRecord; +}; + +using AircraftObjectP = QSharedPointer; + +#endif diff --git a/plugins/Planes/src/AircraftRecord.hpp b/plugins/Planes/src/AircraftRecord.hpp new file mode 100644 index 0000000000000..1573b35b0b4bb --- /dev/null +++ b/plugins/Planes/src/AircraftRecord.hpp @@ -0,0 +1,20 @@ +#ifndef PLANES_AIRCRAFTRECORD_HPP +#define PLANES_AIRCRAFTRECORD_HPP + +#include + +struct AircraftRecord +{ + QString icao24; + QString callsign; + QString aircraftType; + double latitude = 0.0; + double longitude = 0.0; + double altitudeMeters = 0.0; + double groundSpeedMs = 0.0; + double trackDegrees = 0.0; + double verticalRateMs = 0.0; + double snapshotJd = 0.0; +}; + +#endif diff --git a/plugins/Planes/src/CMakeLists.txt b/plugins/Planes/src/CMakeLists.txt new file mode 100644 index 0000000000000..b36afa4a2512a --- /dev/null +++ b/plugins/Planes/src/CMakeLists.txt @@ -0,0 +1,57 @@ +INCLUDE_DIRECTORIES( + . + gui + ${CMAKE_SOURCE_DIR}/src + ${CMAKE_SOURCE_DIR}/src/gui + ${CMAKE_BINARY_DIR}/plugins/Planes/src + ${CMAKE_BINARY_DIR}/plugins/Planes/src/gui +) + +LINK_DIRECTORIES(${BUILD_DIR}/src) + +SET(Planes_SRCS + AircraftObject.hpp + AircraftObject.cpp + AircraftRecord.hpp + Planes.hpp + Planes.cpp +) +IF (STELLARIUM_GUI_MODE STREQUAL "Standard") +LIST(APPEND Planes_SRCS + gui/PlanesDialog.hpp + gui/PlanesDialog.cpp +) + +SET(Planes_UIS + gui/planesDialog.ui +) +ENDIF() + +SET(Planes_RES ../resources/Planes.qrc) +IF (${QT_VERSION_MAJOR} EQUAL "5") + IF (STELLARIUM_GUI_MODE STREQUAL "Standard") + QT5_WRAP_UI(Planes_UIS_H ${Planes_UIS}) + ENDIF() + QT5_ADD_RESOURCES(Planes_RES_CXX ${Planes_RES}) +ELSE() + IF (STELLARIUM_GUI_MODE STREQUAL "Standard") + QT_WRAP_UI(Planes_UIS_H ${Planes_UIS}) + ENDIF() + QT_ADD_RESOURCES(Planes_RES_CXX ${Planes_RES}) +ENDIF() + +SET(Planes_Qt_Libraries + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::Network + Qt${QT_VERSION_MAJOR}::OpenGL + Qt${QT_VERSION_MAJOR}::Widgets +) + +ADD_LIBRARY(Planes-static STATIC ${Planes_SRCS} ${Planes_RES_CXX} ${Planes_UIS_H}) +SET_TARGET_PROPERTIES(Planes-static PROPERTIES OUTPUT_NAME "Planes") +TARGET_LINK_LIBRARIES(Planes-static ${Planes_Qt_Libraries}) +SET_TARGET_PROPERTIES(Planes-static PROPERTIES COMPILE_FLAGS "-DQT_STATICPLUGIN") +ADD_DEPENDENCIES(AllStaticPlugins Planes-static) + +SET_TARGET_PROPERTIES(Planes-static PROPERTIES FOLDER "plugins/Planes") diff --git a/plugins/Planes/src/Planes.cpp b/plugins/Planes/src/Planes.cpp new file mode 100644 index 0000000000000..e72150f1ec5ff --- /dev/null +++ b/plugins/Planes/src/Planes.cpp @@ -0,0 +1,718 @@ +#include "Planes.hpp" + +#include "StelApp.hpp" +#include "StelCore.hpp" +#include "StelFileMgr.hpp" +#include "StelGui.hpp" +#include "StelGuiItems.hpp" +#include "StelLocation.hpp" +#include "StelModuleMgr.hpp" +#include "StelObjectMgr.hpp" +#include "StelProjector.hpp" +#include "StelTextureMgr.hpp" +#include "StelTranslator.hpp" +#include "StelUtils.hpp" + +#ifndef NO_GUI +#include "gui/PlanesDialog.hpp" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +struct ProviderDefinition +{ + QString id; + QString displayName; + QString urlTemplate; + QString websiteUrl; + int maxRadiusNm; +}; + +const QString kAdsbFiTemplate = QStringLiteral("https://opendata.adsb.fi/api/v2/lat/%1/lon/%2/dist/%3"); +const QString kAirplanesLiveTemplate = QStringLiteral("https://api.airplanes.live/v2/point/%1/%2/%3"); +const QString kPluginVersion = QStringLiteral("0.1.0"); +constexpr int kLabelModeFlightNumber = 0; +constexpr int kLabelModeAircraftModel = 1; +constexpr int kDefaultFetchIntervalSec = 15; +constexpr int kMinFetchIntervalSec = 15; +constexpr int kMaxFetchIntervalSec = 60; +constexpr int kDefaultRadiusNm = 250; +constexpr int kMinRadiusNm = 25; +constexpr int kMaxRadiusNm = 500; +constexpr int kMaxPublishedAircraft = 200; +constexpr double kFeetToMeters = 0.3048; +constexpr double kKnotsToMetersPerSecond = 0.514444; +constexpr double kFeetPerMinuteToMetersPerSecond = 0.00508; + +ProviderDefinition providerDefinition(const QString& providerId) +{ + if (providerId == QStringLiteral("airplanes_live")) + { + return { + QStringLiteral("airplanes_live"), + QStringLiteral("airplanes.live"), + kAirplanesLiveTemplate, + QStringLiteral("https://airplanes.live/api-guide/"), + 250 + }; + } + + return { + QStringLiteral("adsb_fi"), + QStringLiteral("adsb.fi"), + kAdsbFiTemplate, + QStringLiteral("https://adsb.fi/"), + kMaxRadiusNm + }; +} + +QString providerIdFromTemplate(const QString& urlTemplate) +{ + if (urlTemplate.contains(QStringLiteral("airplanes.live"), Qt::CaseInsensitive)) + return QStringLiteral("airplanes_live"); + return QStringLiteral("adsb_fi"); +} + +int sanitizeInterval(int seconds) +{ + if (seconds <= 15) + return 15; + if (seconds <= 20) + return 20; + if (seconds <= 30) + return 30; + return 60; +} + +int sanitizeRadius(int nm) +{ + if (nm < kMinRadiusNm) + return kMinRadiusNm; + if (nm > kMaxRadiusNm) + return kMaxRadiusNm; + return nm; +} + +int sanitizeLabelMode(int mode) +{ + return mode == kLabelModeAircraftModel ? kLabelModeAircraftModel : kLabelModeFlightNumber; +} + +bool shouldSkipAircraft(const QJsonObject& object) +{ + if (!object.contains("lat") || !object.contains("lon")) + return true; + if (object.value("lat").isNull() || object.value("lon").isNull()) + return true; + return object.value("alt_baro").toString() == QStringLiteral("ground"); +} + +AircraftRecord parseAircraftRecord(const QJsonObject& object, double snapshotJd) +{ + AircraftRecord record; + record.icao24 = object.value("hex").toString().trimmed().toLower(); + record.callsign = object.value("flight").toString().trimmed(); + record.aircraftType = object.value("t").toString().trimmed(); + record.latitude = object.value("lat").toDouble(); + record.longitude = object.value("lon").toDouble(); + record.altitudeMeters = object.value("alt_baro").toDouble() * kFeetToMeters; + record.groundSpeedMs = object.value("gs").toDouble() * kKnotsToMetersPerSecond; + record.trackDegrees = object.value("track").toDouble(); + record.verticalRateMs = object.value("baro_rate").toDouble() * kFeetPerMinuteToMetersPerSecond; + record.snapshotJd = snapshotJd; + return record; +} + +QVector parseAircraftRecords(const QJsonArray& aircraftArray, double snapshotJd) +{ + QVector nextRecords; + nextRecords.reserve(qMin(aircraftArray.size(), kMaxPublishedAircraft)); + + for (const QJsonValue& value : aircraftArray) + { + const QJsonObject object = value.toObject(); + if (shouldSkipAircraft(object)) + continue; + + const AircraftRecord record = parseAircraftRecord(object, snapshotJd); + if (record.icao24.isEmpty()) + continue; + + nextRecords.append(record); + if (nextRecords.size() >= kMaxPublishedAircraft) + break; + } + + return nextRecords; +} + +QJsonArray aircraftArrayFromResponse(const QJsonObject& root) +{ + const QJsonArray aircraftArray = root.value("aircraft").toArray(); + if (!aircraftArray.isEmpty()) + return aircraftArray; + return root.value("ac").toArray(); +} +} + +StelModule* PlanesStelPluginInterface::getStelModule() const +{ + return new Planes(); +} + +StelPluginInfo PlanesStelPluginInterface::getPluginInfo() const +{ + StelPluginInfo info; + info.id = QStringLiteral("Planes"); + info.displayedName = N_("Planes"); + info.authors = QStringLiteral("Felix Zeltner, Georg Zotti, Kamil Zaraś (astronow.pl)"); + info.contact = STELLARIUM_DEV_URL; + info.description = N_("Display live ADS-B aircraft in the sky."); + info.version = kPluginVersion; + info.license = QStringLiteral("GPL v2 or later"); + return info; +} + +Planes::Planes() + : networkMgr(nullptr) + , fetchTimer(nullptr) + , refreshDebounceTimer(nullptr) + , inFlightReply(nullptr) +#ifndef NO_GUI + , configDialog(nullptr) + , toolbarButton(nullptr) +#endif + , sourceUrlTemplate(kAdsbFiTemplate) + , fetchIntervalSec(kDefaultFetchIntervalSec) + , radiusNm(kDefaultRadiusNm) + , pendingRefresh(false) + , enabled(false) + , showLabels(false) + , showButton(true) + , labelMode(kLabelModeFlightNumber) + , lastStatus(QStringLiteral("idle")) + , lastSuccessfulUpdate(QStringLiteral("Never")) +{ + setObjectName(QStringLiteral("Planes")); +#ifndef NO_GUI + configDialog = new PlanesDialog(); +#endif +} + +Planes::~Planes() +{ +#ifndef NO_GUI + delete configDialog; +#endif +} + +void Planes::loadSettings() +{ + QSettings* conf = StelApp::getInstance().getSettings(); + conf->beginGroup("Planes"); + sourceUrlTemplate = conf->value("source_url_template", kAdsbFiTemplate).toString().trimmed(); + if (sourceUrlTemplate.isEmpty()) + sourceUrlTemplate = kAdsbFiTemplate; + fetchIntervalSec = sanitizeInterval(conf->value("fetch_interval_sec", kDefaultFetchIntervalSec).toInt()); + radiusNm = sanitizeRadius(conf->value("radius_nm", kDefaultRadiusNm).toInt()); + enabled = conf->value("enabled", false).toBool(); + showLabels = conf->value("show_labels", true).toBool(); + showButton = conf->value("show_button", true).toBool(); + labelMode = sanitizeLabelMode(conf->value("label_mode", kLabelModeFlightNumber).toInt()); + conf->endGroup(); + applyProviderDefaults(); +} + +void Planes::saveSettings() const +{ + QSettings* conf = StelApp::getInstance().getSettings(); + conf->beginGroup("Planes"); + conf->setValue("source_url_template", sourceUrlTemplate); + conf->setValue("fetch_interval_sec", fetchIntervalSec); + conf->setValue("radius_nm", radiusNm); + conf->setValue("enabled", enabled); + conf->setValue("show_labels", showLabels); + conf->setValue("show_button", showButton); + conf->setValue("label_mode", labelMode); + conf->endGroup(); +} + +void Planes::init() +{ + Q_INIT_RESOURCE(Planes); + loadSettings(); + planeTexture = StelApp::getInstance().getTextureManager().createTexture(":/planes/plane.png"); + pointerTexture = StelApp::getInstance().getTextureManager().createTexture(StelFileMgr::getInstallationDir()+"/textures/pointeur2.png"); + + networkMgr = new QNetworkAccessManager(this); + connect(networkMgr, &QNetworkAccessManager::finished, this, &Planes::onReply); + + fetchTimer = new QTimer(this); + fetchTimer->setInterval(fetchIntervalSec * 1000); + connect(fetchTimer, &QTimer::timeout, this, &Planes::fetchAircraft); + + refreshDebounceTimer = new QTimer(this); + refreshDebounceTimer->setSingleShot(true); + refreshDebounceTimer->setInterval(1200); + connect(refreshDebounceTimer, &QTimer::timeout, this, &Planes::fetchAircraft); + + GETSTELMODULE(StelObjectMgr)->registerStelObjectMgr(this); + connect(StelApp::getInstance().getCore(), &StelCore::locationChanged, this, &Planes::onLocationChanged); + + addAction("actionShow_Planes", N_("Planes"), N_("Show Planes"), "enabled", "Shift+P"); +#ifndef NO_GUI + addAction("actionShow_Planes_dialog", N_("Planes"), N_("Show settings dialog"), configDialog, "visible", "Ctrl+P"); + applyButtonVisibility(); +#endif + updateStatus(enabled ? QStringLiteral("enabled") : QStringLiteral("disabled; live updates are off")); + + if (enabled) + { + fetchTimer->start(); + QTimer::singleShot(1500, this, &Planes::fetchAircraft); + } +} + +void Planes::deinit() +{ + saveSettings(); + if (inFlightReply) + inFlightReply->abort(); + aircraft.clear(); + planeTexture.clear(); + pointerTexture.clear(); +} + +void Planes::update(double deltaTime) +{ + Q_UNUSED(deltaTime) +} + +QString Planes::buildRequestUrl(const StelCore* core) const +{ + if (!core) + return QString(); + + const StelLocation& loc = core->getCurrentLocation(); + return sourceUrlTemplate + .arg(loc.getLatitude(), 0, 'f', 4) + .arg(loc.getLongitude(), 0, 'f', 4) + .arg(radiusNm); +} + +QString Planes::getProviderId() const +{ + return providerIdFromTemplate(sourceUrlTemplate); +} + +QString Planes::getProviderDisplayName() const +{ + return providerDefinition(getProviderId()).displayName; +} + +QString Planes::getProviderWebsiteUrl() const +{ + return providerDefinition(getProviderId()).websiteUrl; +} + +int Planes::getProviderMaxRadiusNm() const +{ + return providerDefinition(getProviderId()).maxRadiusNm; +} + +void Planes::applyProviderDefaults(bool clampRadius) +{ + const ProviderDefinition provider = providerDefinition(providerIdFromTemplate(sourceUrlTemplate)); + sourceUrlTemplate = provider.urlTemplate; + if (clampRadius) + radiusNm = sanitizeRadius(qMin(radiusNm, provider.maxRadiusNm)); +} + +void Planes::fetchAircraft() +{ + if (!networkMgr || !enabled) + return; + + StelCore* core = StelApp::getInstance().getCore(); + const QString url = buildRequestUrl(core); + if (url.isEmpty()) + { + updateStatus(QStringLiteral("missing core or URL"), false, true); + return; + } + + if (inFlightReply) + { + pendingRefresh = true; + return; + } + + QNetworkRequest request{QUrl(url)}; + request.setRawHeader("User-Agent", QString("Stellarium-Planes/%1").arg(kPluginVersion).toUtf8()); + request.setRawHeader("Accept", "application/json"); + inFlightReply = networkMgr->get(request); + pendingRefresh = false; + updateStatus(QStringLiteral("Updating live aircraft feed...")); +} + +void Planes::onReply(QNetworkReply* reply) +{ + if (reply == inFlightReply) + inFlightReply = nullptr; + + const bool shouldRefreshAgain = pendingRefresh && enabled; + pendingRefresh = false; + reply->deleteLater(); + + if (reply->error() != QNetworkReply::NoError) + { + if (reply->error() != QNetworkReply::OperationCanceledError) + updateStatus(QString("Update failed: %1").arg(reply->errorString()), false, true); + if (shouldRefreshAgain) + scheduleRefresh(250); + return; + } + + const QByteArray payload = reply->readAll(); + + QJsonParseError parseError; + const QJsonDocument doc = QJsonDocument::fromJson(payload, &parseError); + if (parseError.error != QJsonParseError::NoError || !doc.isObject()) + { + updateStatus(QString("Update failed: %1").arg(parseError.errorString()), false, true); + if (shouldRefreshAgain) + scheduleRefresh(250); + return; + } + + const QVector records = parseAircraftRecords(aircraftArrayFromResponse(doc.object()), StelUtils::getJDFromSystem()); + QHash existingById; + existingById.reserve(aircraft.size()); + for (const AircraftObjectP& object : std::as_const(aircraft)) + { + if (object) + existingById.insert(object->getID(), object); + } + + QVector nextAircraft; + nextAircraft.reserve(records.size()); + for (const AircraftRecord& record : records) + { + const auto it = existingById.constFind(record.icao24); + if (it != existingById.constEnd()) + { + it.value()->updateRecord(record); + nextAircraft.append(it.value()); + } + else + { + nextAircraft.append(AircraftObjectP::create(record)); + } + } + aircraft = nextAircraft; + lastSuccessfulUpdate = QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm:ss")); + updateStatus(QString("Showing %1 aircraft from %2").arg(aircraft.size()).arg(getProviderDisplayName())); + if (shouldRefreshAgain) + scheduleRefresh(250); +} + +void Planes::onLocationChanged(const StelLocation& loc) +{ + Q_UNUSED(loc) + scheduleRefresh(); +} + +void Planes::draw(StelCore* core) +{ + if (!core || !enabled) + return; + + StelProjectorP projection = core->getProjection(StelCore::FrameAltAz, StelCore::RefractionOff); + StelPainter painter(projection); + painter.setColor(1.0f, 1.0f, 1.0f, 1.0f); + painter.setBlending(true); + if (planeTexture) + planeTexture->bind(); + for (const AircraftObjectP& object : std::as_const(aircraft)) + object->draw(core, &painter, showLabels, labelMode); + + StelObjectMgr* objectMgr = GETSTELMODULE(StelObjectMgr); + if (!objectMgr->getFlagSelectedObjectPointer()) + return; + + const QList selectedAircraft = objectMgr->getSelectedObject(AircraftObject::STEL_TYPE); + if (selectedAircraft.empty() || !pointerTexture) + return; + + StelPainter pointerPainter(core->getProjection(StelCore::FrameJ2000, StelCore::RefractionOff)); + const StelObjectP object = selectedAircraft.constFirst(); + Vec3f screenPos; + if (!pointerPainter.getProjector()->project(object->getJ2000EquatorialPos(core).toVec3f(), screenPos)) + return; + + pointerPainter.setColor(object->getInfoColor()); + pointerTexture->bind(); + pointerPainter.setBlending(true); + const float angle = static_cast(StelApp::getInstance().getAnimationTime()) * 40.0f; + const float scale = StelApp::getInstance().getScreenScale(); + pointerPainter.drawSprite2dModeNoDeviceScale(screenPos[0], screenPos[1], 13.0f * scale, angle); +} + +double Planes::getCallOrder(StelModuleActionName actionName) const +{ + if (actionName == StelModule::ActionDraw) + return StelApp::getInstance().getModuleMgr().getModule(QStringLiteral("SolarSystem"))->getCallOrder(actionName) + 1.0; + return 0.0; +} + +bool Planes::configureGui(bool show) +{ +#ifndef NO_GUI + if (configDialog) + { + if (show) + configDialog->setVisible(true); + return true; + } +#else + Q_UNUSED(show) +#endif + return false; +} + +void Planes::setEnabled(bool value) +{ + if (enabled == value) + return; + + enabled = value; + if (!enabled) + { + if (fetchTimer) + fetchTimer->stop(); + if (refreshDebounceTimer) + refreshDebounceTimer->stop(); + pendingRefresh = false; + if (inFlightReply) + inFlightReply->abort(); + aircraft.clear(); + updateStatus(QStringLiteral("Live updates are off")); + } + else + { + if (fetchTimer) + fetchTimer->start(); + updateStatus(QStringLiteral("Waiting for first update...")); + scheduleRefresh(0); + } + emit enabledChanged(enabled); +} + +void Planes::setFlagShowLabels(bool value) +{ + if (showLabels == value) + return; + showLabels = value; + emit showLabelsChanged(showLabels); +} + +void Planes::setFlagShowButton(bool value) +{ + if (showButton == value) + return; + showButton = value; +#ifndef NO_GUI + applyButtonVisibility(); +#endif + emit showButtonChanged(showButton); +} + +void Planes::setLabelMode(int mode) +{ + const int sanitized = sanitizeLabelMode(mode); + if (labelMode == sanitized) + return; + labelMode = sanitized; + emit labelModeChanged(labelMode); +} + +void Planes::setProviderId(const QString& providerId) +{ + const QString currentProviderId = getProviderId(); + const ProviderDefinition nextProvider = providerDefinition(providerId); + if (currentProviderId == nextProvider.id) + return; + + sourceUrlTemplate = nextProvider.urlTemplate; + const int clampedRadius = sanitizeRadius(qMin(radiusNm, nextProvider.maxRadiusNm)); + const bool radiusAdjusted = clampedRadius != radiusNm; + radiusNm = clampedRadius; + + emit providerChanged(nextProvider.id); + if (radiusAdjusted) + emit radiusChanged(radiusNm); + + if (enabled) + { + updateStatus(QString("Switched to %1").arg(nextProvider.displayName)); + scheduleRefresh(0); + } +} + +void Planes::setFetchIntervalSec(int seconds) +{ + const int sanitized = sanitizeInterval(seconds); + if (fetchIntervalSec == sanitized) + return; + fetchIntervalSec = sanitized; + if (fetchTimer) + fetchTimer->setInterval(fetchIntervalSec * 1000); + emit fetchIntervalChanged(fetchIntervalSec); + scheduleRefresh(); +} + +void Planes::setRadiusNm(int nm) +{ + const int sanitized = sanitizeRadius(nm); + if (radiusNm == sanitized) + return; + radiusNm = sanitized; + emit radiusChanged(radiusNm); + scheduleRefresh(); +} + +void Planes::refreshNow() +{ + if (enabled) + fetchAircraft(); +} + +void Planes::scheduleRefresh(int delayMs) +{ + if (!enabled || !refreshDebounceTimer) + return; + + refreshDebounceTimer->start(delayMs); +} + +void Planes::updateStatus(const QString& status, bool logInfo, bool logWarning) +{ + lastStatus = status; + if (logInfo) + qInfo().noquote() << "[Planes]" << lastStatus; + if (logWarning) + qWarning().noquote() << "[Planes]" << lastStatus; + emit statusChanged(lastStatus); +} + +void Planes::applyButtonVisibility() +{ +#ifndef NO_GUI + StelGui* gui = dynamic_cast(StelApp::getInstance().getGui()); + if (!gui) + return; + + if (showButton) + { + if (!toolbarButton) + { + toolbarButton = new StelButton(nullptr, + QPixmap(":/planes/planes_on_160.png"), + QPixmap(":/planes/planes_off_160.png"), + QPixmap(":/graphicGui/miscGlow32x32.png"), + "actionShow_Planes", + false, + "actionShow_Planes_dialog"); + } + gui->getButtonBar()->addButton(toolbarButton, "065-pluginsGroup"); + } + else + { + gui->getButtonBar()->hideButton("actionShow_Planes"); + } +#endif +} + +QList Planes::searchAround(const Vec3d& v, double limitFov, const StelCore* core) const +{ + QList result; + if (!core) + return result; + + const double cosLimitFov = std::cos(limitFov * M_PI / 180.0); + Vec3d normalized = v; + normalized.normalize(); + + for (const AircraftObjectP& object : aircraft) + { + if (!object->isAboveHorizon(core)) + continue; + Vec3d objectPos = object->getJ2000EquatorialPos(core); + objectPos.normalize(); + if (normalized.dot(objectPos) >= cosLimitFov) + result.append(qSharedPointerCast(object)); + } + + return result; +} + +StelObjectP Planes::searchByNameI18n(const QString& nameI18n) const +{ + return searchByName(nameI18n); +} + +StelObjectP Planes::searchByName(const QString& name) const +{ + const QString needle = name.trimmed().toUpper(); + for (const AircraftObjectP& object : aircraft) + { + if (object->getEnglishName().toUpper() == needle || object->getID().toUpper() == needle) + return qSharedPointerCast(object); + } + return StelObjectP(); +} + +StelObjectP Planes::searchByID(const QString& id) const +{ + return searchByName(id); +} + +QVector> Planes::listMatchingObjects(const QString& objPrefix, int maxNbItem, bool useStartOfWords) const +{ + Q_UNUSED(useStartOfWords) + QVector> result; + const QString needle = objPrefix.trimmed().toUpper(); + for (const AircraftObjectP& object : aircraft) + { + if (!object->getEnglishName().toUpper().startsWith(needle) && !object->getID().toUpper().startsWith(needle)) + continue; + result.append(qMakePair(object->getEnglishName(), qSharedPointerCast(object))); + if (result.size() >= maxNbItem) + break; + } + return result; +} + +QVector> Planes::listAllObjects(bool inEnglish) const +{ + Q_UNUSED(inEnglish) + QVector> result; + result.reserve(aircraft.size()); + for (const AircraftObjectP& object : aircraft) + result.append(qMakePair(object->getEnglishName(), qSharedPointerCast(object))); + return result; +} diff --git a/plugins/Planes/src/Planes.hpp b/plugins/Planes/src/Planes.hpp new file mode 100644 index 0000000000000..3d1e7fd641d0a --- /dev/null +++ b/plugins/Planes/src/Planes.hpp @@ -0,0 +1,136 @@ +#ifndef PLANES_HPP +#define PLANES_HPP + +#include "AircraftObject.hpp" +#include "StelLocation.hpp" +#include "StelObjectModule.hpp" +#include "StelTextureTypes.hpp" + +#include +#include +#include +#include + +class QNetworkAccessManager; +class QNetworkReply; +class QTimer; +class StelButton; +class PlanesDialog; + +class Planes : public StelObjectModule +{ + Q_OBJECT + Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled NOTIFY enabledChanged) + Q_PROPERTY(bool showLabels READ getFlagShowLabels WRITE setFlagShowLabels NOTIFY showLabelsChanged) + Q_PROPERTY(bool showButton READ getFlagShowButton WRITE setFlagShowButton NOTIFY showButtonChanged) + Q_PROPERTY(int labelMode READ getLabelMode WRITE setLabelMode NOTIFY labelModeChanged) + +public: + Planes(); + ~Planes() override; + + void init() override; + void deinit() override; + void update(double deltaTime) override; + void draw(StelCore* core) override; + double getCallOrder(StelModuleActionName actionName) const override; + bool configureGui(bool show=true) override; + + QList searchAround(const Vec3d& v, double limitFov, const StelCore* core) const override; + StelObjectP searchByNameI18n(const QString& nameI18n) const override; + StelObjectP searchByName(const QString& name) const override; + StelObjectP searchByID(const QString& id) const override; + QVector> listMatchingObjects(const QString& objPrefix, int maxNbItem, bool useStartOfWords) const override; + QVector> listAllObjects(bool inEnglish) const override; + + QString getName() const override { return QStringLiteral("Planes"); } + QString getStelObjectType() const override { return AircraftObject::STEL_TYPE; } + bool isEnabled() const { return enabled; } + bool getFlagShowLabels() const { return showLabels; } + bool getFlagShowButton() const { return showButton; } + int getLabelMode() const { return labelMode; } + int getFetchIntervalSec() const { return fetchIntervalSec; } + int getRadiusNm() const { return radiusNm; } + QString getLastStatus() const { return lastStatus; } + QString getLastSuccessfulUpdate() const { return lastSuccessfulUpdate; } + QString getProviderId() const; + QString getProviderDisplayName() const; + QString getProviderWebsiteUrl() const; + int getProviderMaxRadiusNm() const; + QString getSourceUrlTemplate() const { return sourceUrlTemplate; } + +public slots: + void setEnabled(bool value); + void setFlagShowLabels(bool value); + void setFlagShowButton(bool value); + void setLabelMode(int mode); + void setProviderId(const QString& providerId); + void setFetchIntervalSec(int seconds); + void setRadiusNm(int nm); + void refreshNow(); + +private slots: + void fetchAircraft(); + void onReply(QNetworkReply* reply); + void onLocationChanged(const StelLocation& loc); + +signals: + void enabledChanged(bool value); + void showLabelsChanged(bool value); + void showButtonChanged(bool value); + void labelModeChanged(int mode); + void providerChanged(const QString& providerId); + void fetchIntervalChanged(int seconds); + void radiusChanged(int nm); + void statusChanged(const QString& status); + +private: + void loadSettings(); + void saveSettings() const; + QString buildRequestUrl(const StelCore* core) const; + void applyProviderDefaults(bool clampRadius=true); + void scheduleRefresh(int delayMs=1200); + void updateStatus(const QString& status, bool logInfo=false, bool logWarning=false); + void applyButtonVisibility(); + + QNetworkAccessManager* networkMgr; + QTimer* fetchTimer; + QTimer* refreshDebounceTimer; + QPointer inFlightReply; + QVector aircraft; + StelTextureSP planeTexture; + StelTextureSP pointerTexture; + +#ifndef NO_GUI + PlanesDialog* configDialog; + StelButton* toolbarButton; +#endif + + QString sourceUrlTemplate; + int fetchIntervalSec; + int radiusNm; + bool pendingRefresh; + bool enabled; + bool showLabels; + bool showButton; + int labelMode; + QString lastStatus; + QString lastSuccessfulUpdate; +}; + +#include +#include "StelPluginInterface.hpp" + +class PlanesStelPluginInterface : public QObject, public StelPluginInterface +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID StelPluginInterface_iid) + Q_INTERFACES(StelPluginInterface) + +public: + StelModule* getStelModule() const override; + StelPluginInfo getPluginInfo() const override; + QObjectList getExtensionList() const override { return QObjectList(); } +}; + +#endif diff --git a/plugins/Planes/src/gui/PlanesDialog.cpp b/plugins/Planes/src/gui/PlanesDialog.cpp new file mode 100644 index 0000000000000..0f8280e424328 --- /dev/null +++ b/plugins/Planes/src/gui/PlanesDialog.cpp @@ -0,0 +1,241 @@ +#include "gui/PlanesDialog.hpp" +#include "ui_planesDialog.h" + +#include "Planes.hpp" +#include "Dialog.hpp" +#include "StelApp.hpp" +#include "StelGui.hpp" +#include "StelModuleMgr.hpp" +#include "StelTranslator.hpp" + +#include +#include +#include +#include +#include +#include + +namespace +{ +QString providerWebsiteLink(const QString& label, const QString& url) +{ + return QString("%2").arg(url.toHtmlEscaped(), label.toHtmlEscaped()); +} +} + +PlanesDialog::PlanesDialog() + : StelDialog("Planes") + , planes(nullptr) + , ui(new Ui_planesDialog) +{ +} + +PlanesDialog::~PlanesDialog() +{ + delete ui; +} + +void PlanesDialog::updateComboTexts() +{ + if (!ui) + return; + + ui->labelModeComboBox->setItemText(0, q_("Flight number")); + ui->labelModeComboBox->setItemText(1, q_("Aircraft model")); +} + +void PlanesDialog::retranslate() +{ + if (dialog) + { + ui->retranslateUi(dialog); + updateComboTexts(); + setAboutHtml(); + } +} + +void PlanesDialog::createDialogContent() +{ + planes = GETSTELMODULE(Planes); + ui->setupUi(dialog); + + TitleBar* titleBar = new TitleBar(dialog); + titleBar->setTitle(q_("Planes Plug-in Configuration")); + if (auto* layout = qobject_cast(dialog->layout())) + layout->insertWidget(0, titleBar); + + ui->refreshIntervalSpinBox->setMinimum(15); + ui->refreshIntervalSpinBox->setMaximum(60); + ui->refreshIntervalSpinBox->setSingleStep(5); + ui->refreshIntervalSpinBox->setSuffix(QStringLiteral(" s")); + ui->radiusSpinBox->setMinimum(25); + ui->radiusSpinBox->setMaximum(500); + ui->radiusSpinBox->setSingleStep(25); + ui->radiusSpinBox->setSuffix(QStringLiteral(" NM")); + ui->providerComboBox->clear(); + ui->providerComboBox->addItem(QStringLiteral("adsb.fi"), QStringLiteral("adsb_fi")); + ui->providerComboBox->addItem(QStringLiteral("airplanes.live"), QStringLiteral("airplanes_live")); + ui->labelModeComboBox->clear(); + ui->labelModeComboBox->addItem(QString(), 0); + ui->labelModeComboBox->addItem(QString(), 1); + updateComboTexts(); + ui->providerValueLabel->setOpenExternalLinks(false); + ui->providerValueLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); + + kineticScrollingList << ui->aboutTextBrowser; + StelGui* gui = dynamic_cast(StelApp::getInstance().getGui()); + if (gui) + { + enableKineticScrolling(gui->getFlagUseKineticScrolling()); + connect(gui, SIGNAL(flagUseKineticScrollingChanged(bool)), this, SLOT(enableKineticScrolling(bool))); + } + + connect(&StelApp::getInstance(), SIGNAL(languageChanged()), this, SLOT(retranslate())); + connect(titleBar, &TitleBar::closeClicked, this, &StelDialog::close); + connect(titleBar, SIGNAL(movedTo(QPoint)), this, SLOT(handleMovedTo(QPoint))); + + connect(ui->enabledCheckBox, &QCheckBox::toggled, this, &PlanesDialog::setEnabledFlag); + connect(ui->showLabelsCheckBox, &QCheckBox::toggled, this, &PlanesDialog::setShowLabels); + connect(ui->showButtonCheckBox, &QCheckBox::toggled, this, &PlanesDialog::setShowButton); + connect(ui->labelModeComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &PlanesDialog::setLabelMode); + connect(ui->providerComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &PlanesDialog::setProvider); + connect(ui->providerValueLabel, &QLabel::linkActivated, this, &PlanesDialog::openExternalLink); + connect(ui->radiusSpinBox, QOverload::of(&QSpinBox::valueChanged), this, &PlanesDialog::setRadius); + connect(ui->refreshIntervalSpinBox, QOverload::of(&QSpinBox::valueChanged), this, &PlanesDialog::setFetchInterval); + connect(ui->refreshButton, &QPushButton::clicked, this, &PlanesDialog::triggerRefresh); + + connect(planes, &Planes::enabledChanged, this, &PlanesDialog::updateFromPlugin); + connect(planes, &Planes::showLabelsChanged, this, &PlanesDialog::updateFromPlugin); + connect(planes, &Planes::showButtonChanged, this, &PlanesDialog::updateFromPlugin); + connect(planes, &Planes::labelModeChanged, this, &PlanesDialog::updateFromPlugin); + connect(planes, &Planes::providerChanged, this, &PlanesDialog::updateFromPlugin); + connect(planes, &Planes::radiusChanged, this, &PlanesDialog::updateFromPlugin); + connect(planes, &Planes::fetchIntervalChanged, this, &PlanesDialog::updateFromPlugin); + connect(planes, &Planes::statusChanged, this, &PlanesDialog::setStatus); + + updateFromPlugin(); + setAboutHtml(); +} + +void PlanesDialog::updateFromPlugin() +{ + if (!planes) + return; + + ui->enabledCheckBox->blockSignals(true); + ui->showLabelsCheckBox->blockSignals(true); + ui->showButtonCheckBox->blockSignals(true); + ui->labelModeComboBox->blockSignals(true); + ui->providerComboBox->blockSignals(true); + ui->radiusSpinBox->blockSignals(true); + ui->refreshIntervalSpinBox->blockSignals(true); + + ui->enabledCheckBox->setChecked(planes->isEnabled()); + ui->showLabelsCheckBox->setChecked(planes->getFlagShowLabels()); + ui->showButtonCheckBox->setChecked(planes->getFlagShowButton()); + ui->labelModeComboBox->setCurrentIndex(planes->getLabelMode()); + ui->providerComboBox->setCurrentText(planes->getProviderDisplayName()); + ui->radiusSpinBox->setMaximum(planes->getProviderMaxRadiusNm()); + ui->radiusSpinBox->setValue(planes->getRadiusNm()); + ui->refreshIntervalSpinBox->setValue(planes->getFetchIntervalSec()); + ui->providerValueLabel->setText(providerWebsiteLink(planes->getProviderDisplayName(), planes->getProviderWebsiteUrl())); + ui->lastUpdateValueLabel->setText(planes->getLastSuccessfulUpdate()); + ui->statusValueLabel->setText(planes->getLastStatus()); + ui->statusValueLabel->setWordWrap(true); + ui->refreshButton->setEnabled(planes->isEnabled()); + + ui->enabledCheckBox->blockSignals(false); + ui->showLabelsCheckBox->blockSignals(false); + ui->showButtonCheckBox->blockSignals(false); + ui->labelModeComboBox->blockSignals(false); + ui->providerComboBox->blockSignals(false); + ui->radiusSpinBox->blockSignals(false); + ui->refreshIntervalSpinBox->blockSignals(false); +} + +void PlanesDialog::setStatus(const QString& status) +{ + ui->statusValueLabel->setText(status); +} + +void PlanesDialog::setEnabledFlag(bool enabled) +{ + if (planes) + planes->setEnabled(enabled); +} + +void PlanesDialog::setShowLabels(bool enabled) +{ + if (planes) + planes->setFlagShowLabels(enabled); +} + +void PlanesDialog::setShowButton(bool enabled) +{ + if (planes) + planes->setFlagShowButton(enabled); +} + +void PlanesDialog::setLabelMode(int index) +{ + if (!planes) + return; + + if (index >= 0) + planes->setLabelMode(ui->labelModeComboBox->itemData(index).toInt()); +} + +void PlanesDialog::setProvider(int index) +{ + if (!planes) + return; + + if (index >= 0) + planes->setProviderId(ui->providerComboBox->itemData(index).toString()); +} + +void PlanesDialog::setRadius(int radiusNm) +{ + if (planes) + planes->setRadiusNm(radiusNm); +} + +void PlanesDialog::setFetchInterval(int seconds) +{ + if (planes) + planes->setFetchIntervalSec(seconds); +} + +void PlanesDialog::triggerRefresh() +{ + if (planes) + planes->refreshNow(); +} + +void PlanesDialog::openExternalLink(const QString& url) +{ + QDesktopServices::openUrl(QUrl(url)); +} + +void PlanesDialog::setAboutHtml() +{ + QString html = ""; + html += "

" + q_("Planes Plug-in") + "

"; + html += ""; + html += ""; + html += ""; + html += "
" + q_("Version") + ":0.1.0
" + q_("License") + ":GPL v2 or later
" + q_("Authors") + ":Felix Zeltner, Georg Zotti, Kamil Zaraś (astronow.pl)
"; + html += "

" + q_("This plug-in shows live ADS-B aircraft as native Stellarium objects.") + "

"; + html += "

" + q_("It provides basic visibility, label, and refresh controls for a live aircraft feed around the current observer location.") + "

"; + html += "

" + q_("Live requests remain disabled until you enable aircraft display. When enabled, the plugin sends the current observer latitude, longitude, and search radius to the configured data source.") + "

"; + html += "

" + q_("The plugin is not loaded at Stellarium startup unless you explicitly enable it in the plug-in manager.") + "

"; + html += ""; + + StelGui* gui = dynamic_cast(StelApp::getInstance().getGui()); + if (gui) + { + QString htmlStyleSheet(gui->getStelStyle().htmlStyleSheet); + ui->aboutTextBrowser->document()->setDefaultStyleSheet(htmlStyleSheet); + } + ui->aboutTextBrowser->setHtml(html); +} diff --git a/plugins/Planes/src/gui/PlanesDialog.hpp b/plugins/Planes/src/gui/PlanesDialog.hpp new file mode 100644 index 0000000000000..74956cc89d64c --- /dev/null +++ b/plugins/Planes/src/gui/PlanesDialog.hpp @@ -0,0 +1,44 @@ +#ifndef PLANESDIALOG_HPP +#define PLANESDIALOG_HPP + +#include "StelDialog.hpp" + +class Ui_planesDialog; +class Planes; + +class PlanesDialog : public StelDialog +{ + Q_OBJECT + +public: + PlanesDialog(); + ~PlanesDialog() override; + +public slots: + void retranslate() override; + void updateFromPlugin(); + void setStatus(const QString& status); + +protected: + void createDialogContent() override; + +private slots: + void setEnabledFlag(bool enabled); + void setShowLabels(bool enabled); + void setShowButton(bool enabled); + void setLabelMode(int index); + void setProvider(int index); + void setRadius(int radiusNm); + void setFetchInterval(int seconds); + void triggerRefresh(); + void openExternalLink(const QString& url); + +private: + void updateComboTexts(); + void setAboutHtml(); + + Planes* planes; + Ui_planesDialog* ui; +}; + +#endif diff --git a/plugins/Planes/src/gui/planesDialog.ui b/plugins/Planes/src/gui/planesDialog.ui new file mode 100644 index 0000000000000..4e9d68c6ad11d --- /dev/null +++ b/plugins/Planes/src/gui/planesDialog.ui @@ -0,0 +1,245 @@ + + + planesDialog + + + + 0 + 0 + 700 + 480 + + + + Planes Plug-in Configuration + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + Settings + + + + + + General + + + + + + Show live aircraft + + + + + + + true + + + When enabled, Stellarium requests live ADS-B positions around the current observer location. + + + + + + + Show aircraft labels + + + + + + + + + Label content + + + + + + + + + + + + Show toolbar button + + + + + + + + + + Live Data + + + + + + Search radius + + + + + + + + + + Refresh interval + + + + + + + + + + + + + + + + + true + + + Intervals below 15 seconds are disabled to reduce the risk of provider rate limits. + + + + + + + Provider + + + + + + + + + + Provider website + + + + + + + true + + + true + + + + + + + + + + Last successful update + + + + + + + true + + + Never + + + + + + + Status + + + + + + + idle + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Update now + + + + + + + + + + About + + + + + + + + + + + + + + From 2cdfb6b38b0706de8f92b567177928a3f724f9df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Zara=C5=9B?= Date: Thu, 21 May 2026 15:40:17 +0200 Subject: [PATCH 3/5] Add Planes source copyright headers --- plugins/Planes/src/AircraftObject.cpp | 19 +++++++++++++++++++ plugins/Planes/src/AircraftObject.hpp | 19 +++++++++++++++++++ plugins/Planes/src/AircraftRecord.hpp | 19 +++++++++++++++++++ plugins/Planes/src/Planes.cpp | 19 +++++++++++++++++++ plugins/Planes/src/Planes.hpp | 20 +++++++++++++++++++- plugins/Planes/src/gui/PlanesDialog.cpp | 19 +++++++++++++++++++ plugins/Planes/src/gui/PlanesDialog.hpp | 19 +++++++++++++++++++ 7 files changed, 133 insertions(+), 1 deletion(-) diff --git a/plugins/Planes/src/AircraftObject.cpp b/plugins/Planes/src/AircraftObject.cpp index 97675af9532ec..da01467117d2b 100644 --- a/plugins/Planes/src/AircraftObject.cpp +++ b/plugins/Planes/src/AircraftObject.cpp @@ -1,3 +1,22 @@ +/* + * Copyright (C) 2013 Felix Zeltner + * Copyright (C) 2026 Kamil Zaraś (astronow.pl) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA. + */ + #include "AircraftObject.hpp" #include "StelCore.hpp" diff --git a/plugins/Planes/src/AircraftObject.hpp b/plugins/Planes/src/AircraftObject.hpp index f3447b811cc86..077bc7c82ffc0 100644 --- a/plugins/Planes/src/AircraftObject.hpp +++ b/plugins/Planes/src/AircraftObject.hpp @@ -1,3 +1,22 @@ +/* + * Copyright (C) 2013 Felix Zeltner + * Copyright (C) 2026 Kamil Zaraś (astronow.pl) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA. + */ + #ifndef PLANES_AIRCRAFTOBJECT_HPP #define PLANES_AIRCRAFTOBJECT_HPP diff --git a/plugins/Planes/src/AircraftRecord.hpp b/plugins/Planes/src/AircraftRecord.hpp index 1573b35b0b4bb..2c63e9204eaf0 100644 --- a/plugins/Planes/src/AircraftRecord.hpp +++ b/plugins/Planes/src/AircraftRecord.hpp @@ -1,3 +1,22 @@ +/* + * Copyright (C) 2013 Felix Zeltner + * Copyright (C) 2026 Kamil Zaraś (astronow.pl) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA. + */ + #ifndef PLANES_AIRCRAFTRECORD_HPP #define PLANES_AIRCRAFTRECORD_HPP diff --git a/plugins/Planes/src/Planes.cpp b/plugins/Planes/src/Planes.cpp index e72150f1ec5ff..c23dd73d6b5e5 100644 --- a/plugins/Planes/src/Planes.cpp +++ b/plugins/Planes/src/Planes.cpp @@ -1,3 +1,22 @@ +/* + * Copyright (C) 2013 Felix Zeltner + * Copyright (C) 2026 Kamil Zaraś (astronow.pl) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA. + */ + #include "Planes.hpp" #include "StelApp.hpp" diff --git a/plugins/Planes/src/Planes.hpp b/plugins/Planes/src/Planes.hpp index 3d1e7fd641d0a..f5f4ea3d63669 100644 --- a/plugins/Planes/src/Planes.hpp +++ b/plugins/Planes/src/Planes.hpp @@ -1,3 +1,22 @@ +/* + * Copyright (C) 2013 Felix Zeltner + * Copyright (C) 2026 Kamil Zaraś (astronow.pl) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA. + */ + #ifndef PLANES_HPP #define PLANES_HPP @@ -7,7 +26,6 @@ #include "StelTextureTypes.hpp" #include -#include #include #include diff --git a/plugins/Planes/src/gui/PlanesDialog.cpp b/plugins/Planes/src/gui/PlanesDialog.cpp index 0f8280e424328..250534fbe3a94 100644 --- a/plugins/Planes/src/gui/PlanesDialog.cpp +++ b/plugins/Planes/src/gui/PlanesDialog.cpp @@ -1,3 +1,22 @@ +/* + * Copyright (C) 2013 Felix Zeltner + * Copyright (C) 2026 Kamil Zaraś (astronow.pl) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA. + */ + #include "gui/PlanesDialog.hpp" #include "ui_planesDialog.h" diff --git a/plugins/Planes/src/gui/PlanesDialog.hpp b/plugins/Planes/src/gui/PlanesDialog.hpp index 74956cc89d64c..f858524e66ebc 100644 --- a/plugins/Planes/src/gui/PlanesDialog.hpp +++ b/plugins/Planes/src/gui/PlanesDialog.hpp @@ -1,3 +1,22 @@ +/* + * Copyright (C) 2013 Felix Zeltner + * Copyright (C) 2026 Kamil Zaraś (astronow.pl) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA. + */ + #ifndef PLANESDIALOG_HPP #define PLANESDIALOG_HPP From b873520b62256aeb183537288be015c05e0de581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Zara=C5=9B?= Date: Fri, 22 May 2026 00:22:45 +0200 Subject: [PATCH 4/5] Polish Planes UI and realtime behavior --- plugins/Planes/src/AircraftObject.cpp | 6 +- plugins/Planes/src/Planes.cpp | 90 ++++++++++++++++++++++--- plugins/Planes/src/Planes.hpp | 1 + plugins/Planes/src/gui/PlanesDialog.cpp | 10 +-- plugins/Planes/src/gui/planesDialog.ui | 15 +++++ 5 files changed, 101 insertions(+), 21 deletions(-) diff --git a/plugins/Planes/src/AircraftObject.cpp b/plugins/Planes/src/AircraftObject.cpp index da01467117d2b..60403b6ec976e 100644 --- a/plugins/Planes/src/AircraftObject.cpp +++ b/plugins/Planes/src/AircraftObject.cpp @@ -38,6 +38,7 @@ constexpr double kMaxDeadReckoningSeconds = 30.0; constexpr double kHeadingProbeSeconds = 1.0; constexpr double kMetersToFeet = 3.280839895; constexpr double kMetersPerSecondToKnots = 1.943844492; +constexpr double kMetersPerSecondToFeetPerMinute = 196.8503937; constexpr float kPlaneSpriteSize = 16.0f; constexpr float kSpriteHeadingOffsetDegrees = -180.0f; @@ -191,8 +192,9 @@ QString AircraftObject::getInfoString(const StelCore* core, const InfoStringGrou const QString groundSpeed = QString("%1 m/s / %2 kt") .arg(QString::number(currentRecord.groundSpeedMs, 'f', 0), QString::number(currentRecord.groundSpeedMs * kMetersPerSecondToKnots, 'f', 0)); - const QString verticalRate = QString("%1 m/s") - .arg(QString::number(currentRecord.verticalRateMs, 'f', 1)); + const QString verticalRate = QString("%1 m/s / %2 ft/min") + .arg(QString::number(currentRecord.verticalRateMs, 'f', 1), + QString::number(currentRecord.verticalRateMs * kMetersPerSecondToFeetPerMinute, 'f', 0)); stream << QString("%1: %2
").arg(q_("Identifier"), aircraftRecord.icao24.toHtmlEscaped()); if (!aircraftRecord.callsign.isEmpty()) diff --git a/plugins/Planes/src/Planes.cpp b/plugins/Planes/src/Planes.cpp index c23dd73d6b5e5..9198a0da0b5bb 100644 --- a/plugins/Planes/src/Planes.cpp +++ b/plugins/Planes/src/Planes.cpp @@ -76,6 +76,7 @@ constexpr int kMaxPublishedAircraft = 200; constexpr double kFeetToMeters = 0.3048; constexpr double kKnotsToMetersPerSecond = 0.514444; constexpr double kFeetPerMinuteToMetersPerSecond = 0.00508; +const QString kRealtimeOnlyStatus = QStringLiteral("Live aircraft are shown only in real-time mode."); ProviderDefinition providerDefinition(const QString& providerId) { @@ -186,6 +187,11 @@ QJsonArray aircraftArrayFromResponse(const QJsonObject& root) return aircraftArray; return root.value("ac").toArray(); } + +bool isRealtimeMode(const StelCore* core) +{ + return core && core->getIsTimeNow(); +} } StelModule* PlanesStelPluginInterface::getStelModule() const @@ -222,6 +228,7 @@ Planes::Planes() , enabled(false) , showLabels(false) , showButton(true) + , lastRealtimeState(true) , labelMode(kLabelModeFlightNumber) , lastStatus(QStringLiteral("idle")) , lastSuccessfulUpdate(QStringLiteral("Never")) @@ -275,7 +282,7 @@ void Planes::init() Q_INIT_RESOURCE(Planes); loadSettings(); planeTexture = StelApp::getInstance().getTextureManager().createTexture(":/planes/plane.png"); - pointerTexture = StelApp::getInstance().getTextureManager().createTexture(StelFileMgr::getInstallationDir()+"/textures/pointeur2.png"); + pointerTexture = StelApp::getInstance().getTextureManager().createTexture(StelFileMgr::getInstallationDir()+"/textures/pointeur5.png"); networkMgr = new QNetworkAccessManager(this); connect(networkMgr, &QNetworkAccessManager::finished, this, &Planes::onReply); @@ -291,18 +298,29 @@ void Planes::init() GETSTELMODULE(StelObjectMgr)->registerStelObjectMgr(this); connect(StelApp::getInstance().getCore(), &StelCore::locationChanged, this, &Planes::onLocationChanged); + lastRealtimeState = isRealtimeMode(StelApp::getInstance().getCore()); addAction("actionShow_Planes", N_("Planes"), N_("Show Planes"), "enabled", "Shift+P"); #ifndef NO_GUI addAction("actionShow_Planes_dialog", N_("Planes"), N_("Show settings dialog"), configDialog, "visible", "Ctrl+P"); applyButtonVisibility(); #endif - updateStatus(enabled ? QStringLiteral("enabled") : QStringLiteral("disabled; live updates are off")); - if (enabled) { fetchTimer->start(); - QTimer::singleShot(1500, this, &Planes::fetchAircraft); + if (lastRealtimeState) + { + updateStatus(QStringLiteral("Waiting for first update...")); + QTimer::singleShot(1500, this, &Planes::fetchAircraft); + } + else + { + updateStatus(kRealtimeOnlyStatus); + } + } + else + { + updateStatus(QStringLiteral("disabled; live updates are off")); } } @@ -319,6 +337,25 @@ void Planes::deinit() void Planes::update(double deltaTime) { Q_UNUSED(deltaTime) + + StelCore* core = StelApp::getInstance().getCore(); + const bool realtimeNow = isRealtimeMode(core); + if (realtimeNow == lastRealtimeState) + return; + + lastRealtimeState = realtimeNow; + if (!enabled) + return; + + if (realtimeNow) + { + updateStatus(QStringLiteral("Returned to real-time mode.")); + scheduleRefresh(0); + } + else + { + updateStatus(kRealtimeOnlyStatus); + } } QString Planes::buildRequestUrl(const StelCore* core) const @@ -367,6 +404,9 @@ void Planes::fetchAircraft() return; StelCore* core = StelApp::getInstance().getCore(); + if (!isRealtimeMode(core)) + return; + const QString url = buildRequestUrl(core); if (url.isEmpty()) { @@ -397,6 +437,12 @@ void Planes::onReply(QNetworkReply* reply) pendingRefresh = false; reply->deleteLater(); + if (!isRealtimeMode(StelApp::getInstance().getCore())) + { + updateStatus(kRealtimeOnlyStatus); + return; + } + if (reply->error() != QNetworkReply::NoError) { if (reply->error() != QNetworkReply::OperationCanceledError) @@ -457,7 +503,7 @@ void Planes::onLocationChanged(const StelLocation& loc) void Planes::draw(StelCore* core) { - if (!core || !enabled) + if (!core || !enabled || !isRealtimeMode(core)) return; StelProjectorP projection = core->getProjection(StelCore::FrameAltAz, StelCore::RefractionOff); @@ -483,12 +529,20 @@ void Planes::draw(StelCore* core) if (!pointerPainter.getProjector()->project(object->getJ2000EquatorialPos(core).toVec3f(), screenPos)) return; - pointerPainter.setColor(object->getInfoColor()); + pointerPainter.setColor(0.4f, 0.5f, 0.8f); pointerTexture->bind(); pointerPainter.setBlending(true); - const float angle = static_cast(StelApp::getInstance().getAnimationTime()) * 40.0f; const float scale = StelApp::getInstance().getScreenScale(); - pointerPainter.drawSprite2dModeNoDeviceScale(screenPos[0], screenPos[1], 13.0f * scale, angle); + float size = static_cast(object->getAngularRadius(core) * (2. * M_PI_180) * + static_cast(pointerPainter.getProjector()->getPixelPerRadAtCenter())); + size += (12.f + 3.f * std::sin(2. * StelApp::getInstance().getTotalRunTime())) * scale; + const float radius = 20.f * scale; + const float x = screenPos[0]; + const float y = screenPos[1]; + pointerPainter.drawSprite2dModeNoDeviceScale(x - size / 2.f, y - size / 2.f, radius, 90.f); + pointerPainter.drawSprite2dModeNoDeviceScale(x - size / 2.f, y + size / 2.f, radius, 0.f); + pointerPainter.drawSprite2dModeNoDeviceScale(x + size / 2.f, y + size / 2.f, radius, -90.f); + pointerPainter.drawSprite2dModeNoDeviceScale(x + size / 2.f, y - size / 2.f, radius, -180.f); } double Planes::getCallOrder(StelModuleActionName actionName) const @@ -535,8 +589,15 @@ void Planes::setEnabled(bool value) { if (fetchTimer) fetchTimer->start(); - updateStatus(QStringLiteral("Waiting for first update...")); - scheduleRefresh(0); + if (isRealtimeMode(StelApp::getInstance().getCore())) + { + updateStatus(QStringLiteral("Waiting for first update...")); + scheduleRefresh(0); + } + else + { + updateStatus(kRealtimeOnlyStatus); + } } emit enabledChanged(enabled); } @@ -617,12 +678,19 @@ void Planes::setRadiusNm(int nm) void Planes::refreshNow() { if (enabled) + { + if (!isRealtimeMode(StelApp::getInstance().getCore())) + { + updateStatus(kRealtimeOnlyStatus); + return; + } fetchAircraft(); + } } void Planes::scheduleRefresh(int delayMs) { - if (!enabled || !refreshDebounceTimer) + if (!enabled || !refreshDebounceTimer || !isRealtimeMode(StelApp::getInstance().getCore())) return; refreshDebounceTimer->start(delayMs); diff --git a/plugins/Planes/src/Planes.hpp b/plugins/Planes/src/Planes.hpp index f5f4ea3d63669..1318e2154b940 100644 --- a/plugins/Planes/src/Planes.hpp +++ b/plugins/Planes/src/Planes.hpp @@ -131,6 +131,7 @@ private slots: bool enabled; bool showLabels; bool showButton; + bool lastRealtimeState; int labelMode; QString lastStatus; QString lastSuccessfulUpdate; diff --git a/plugins/Planes/src/gui/PlanesDialog.cpp b/plugins/Planes/src/gui/PlanesDialog.cpp index 250534fbe3a94..b8d58cbac021c 100644 --- a/plugins/Planes/src/gui/PlanesDialog.cpp +++ b/plugins/Planes/src/gui/PlanesDialog.cpp @@ -32,7 +32,6 @@ #include #include #include -#include namespace { @@ -78,11 +77,6 @@ void PlanesDialog::createDialogContent() planes = GETSTELMODULE(Planes); ui->setupUi(dialog); - TitleBar* titleBar = new TitleBar(dialog); - titleBar->setTitle(q_("Planes Plug-in Configuration")); - if (auto* layout = qobject_cast(dialog->layout())) - layout->insertWidget(0, titleBar); - ui->refreshIntervalSpinBox->setMinimum(15); ui->refreshIntervalSpinBox->setMaximum(60); ui->refreshIntervalSpinBox->setSingleStep(5); @@ -110,8 +104,8 @@ void PlanesDialog::createDialogContent() } connect(&StelApp::getInstance(), SIGNAL(languageChanged()), this, SLOT(retranslate())); - connect(titleBar, &TitleBar::closeClicked, this, &StelDialog::close); - connect(titleBar, SIGNAL(movedTo(QPoint)), this, SLOT(handleMovedTo(QPoint))); + connect(ui->titleBar, &TitleBar::closeClicked, this, &StelDialog::close); + connect(ui->titleBar, SIGNAL(movedTo(QPoint)), this, SLOT(handleMovedTo(QPoint))); connect(ui->enabledCheckBox, &QCheckBox::toggled, this, &PlanesDialog::setEnabledFlag); connect(ui->showLabelsCheckBox, &QCheckBox::toggled, this, &PlanesDialog::setShowLabels); diff --git a/plugins/Planes/src/gui/planesDialog.ui b/plugins/Planes/src/gui/planesDialog.ui index 4e9d68c6ad11d..de72d7dc240ab 100644 --- a/plugins/Planes/src/gui/planesDialog.ui +++ b/plugins/Planes/src/gui/planesDialog.ui @@ -29,6 +29,13 @@ 0 + + + + Planes Plug-in Configuration + + + @@ -241,5 +248,13 @@ + + + TitleBar + QFrame +
Dialog.hpp
+ 1 +
+
From 669a444492b717f1d6b8f48b8558060a263b4e77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Zara=C5=9B?= Date: Sat, 23 May 2026 23:55:45 +0200 Subject: [PATCH 5/5] Update Planes translation source list --- po/stellarium/POTFILES.in | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/po/stellarium/POTFILES.in b/po/stellarium/POTFILES.in index 06605f58eb47e..33414519f1e00 100644 --- a/po/stellarium/POTFILES.in +++ b/po/stellarium/POTFILES.in @@ -265,6 +265,10 @@ plugins/OnlineQueries/src/HipOnlineQuery.cpp plugins/OnlineQueries/src/OnlineQueries.cpp plugins/OnlineQueries/src/gui/OnlineQueriesDialog.cpp plugins/OnlineQueries/src/ui_onlineQueriesDialog.h +plugins/Planes/src/AircraftObject.cpp +plugins/Planes/src/Planes.cpp +plugins/Planes/src/gui/PlanesDialog.cpp +plugins/Planes/src/ui_planesDialog.h plugins/LensDistortionEstimator/src/LensDistortionEstimator.cpp plugins/LensDistortionEstimator/src/gui/LensDistortionEstimatorDialog.cpp plugins/LensDistortionEstimator/src/ui_lensDistortionEstimatorDialog.h