celestia/src/celestia/qt/qtinfopanel.cpp

445 lines
14 KiB
C++

// qtinfopanel.cpp
//
// Copyright (C) 2008, Celestia Development Team
// celestia-developers@lists.sourceforge.net
//
// Information panel for Qt4 UI.
//
// 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.
#include <celestia/celestiacore.h>
#include <celengine/astro.h>
#include <celutil/gettext.h>
#include <celutil/utf8.h>
#include <celengine/universe.h>
#include <QTextBrowser>
#include <QItemSelection>
#include "qtinfopanel.h"
using namespace std;
using namespace Eigen;
using namespace celmath;
// TODO: This should be moved to astro.cpp
struct OrbitalElements
{
double semimajorAxis;
double eccentricity;
double inclination;
double longAscendingNode;
double argPericenter;
double meanAnomaly;
double period;
};
static void CalculateOsculatingElements(const Orbit& orbit,
double t,
double dt,
OrbitalElements* elements);
InfoPanel::InfoPanel(CelestiaCore* _appCore, const QString& title, QWidget* parent) :
QDockWidget(title, parent),
appCore(_appCore)
{
textBrowser = new QTextBrowser(this);
textBrowser->setOpenExternalLinks(true);
setWidget(textBrowser);
}
void InfoPanel::buildInfoPage(Selection sel,
Universe* universe,
double tdb)
{
QString pageText;
QTextStream stream(&pageText, QIODevice::WriteOnly);
pageHeader(stream);
if (sel.body() != nullptr)
{
buildSolarSystemBodyPage(sel.body(), tdb, stream);
}
else if (sel.star() != nullptr)
{
buildStarPage(sel.star(), universe, tdb, stream);
}
else if (sel.deepsky() != nullptr)
{
buildDSOPage(sel.deepsky(), universe, stream);
}
else
{
stream << QString(_("Error: no object selected!\n"));
}
pageFooter(stream);
textBrowser->setHtml(pageText);
}
void InfoPanel::pageHeader(QTextStream& stream)
{
stream << "<html><head><title>" << QString(_("Info")) << "</title></head><body>";
}
void InfoPanel::pageFooter(QTextStream& stream)
{
stream << "</body></html>";
}
static QString anchor(const QString& href, const QString& text)
{
return QString("<a href=\"%1\">%2</a>").arg(href, text);
}
void InfoPanel::buildSolarSystemBodyPage(const Body* body,
double t,
QTextStream& stream)
{
stream << QString("<h1>%1</h1>").arg(QString::fromStdString(body->getName(true)));
if (!body->getInfoURL().empty())
{
QString infoURL = QString::fromStdString(body->getInfoURL());
stream << QString(_("Web info: %1")).arg(anchor(infoURL, infoURL)) << "<br>\n";
}
stream << "<br>";
bool isArtificial = body->getClassification() == Body::Spacecraft;
QString units(_("km"));
double radius = body->getRadius();
if (radius < 1.0)
{
units = _("m");
radius *= 1000.0;
}
if (body->isEllipsoid())
stream << QString(_("<b>Equatorial radius:</b> %L1 %2")).arg(radius).arg(units) << "<br>\n";
else
stream << QString(_("<b>Size:</b> %L1 %2")).arg(radius).arg(units) << "<br>\n";
#if 0
if (body->getOblateness() != 0.0f && body->isEllipsoid())
{
stream << QString(_("<b>Oblateness: ")) << body->getOblateness() << "<br>\n";
}
#endif
double orbitalPeriod = 0.0;
if (body->getOrbit(t)->isPeriodic())
orbitalPeriod = body->getOrbit(t)->getPeriod();
// Show rotation information for natural, periodic rotators
if (body->getRotationModel(t)->isPeriodic() && !isArtificial)
{
double rotPeriod = body->getRotationModel(t)->getPeriod();
double dayLength = 0.0;
if (body->getOrbit(t)->isPeriodic())
{
double siderealDaysPerYear = orbitalPeriod / rotPeriod;
double solarDaysPerYear = siderealDaysPerYear - 1.0;
if (solarDaysPerYear > 0.0001)
{
dayLength = orbitalPeriod / (siderealDaysPerYear - 1.0);
}
}
if (rotPeriod < 2.0)
{
rotPeriod *= 24.0;
dayLength *= 24.0;
units = _("hours");
}
else
{
units = _("days");
}
stream << QString(_("<b>Sidereal rotation period:</b> %L1 %2")).arg(rotPeriod).arg(units) << "<br>\n";
if (dayLength != 0.0)
{
stream << QString(_("<b>Length of day:</b> %L1 %2")).arg(dayLength).arg(units) << "<br>\n";
}
}
// Orbit information
if (orbitalPeriod != 0.0 || 1)
{
if (orbitalPeriod == 0.0)
orbitalPeriod = 365.25;
OrbitalElements elements;
CalculateOsculatingElements(*body->getOrbit(t),
t,
orbitalPeriod * 1.0e-6,
&elements);
if (orbitalPeriod < 2.0)
{
orbitalPeriod *= 24.0;
units = _("hours");
}
else if (orbitalPeriod < 365.25 * 2.0)
{
units = _("days");
}
else
{
units = _("years");
orbitalPeriod /= 365.25;
}
if (body->getRings() != nullptr)
stream << QString(_("<b>Has rings</b>")) << "<br>\n";
if (body->getAtmosphere() != nullptr)
stream << QString(_("<b>Has atmosphere</b>")) << "<br>\n";
// Start and end dates
double startTime = 0.0;
double endTime = 0.0;
body->getLifespan(startTime, endTime);
if (startTime > -1.0e9)
stream << "<br>" << QString(_("<b>Start:</b> %1")).arg(astro::TDBtoUTC(startTime).toCStr()) << "<br>\n";
if (endTime < 1.0e9)
stream << "<br>" << QString(_("<b>End:</b> %1")).arg(astro::TDBtoUTC(endTime).toCStr()) << "<br>\n";
stream << "<br><big><b>" << QString(_("Orbit information")) << "</b></big><br>\n";
stream << QString(_("Osculating elements for %1")).arg(astro::TDBtoUTC(t).toCStr()) << "<br>\n";
stream << "<br>\n";
// stream << "<i>[ Orbit reference plane info goes here ]</i><br>\n";
stream << QString(_("<b>Period:</b> %L1 %2")).arg(orbitalPeriod).arg(units) << "<br>\n";
double sma = elements.semimajorAxis;
if (sma > 2.5e7)
{
units = _("AU");
sma = astro::kilometersToAU(sma);
}
else
{
units = _("km");
}
stream << QString(_("<b>Semi-major axis:</b> %L1 %2")).arg(sma).arg(units) << "<br>\n";
stream << QString(_("<b>Eccentricity:</b> %L1")).arg(elements.eccentricity) << "<br>\n";
stream << QString(_("<b>Inclination:</b> %L1%2")).arg(radToDeg(elements.inclination)).arg(QString::fromUtf8(UTF8_DEGREE_SIGN)) << "<br>\n";
stream << QString(_("<b>Pericenter distance:</b> %L1 %2")).arg(sma * (1 - elements.eccentricity)).arg(units) << "<br>\n";
stream << QString(_("<b>Apocenter distance:</b> %L1 %2")).arg(sma * (1 + elements.eccentricity)).arg(units) << "<br>\n";
stream << QString(_("<b>Ascending node:</b> %L1%2")).arg(radToDeg(elements.longAscendingNode)).arg(QString::fromUtf8(UTF8_DEGREE_SIGN)) << "<br>\n";
stream << QString(_("<b>Argument of periapsis:</b> %L1%2")).arg(radToDeg(elements.argPericenter)).arg(QString::fromUtf8(UTF8_DEGREE_SIGN)) << "<br>\n";
stream << QString(_("<b>Mean anomaly:</b> %L1%2")).arg(radToDeg(elements.meanAnomaly)).arg(QString::fromUtf8(UTF8_DEGREE_SIGN)) << "<br>\n";
stream << QString(_("<b>Period (calculated):</b> %L1 %2")).arg(elements.period).arg(_("days")) << "<br>\n";;
}
}
Vector3d celToJ2000Ecliptic(const Vector3d& p)
{
return Vector3d(p.x(), -p.z(), p.y());
}
Vector3d rectToSpherical(const Vector3d& v)
{
double r = v.norm();
double theta = atan2(v.y(), v.x());
if (theta < 0)
theta = theta + 2 * PI;
double phi = asin(v.z() / r);
return Vector3d(theta, phi, r);
}
void InfoPanel::buildStarPage(const Star* star, const Universe* universe, double tdb, QTextStream& stream)
{
string name = ReplaceGreekLetterAbbr(universe->getStarCatalog()->getStarName(*star, true));
stream << QString("<h1>%1</h1>").arg(QString::fromStdString(name));
// Compute the star's position relative to the Solar System Barycenter. Note that
// this will ignore the effect of parallax in the star's position.
// TODO: Use either the observer's position or the Earth's position as the
// origin instead.
Vector3d celPos = star->getPosition(tdb).offsetFromKm(UniversalCoord::Zero());
Vector3d eqPos = astro::eclipticToEquatorial(celToJ2000Ecliptic(celPos));
Vector3d sph = rectToSpherical(eqPos);
int hours = 0;
int minutes = 0;
double seconds = 0;
astro::decimalToHourMinSec(radToDeg(sph.x()), hours, minutes, seconds);
stream << QString(_("<b>RA:</b> %L1h %L2m %L3s")).arg(hours).arg(abs(minutes)).arg(abs(seconds)) << "<br>\n";
int degrees = 0;
astro::decimalToDegMinSec(radToDeg(sph.y()), degrees, minutes, seconds);
stream << QString(_("<b>Dec:</b> %L1%2 %L3' %L4\"")).arg(degrees).arg(QString::fromUtf8(UTF8_DEGREE_SIGN))
.arg(abs(minutes)).arg(abs(seconds)) << "<br>\n";
}
void InfoPanel::buildDSOPage(const DeepSkyObject* dso,
const Universe* universe,
QTextStream& stream)
{
string name = universe->getDSOCatalog()->getDSOName(dso, true);
stream << QString("<h1>%1</h1>").arg(QString::fromStdString(name));
Vector3d eqPos = astro::eclipticToEquatorial(celToJ2000Ecliptic(dso->getPosition()));
Vector3d sph = rectToSpherical(eqPos);
int hours = 0;
int minutes = 0;
double seconds = 0;
astro::decimalToHourMinSec(radToDeg(sph.x()), hours, minutes, seconds);
stream << QString(_("<b>RA:</b> %L1h %L2m %L3s")).arg(hours).arg(abs(minutes)).arg(abs(seconds)) << "<br>\n";
int degrees = 0;
astro::decimalToDegMinSec(radToDeg(sph.y()), degrees, minutes, seconds);
stream << QString(_("<b>Dec:</b> %L1%2 %L3' %L4\"")).arg(degrees).arg(QString::fromUtf8(UTF8_DEGREE_SIGN))
.arg(abs(minutes)).arg(abs(seconds)) << "<br>\n";
Vector3d galPos = astro::equatorialToGalactic(eqPos);
sph = rectToSpherical(galPos);
astro::decimalToDegMinSec(radToDeg(sph.x()), degrees, minutes, seconds);
// TRANSLATORS: Galactic longitude
stream << QString(_("<b>L:</b> %L1%2 %L3' %L4\"")).arg(degrees).arg(QString::fromUtf8(UTF8_DEGREE_SIGN))
.arg(abs(minutes)).arg(abs(seconds)) << "<br>\n";
astro::decimalToDegMinSec(radToDeg(sph.y()), degrees, minutes, seconds);
// TRANSLATORS: Galactic latitude
stream << QString(_("<b>B:</b> %L1%2 %L3' %L4\"")).arg(degrees).arg(QString::fromUtf8(UTF8_DEGREE_SIGN))
.arg(abs(minutes)).arg(abs(seconds)) << "<br>\n";
}
void InfoPanel::updateHelper(ModelHelper* model, const QItemSelection& newSel, const QItemSelection& oldSel)
{
if (!isVisible() || newSel == oldSel || newSel.indexes().length() == 0)
return;
Selection sel = model->itemForInfoPanel(newSel.indexes().at(0));
if (!sel.empty())
{
buildInfoPage(sel,
appCore->getSimulation()->getUniverse(),
appCore->getSimulation()->getTime());
}
}
static void StateVectorToElements(const Vector3d& position,
const Vector3d& v,
double GM,
OrbitalElements* elements)
{
Vector3d R = position;
Vector3d L = R.cross(v);
double magR = R.norm();
double magL = L.norm();
double magV = v.norm();
L *= (1.0 / magL);
Vector3d W = L.cross(R / magR);
// Compute the semimajor axis
double a = 1.0 / (2.0 / magR - square(magV) / GM);
// Compute the eccentricity
double p = square(magL) / GM;
double q = R.dot(v);
double ex = 1.0 - magR / a;
double ey = q / sqrt(a * GM);
double e = sqrt(ex * ex + ey * ey);
// Compute the mean anomaly
double E = atan2(ey, ex);
double M = E - e * sin(E);
// Compute the inclination
double cosi = L.dot(Vector3d::UnitY());
double i = 0.0;
if (cosi < 1.0)
i = acos(cosi);
// Compute the longitude of ascending node
double Om = atan2(L.x(), L.z());
// Compute the argument of pericenter
Vector3d U = R / magR;
double s_nu = v.dot(U) * sqrt(p / GM);
double c_nu = v.dot(W) * sqrt(p / GM) - 1;
s_nu /= e;
c_nu /= e;
Vector3d P = U * c_nu - W * s_nu;
Vector3d Q = U * s_nu + W * c_nu;
double om = atan2(P.y(), Q.y());
// Compute the period
double T = 2 * PI * sqrt(cube(a) / GM);
elements->semimajorAxis = a;
elements->eccentricity = e;
elements->inclination = i;
elements->longAscendingNode = Om;
elements->argPericenter = om;
elements->meanAnomaly = M;
elements->period = T;
}
static void CalculateOsculatingElements(const Orbit& orbit,
double t,
double dt,
OrbitalElements* elements)
{
double sdt = dt;
// If the trajectory is finite, make sure we sample
// it inside the valid time interval.
if (!orbit.isPeriodic())
{
double beginTime = 0.0;
double endTime = 0.0;
orbit.getValidRange(beginTime, endTime);
clog << "t+dt: " << t + dt << ", endTime: " << endTime << endl;
if (t + dt > endTime)
{
clog << "REVERSE\n";
sdt = -dt;
}
}
Vector3d p0 = orbit.positionAtTime(t);
Vector3d p1 = orbit.positionAtTime(t + sdt);
Vector3d v0 = orbit.velocityAtTime(t);
Vector3d v1 = orbit.velocityAtTime(t + sdt);
double accel = ((v1 - v0) / sdt).norm();
Vector3d r = p0;
double GM = accel * r.squaredNorm();
clog << "vel: " << v0.norm() / 86400.0 << endl;
clog << "vel (est): " << (p1 - p0).norm() / sdt / 86400 << endl;
clog << "osc: " << t << ", " << dt << ", " << accel << ", GM=" << GM << endl;
StateVectorToElements(p0, v0, GM, elements);
}