celestia/src/celestia/qt/qteventfinder.cpp

570 lines
19 KiB
C++

// qteventfinder.cpp
//
// Copyright (C) 2008, Celestia Development Team
// celestia-developers@lists.sourceforge.net
//
// Qt4 interface for the celestial event finder.
//
// 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 <QRadioButton>
#include <QTreeView>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGroupBox>
#include <QDateEdit>
#include <QComboBox>
#include <QPushButton>
#include <QStandardItemModel>
#include <QMessageBox>
#include <QProgressDialog>
#include <QApplication>
#include <QMenu>
#include <vector>
#include <algorithm>
#include <cassert>
#include <celestia/celestiacore.h>
#include <celestia/eclipsefinder.h>
#include <celmath/distance.h>
#include <celmath/intersect.h>
#include <celmath/geomutil.h>
#include <celutil/gettext.h>
#include "qteventfinder.h"
using namespace Eigen;
using namespace std;
using namespace celmath;
// Functions to convert between Qt dates and Celestia dates.
// TODO: Qt's date class doesn't support leap seconds
static double QDateToTDB(const QDate& date)
{
return astro::UTCtoTDB(astro::Date(date.year(), date.month(), date.day()));
}
static QDateTime TDBToQDate(double tdb)
{
astro::Date date = astro::TDBtoUTC(tdb);
int sec = (int) date.seconds;
int msec = (int) ((date.seconds - sec) * 1000);
return QDateTime(QDate(date.year, date.month, date.day),
QTime(date.hour, date.minute, sec, msec),
Qt::UTC);
}
class EventTableModel : public QAbstractTableModel
{
public:
EventTableModel() = default;
virtual ~EventTableModel() = default;
// Methods from QAbstractTableModel
Qt::ItemFlags flags(const QModelIndex& index) const override;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
int rowCount(const QModelIndex& index) const override;
int columnCount(const QModelIndex& index) const override;
void sort(int column, Qt::SortOrder order) override;
void setEclipses(const vector<Eclipse>& _eclipses);
const Eclipse* eclipseAtIndex(const QModelIndex& index) const;
enum
{
ReceiverColumn = 0,
OcculterColumn = 1,
StartTimeColumn = 2,
DurationColumn = 3,
};
private:
vector<Eclipse> eclipses;
};
Qt::ItemFlags EventTableModel::flags(const QModelIndex& /*unused*/) const
{
return Qt::ItemIsSelectable | Qt::ItemIsEnabled;
}
QVariant EventTableModel::data(const QModelIndex& index, int role) const
{
if (index.row() < 0 || index.row() >= (int) eclipses.size())
{
// Out of range
return QVariant();
}
if (role != Qt::DisplayRole)
{
return QVariant();
}
const Eclipse& eclipse = eclipses[index.row()];
switch (index.column())
{
case ReceiverColumn:
return QString(eclipse.receiver->getName(true).c_str());
case OcculterColumn:
return QString(eclipse.occulter->getName(true).c_str());
case StartTimeColumn:
return TDBToQDate(eclipse.startTime).toLocalTime().toString("dd MMM yyyy hh:mm");
case DurationColumn:
{
int minutes = (int) ((eclipse.endTime - eclipse.startTime) * 24 * 60);
return QString("%1:%2").arg(minutes / 60).arg(minutes % 60, 2, 10, QLatin1Char('0'));
}
default:
return QVariant();
}
}
QVariant EventTableModel::headerData(int section, Qt::Orientation /*unused*/, int role) const
{
if (role != Qt::DisplayRole)
return QVariant();
switch (section)
{
case 0:
return _("Eclipsed body");
case 1:
return _("Occulter");
case 2:
return _("Start time");
case 3:
return _("Duration");
default:
return QVariant();
}
}
int EventTableModel::rowCount(const QModelIndex& /*unused*/) const
{
return (int) eclipses.size();
}
int EventTableModel::columnCount(const QModelIndex& /*unused*/) const
{
return 4;
}
void EventTableModel::sort(int column, Qt::SortOrder order)
{
switch (column)
{
case ReceiverColumn:
std::sort(eclipses.begin(), eclipses.end(),
[](const Eclipse& e0, const Eclipse& e1) { return e0.receiver->getName() < e1.receiver->getName(); });
break;
case OcculterColumn:
std::sort(eclipses.begin(), eclipses.end(),
[](const Eclipse& e0, const Eclipse& e1) { return e0.occulter->getName() < e1.occulter->getName(); });
break;
case StartTimeColumn:
std::sort(eclipses.begin(), eclipses.end(),
[](const Eclipse& e0, const Eclipse& e1) { return e0.startTime < e1.startTime; });
break;
case DurationColumn:
std::sort(eclipses.begin(), eclipses.end(),
[](const Eclipse& e0, const Eclipse& e1) { return e0.endTime - e0.startTime < e1.endTime - e1.startTime; });
break;
}
if (order == Qt::DescendingOrder)
reverse(eclipses.begin(), eclipses.end());
dataChanged(index(0, 0), index(eclipses.size() - 1, columnCount(QModelIndex())));
}
void EventTableModel::setEclipses(const vector<Eclipse>& _eclipses)
{
beginResetModel();
eclipses = _eclipses;
endResetModel();
}
const Eclipse* EventTableModel::eclipseAtIndex(const QModelIndex& index) const
{
int row = index.row();
if (row >= 0 && row < (int) eclipses.size())
return &eclipses[row];
else
return nullptr;
}
EventFinder::EventFinder(CelestiaCore* _appCore,
const QString& title,
QWidget* parent) :
QDockWidget(title, parent),
appCore(_appCore)
{
QWidget* finderWidget = new QWidget(this);
QVBoxLayout* layout = new QVBoxLayout();
eventTable = new QTreeView();
eventTable->setRootIsDecorated(false);
eventTable->setAlternatingRowColors(true);
eventTable->setItemsExpandable(false);
eventTable->setUniformRowHeights(true);
eventTable->setSortingEnabled(true);
eventTable->setContextMenuPolicy(Qt::CustomContextMenu);
connect(eventTable, SIGNAL(customContextMenuRequested(const QPoint&)),
this, SLOT(slotContextMenu(const QPoint&)));
layout->addWidget(eventTable);
// Create the eclipse type box
QWidget* eclipseTypeBox = new QWidget();
QVBoxLayout* eclipseTypeLayout = new QVBoxLayout();
solarOnlyButton = new QRadioButton(_("Solar eclipses"));
lunarOnlyButton = new QRadioButton(_("Lunar eclipses"));
allEclipsesButton = new QRadioButton(_("All eclipses"));
eclipseTypeLayout->addWidget(solarOnlyButton);
eclipseTypeLayout->addWidget(lunarOnlyButton);
eclipseTypeLayout->addWidget(allEclipsesButton);
eclipseTypeBox->setLayout(eclipseTypeLayout);
// Search the search range box
QGroupBox* searchRangeBox = new QGroupBox(_("Search range"));
QVBoxLayout* searchRangeLayout = new QVBoxLayout();
startDateEdit = new QDateEdit(searchRangeBox);
endDateEdit = new QDateEdit(searchRangeBox);
startDateEdit->setDisplayFormat("dd MMM yyyy");
endDateEdit->setDisplayFormat("dd MMM yyyy");
searchRangeLayout->addWidget(startDateEdit);
searchRangeLayout->addWidget(endDateEdit);
searchRangeBox->setLayout(searchRangeLayout);
// subgroup for layout
QWidget* subgroup = new QWidget(this);
QHBoxLayout* subLayout = new QHBoxLayout();
subLayout->addWidget(eclipseTypeBox);
subLayout->addWidget(searchRangeBox);
subgroup->setLayout(subLayout);
layout->addWidget(subgroup);
planetSelect = new QComboBox();
planetSelect->addItem(_("Earth"));
planetSelect->addItem(_("Jupiter"));
planetSelect->addItem(_("Saturn"));
planetSelect->addItem(_("Uranus"));
planetSelect->addItem(_("Neptune"));
planetSelect->addItem(_("Pluto"));
layout->addWidget(planetSelect);
QPushButton* findButton = new QPushButton(_("Find eclipses"));
connect(findButton, SIGNAL(clicked()), this, SLOT(slotFindEclipses()));
layout->addWidget(findButton);
finderWidget->setLayout(layout);
// Set default values:
// Two year search range centered on current system date
// Solar eclipses only
QDate now = QDate::currentDate();
startDateEdit->setDate(now.addYears(-1));
endDateEdit->setDate(now.addYears(1));
solarOnlyButton->setChecked(true);
model = new EventTableModel();
eventTable->setModel(model);
this->setWidget(finderWidget);
}
EclipseFinderWatcher::Status EventFinder::eclipseFinderProgressUpdate(double t)
{
if (progress != nullptr)
{
// Avoid processing events at every update, otherwise finding eclipse
// can take a very long time.
//if (t - lastProgressUpdate > searchSpan * 0.01)
if (searchTimer.elapsed() >= 100)
{
searchTimer.start();
progress->setValue((int) t);
QApplication::processEvents();
lastProgressUpdate = t;
}
return progress->wasCanceled() ? AbortOperation : ContinueOperation;
}
return EclipseFinderWatcher::ContinueOperation;
}
void EventFinder::slotFindEclipses()
{
int eclipseTypeMask = Eclipse::Solar;
if (lunarOnlyButton->isChecked())
eclipseTypeMask = Eclipse::Lunar;
else if (allEclipsesButton->isChecked())
eclipseTypeMask = Eclipse::Solar | Eclipse::Lunar;
QString bodyName = QString("Sol/") + planetSelect->currentText();
Selection obj = appCore->getSimulation()->findObjectFromPath(bodyName.toStdString(), true);
if (obj.body() == nullptr)
{
QString msg(_("%1 is not a valid object"));
QMessageBox::critical(this, _("Event Finder"),
msg.arg(planetSelect->currentText()));
return;
}
QDate startDate = startDateEdit->date();
QDate endDate = endDateEdit->date();
if (startDate > endDate)
{
QMessageBox::critical(this, _("Event Finder"),
_("End date is earlier than start date."));
return;
}
EclipseFinder finder(obj.body(), this);
searchTimer.start();
double startTimeTDB = QDateToTDB(startDate);
double endTimeTDB = QDateToTDB(endDate);
// Initialize values used by progress bar
searchSpan = endTimeTDB - startTimeTDB;
lastProgressUpdate = startTimeTDB;
progress = new QProgressDialog(_("Finding eclipses..."), "Abort", (int) startTimeTDB, (int) endTimeTDB, this);
progress->setWindowModality(Qt::WindowModal);
progress->show();
vector<Eclipse> eclipses;
finder.findEclipses(startTimeTDB, endTimeTDB,
eclipseTypeMask,
eclipses);
delete progress;
progress = nullptr;
model->setEclipses(eclipses);
eventTable->resizeColumnToContents(EventTableModel::OcculterColumn);
eventTable->resizeColumnToContents(EventTableModel::ReceiverColumn);
eventTable->resizeColumnToContents(EventTableModel::StartTimeColumn);
}
void EventFinder::slotContextMenu(const QPoint& pos)
{
QModelIndex index = eventTable->indexAt(pos);
activeEclipse = model->eclipseAtIndex(index);
if (activeEclipse != nullptr)
{
if (contextMenu == nullptr)
contextMenu = new QMenu(this);
contextMenu->clear();
QAction* setTimeAction = new QAction(_("Set time to mid-eclipse"), contextMenu);
connect(setTimeAction, SIGNAL(triggered()), this, SLOT(slotSetEclipseTime()));
contextMenu->addAction(setTimeAction);
QAction* viewNearEclipsedAction = new QAction(QString(_("Near %1")).arg(activeEclipse->receiver->getName(true).c_str()), contextMenu);
connect(viewNearEclipsedAction, SIGNAL(triggered()), this, SLOT(slotViewNearEclipsed()));
contextMenu->addAction(viewNearEclipsedAction);
QAction* viewEclipsedSurfaceAction = new QAction(QString(_("From surface of %1")).arg(activeEclipse->receiver->getName(true).c_str()), contextMenu);
connect(viewEclipsedSurfaceAction, SIGNAL(triggered()), this, SLOT(slotViewEclipsedSurface()));
contextMenu->addAction(viewEclipsedSurfaceAction);
QAction* viewOccluderSurfaceAction = new QAction(QString(_("From surface of %1")).arg(activeEclipse->occulter->getName(true).c_str()), contextMenu);
connect(viewOccluderSurfaceAction, SIGNAL(triggered()), this, SLOT(slotViewOccluderSurface()));
contextMenu->addAction(viewOccluderSurfaceAction);
QAction* viewBehindOccluderAction = new QAction(QString(_("Behind %1")).arg(activeEclipse->occulter->getName(true).c_str()), contextMenu);
connect(viewBehindOccluderAction, SIGNAL(triggered()), this, SLOT(slotViewBehindOccluder()));
contextMenu->addAction(viewBehindOccluderAction);
contextMenu->popup(eventTable->mapToGlobal(pos), viewNearEclipsedAction);
}
}
void EventFinder::slotSetEclipseTime()
{
double midEclipseTime = (activeEclipse->startTime + activeEclipse->endTime) / 2.0;
appCore->getSimulation()->setTime(midEclipseTime);
}
// Find the point of maximum eclipse, either the intersection of the eclipsed body with the
// ray from sun to occluder, or the nearest point to that ray if there is no intersection.
// The returned point is relative to the center of the eclipsed body. Note that this function
// assumes that the eclipsed body is spherical.
static Vector3d findMaxEclipsePoint(const Vector3d& toCasterDir,
const Vector3d& toReceiver,
double eclipsedBodyRadius)
{
double distance = 0.0;
bool intersect = testIntersection(Eigen::ParametrizedLine<double, 3>(Vector3d::Zero(), toCasterDir),
Sphered(toReceiver, eclipsedBodyRadius),
distance);
Vector3d maxEclipsePoint;
if (intersect)
{
maxEclipsePoint = (toCasterDir * distance) - toReceiver;
}
else
{
// If the center line doesn't actually intersect the occluder, find the point on the
// eclipsed body nearest to the line.
double t = toReceiver.dot(toCasterDir) / toCasterDir.squaredNorm();
Vector3d closestPoint = t * toCasterDir;
maxEclipsePoint = closestPoint - toReceiver;
maxEclipsePoint *= eclipsedBodyRadius / maxEclipsePoint.norm();
}
return maxEclipsePoint;
}
/*! Move the camera to a point 3 radii from the surface, aimed at the point of maximum eclipse.
*/
void EventFinder::slotViewNearEclipsed()
{
Simulation* sim = appCore->getSimulation();
// Only set the time if the current time isn't within the eclipse span
double now = sim->getTime();
if (now < activeEclipse->startTime || now > activeEclipse->endTime)
slotSetEclipseTime();
now = sim->getTime();
Selection receiver(activeEclipse->receiver);
Selection caster(activeEclipse->occulter);
Selection sun(activeEclipse->receiver->getSystem()->getStar());
Vector3d toCasterDir = caster.getPosition(now).offsetFromKm(sun.getPosition(now));
Vector3d toReceiver = receiver.getPosition(now).offsetFromKm(sun.getPosition(now));
Vector3d maxEclipsePoint = findMaxEclipsePoint(toCasterDir, toReceiver, receiver.radius());
Vector3d up = activeEclipse->receiver->getEclipticToBodyFixed(now).conjugate() * Vector3d::UnitY();
Vector3d viewerPos = maxEclipsePoint * 4.0; // 4 radii from center
Quaterniond viewOrientation = LookAt(viewerPos, maxEclipsePoint, up);
sim->setFrame(ObserverFrame::Ecliptical, receiver);
sim->gotoLocation(UniversalCoord::Zero().offsetKm(viewerPos), viewOrientation, 5.0);
}
/*! Position the camera on the surface of the eclipsed body and pointing directly at the sun.
*/
void EventFinder::slotViewEclipsedSurface()
{
Simulation* sim = appCore->getSimulation();
// Only set the time if the current time isn't within the eclipse span
double now = sim->getTime();
if (now < activeEclipse->startTime || now > activeEclipse->endTime)
slotSetEclipseTime();
now = sim->getTime();
Selection receiver(activeEclipse->receiver);
Selection caster(activeEclipse->occulter);
Selection sun(activeEclipse->receiver->getSystem()->getStar());
Vector3d toCasterDir = caster.getPosition(now).offsetFromKm(sun.getPosition(now));
Vector3d toReceiver = receiver.getPosition(now).offsetFromKm(sun.getPosition(now));
Vector3d maxEclipsePoint = findMaxEclipsePoint(toCasterDir, toReceiver, receiver.radius());
Vector3d up = maxEclipsePoint.normalized();
// TODO: Select alternate up direction when eclipse is directly overhead
Quaterniond viewOrientation = LookAt<double>(maxEclipsePoint, -toReceiver, up);
Vector3d v = maxEclipsePoint * 1.0001;
sim->setFrame(ObserverFrame::Ecliptical, receiver);
sim->gotoLocation(UniversalCoord::Zero().offsetKm(v), viewOrientation, 5.0);
}
/*! Position the camera on the surface of the occluding body, aimed toward the eclipse. */
void EventFinder::slotViewOccluderSurface()
{
Simulation* sim = appCore->getSimulation();
// Only set the time if the current time isn't within the eclipse span
double now = sim->getTime();
if (now < activeEclipse->startTime || now > activeEclipse->endTime)
slotSetEclipseTime();
now = sim->getTime();
Selection receiver(activeEclipse->receiver);
Selection caster(activeEclipse->occulter);
Selection sun(activeEclipse->receiver->getSystem()->getStar());
Vector3d toCasterDir = caster.getPosition(now).offsetFromKm(sun.getPosition(now));
Vector3d up = activeEclipse->receiver->getEclipticToBodyFixed(now).conjugate() * Vector3d::UnitY();
Vector3d toReceiverDir = receiver.getPosition(now).offsetFromKm(sun.getPosition(now));
Vector3d surfacePoint = toCasterDir * caster.radius() / toCasterDir.norm() * 1.0001;
Quaterniond viewOrientation = LookAt<double>(surfacePoint, toReceiverDir, up);
sim->setFrame(ObserverFrame::Ecliptical, caster);
sim->gotoLocation(UniversalCoord::Zero().offsetKm(surfacePoint), viewOrientation, 5.0);
}
/*! Position the camera directly behind the occluding body in the direction
* of the sun.
*/
void EventFinder::slotViewBehindOccluder()
{
Simulation* sim = appCore->getSimulation();
// Only set the time if the current time isn't within the eclipse span
double now = sim->getTime();
if (now < activeEclipse->startTime || now > activeEclipse->endTime)
slotSetEclipseTime();
now = sim->getTime();
Selection receiver(activeEclipse->receiver);
Selection caster(activeEclipse->occulter);
Selection sun(activeEclipse->receiver->getSystem()->getStar());
Vector3d toCasterDir = caster.getPosition(now).offsetFromKm(sun.getPosition(now));
Vector3d up = activeEclipse->receiver->getEclipticToBodyFixed(now).conjugate() * Vector3d::UnitY();
Vector3d toReceiverDir = receiver.getPosition(now).offsetFromKm(sun.getPosition(now));
Vector3d surfacePoint = toCasterDir * caster.radius() / toCasterDir.norm() * 20.0;
Quaterniond viewOrientation = LookAt<double>(surfacePoint, toReceiverDir, up);
sim->setFrame(ObserverFrame::Ecliptical, caster);
sim->gotoLocation(UniversalCoord::Zero().offsetKm(surfacePoint), viewOrientation, 5.0);
}