Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions cmake/default_cfg.ini.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Exoplanets = true
MeteorShowers = true
Novae = true
FOV = true
Planes = false

[video]
fullscreen = true
Expand Down
1 change: 1 addition & 0 deletions plugins/Planes/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ADD_SUBDIRECTORY(src)
4 changes: 4 additions & 0 deletions plugins/Planes/module.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[module]
id=Planes
version=0.1.0
stellarium-minimum-version=26.1.0
7 changes: 7 additions & 0 deletions plugins/Planes/resources/Planes.qrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<RCC>
<qresource prefix="/planes">
<file>plane.png</file>
<file>planes_off_160.png</file>
<file>planes_on_160.png</file>
</qresource>
</RCC>
Binary file added plugins/Planes/resources/plane.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added plugins/Planes/resources/planes_off_160.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added plugins/Planes/resources/planes_on_160.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
320 changes: 320 additions & 0 deletions plugins/Planes/src/AircraftObject.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
/*
* 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"
#include "StelLocation.hpp"
#include "StelPainter.hpp"
#include "StelProjector.hpp"
#include "StelTranslator.hpp"
#include "StelUtils.hpp"

#include <QTextStream>
#include <cmath>

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 double kMetersPerSecondToFeetPerMinute = 196.8503937;
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<int>(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("<h2>%1</h2>").arg(labelText().toHtmlEscaped());
if (flags & ObjectType)
stream << QString("%1: <b>%2</b><br />").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 / %2 ft/min")
.arg(QString::number(currentRecord.verticalRateMs, 'f', 1),
QString::number(currentRecord.verticalRateMs * kMetersPerSecondToFeetPerMinute, 'f', 0));

stream << QString("%1: <b>%2</b><br />").arg(q_("Identifier"), aircraftRecord.icao24.toHtmlEscaped());
if (!aircraftRecord.callsign.isEmpty())
stream << QString("%1: <b>%2</b><br />").arg(q_("Flight"), aircraftRecord.callsign.toHtmlEscaped());
if (!aircraftRecord.aircraftType.isEmpty())
stream << QString("%1: <b>%2</b><br />").arg(q_("Model"), aircraftRecord.aircraftType.toHtmlEscaped());
stream << "<br />";
stream << "<table style='margin:0em 0em 0em -0.125em;border-spacing:0px;border:0px;'>";
stream << QString("<tr><td>%1:</td><td><b>%2</b></td></tr>").arg(q_("Altitude"), altitude);
stream << QString("<tr><td>%1:</td><td><b>%2</b></td></tr>").arg(q_("Ground speed"), groundSpeed);
stream << QString("<tr><td>%1:</td><td><b>%2</b></td></tr>").arg(q_("Vertical rate"), verticalRate);
stream << QString("<tr><td>%1:</td><td><b>%2</b></td></tr>").arg(q_("Track"), heading);
stream << QString("<tr><td>%1:</td><td><b>%2 s</b></td></tr>")
.arg(q_("Data age"), QString::number(dataAgeSeconds, 'f', dataAgeSeconds < 10.0 ? 1 : 0));
stream << "</table>";
}

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<double>(location.getLatitude()) * M_PI / 180.0;
const double obsLon = static_cast<double>(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<double>(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<float>(normalizeDegrees(aircraftRecord.trackDegrees) + kSpriteHeadingOffsetDegrees);

Vec3d futureScreenPos;
if (!projector->project(getAltAzPos(core, getElapsedSeconds() + kHeadingProbeSeconds), futureScreenPos))
return static_cast<float>(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<float>(normalizeDegrees(aircraftRecord.trackDegrees) + kSpriteHeadingOffsetDegrees);

const double angleDeg = std::atan2(dy, dx) * 180.0 / M_PI;
return static_cast<float>(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<float>(screenPos[0]), static_cast<float>(screenPos[1]),
kPlaneSpriteSize, getScreenRotationDegrees(core, projector, screenPos));

if (drawLabels)
{
painter->drawText(static_cast<float>(screenPos[0]), static_cast<float>(screenPos[1]), displayLabelText(labelMode), 0, 10.0f, 10.0f, false);
}
}
Loading
Loading