1
0
Fork 0

Flight planning in the launcher

Still evolving but usable to import or build a route now
This commit is contained in:
James Turner 2018-08-11 22:05:04 +02:00
parent 331939f640
commit df7e13d734
13 changed files with 1396 additions and 8 deletions

View file

@ -157,10 +157,14 @@ if (HAVE_QT)
UnitsModel.hxx
NavaidSearchModel.hxx
NavaidSearchModel.cxx
FlightPlanController.cxx
FlightPlanController.hxx
RouteDiagram.cxx
RouteDiagram.hxx
)
set_property(TARGET fgqmlui PROPERTY AUTOMOC ON)
target_link_libraries(fgqmlui Qt5::Quick Qt5::Network Qt5::Qml SimGearCore)
target_link_libraries(fgqmlui Qt5::Quick Qt5::Widgets Qt5::Network Qt5::Qml SimGearCore)
target_include_directories(fgqmlui PRIVATE ${PROJECT_BINARY_DIR}/src/GUI)
add_dependencies(fgqmlui fgfs_qm_files)

View file

@ -0,0 +1,552 @@
#include "FlightPlanController.hxx"
#include <QDebug>
#include <QAbstractListModel>
#include <QQmlEngine>
#include <QFileDialog>
#include <QTimer>
#include <simgear/misc/sg_path.hxx>
#include <Main/globals.hxx>
#include <Navaids/waypoint.hxx>
#include <Navaids/airways.hxx>
#include <Navaids/navrecord.hxx>
#include <Navaids/airways.hxx>
#include "QmlPositioned.hxx"
#include "LaunchConfig.hxx"
using namespace flightgear;
const int LegDistanceRole = Qt::UserRole;
const int LegTrackRole = Qt::UserRole + 1;
const int LegTerminatorNavRole = Qt::UserRole + 2;
const int LegAirwayIdentRole = Qt::UserRole + 3;
const int LegTerminatorTypeRole = Qt::UserRole + 4;
const int LegTerminatorNavNameRole = Qt::UserRole + 5;
const int LegTerminatorNavFrequencyRole = Qt::UserRole + 6;
class LegsModel : public QAbstractListModel
{
Q_OBJECT
public:
void setFlightPlan(flightgear::FlightPlanRef f)
{
beginResetModel();
_fp = f;
endResetModel();
}
int rowCount(const QModelIndex &parent) const override
{
Q_UNUSED(parent)
return _fp->numLegs();
}
QVariant data(const QModelIndex &index, int role) const override
{
const auto leg = _fp->legAtIndex(index.row());
if (!leg)
return {};
switch (role) {
case Qt::DisplayRole:
return QString::fromStdString(leg->waypoint()->ident());
case LegDistanceRole:
return QVariant::fromValue(QuantityValue{Units::NauticalMiles, leg->distanceNm()});
case LegTrackRole:
return QVariant::fromValue(QuantityValue{Units::DegreesTrue, leg->courseDeg()});
case LegAirwayIdentRole:
{
const auto wp = leg->waypoint();
if (wp->type() == "via") {
auto via = static_cast<flightgear::Via*>(leg->waypoint());
return QString::fromStdString(via->airway());
}
if (wp->flag(WPT_VIA)) {
AirwayRef awy = static_cast<Airway*>(wp->owner());
return QString::fromStdString(awy->ident());
}
break;
}
case LegTerminatorNavRole:
{
if (leg->waypoint()->source()) {
return QString::fromStdString(leg->waypoint()->source()->ident());
}
break;
}
case LegTerminatorNavFrequencyRole:
{
const auto n = fgpositioned_cast<FGNavRecord>(leg->waypoint()->source());
if (n) {
const double f = n->get_freq() / 100.0;
if (n->type() == FGPositioned::NDB) {
return QVariant::fromValue(QuantityValue(Units::FreqKHz, f));
}
return QVariant::fromValue(QuantityValue(Units::FreqMHz, f));
}
return QVariant::fromValue(QuantityValue());
}
case LegTerminatorNavNameRole:
{
if (leg->waypoint()->source()) {
return QString::fromStdString(leg->waypoint()->source()->name());
}
break;
}
case LegTerminatorTypeRole:
return QString::fromStdString(leg->waypoint()->type());
default:
break;
}
return {};
}
void waypointsChanged()
{
beginResetModel();
endResetModel();
}
QHash<int, QByteArray> roleNames() const override
{
QHash<int, QByteArray> result = QAbstractListModel::roleNames();
result[Qt::DisplayRole] = "label";
result[LegDistanceRole] = "distance";
result[LegTrackRole] = "track";
result[LegTerminatorNavRole] = "to";
result[LegTerminatorNavFrequencyRole] = "frequency";
result[LegAirwayIdentRole] = "via";
result[LegTerminatorTypeRole] = "wpType";
result[LegTerminatorNavNameRole] = "toName";
return result;
}
private:
flightgear::FlightPlanRef _fp;
};
/////////////////////////////////////////////////////////////////////////////
class FPDelegate : public FlightPlan::Delegate
{
public:
void arrivalChanged() override
{
p->infoChanged();
}
void departureChanged() override
{
p->infoChanged();
}
void cruiseChanged() override
{
p->infoChanged();
}
void waypointsChanged() override
{
QTimer::singleShot(0, p->_legs, &LegsModel::waypointsChanged);
p->waypointsChanged();
}
FlightPlanController* p;
};
/////////////////////////////////////////////////////////////////////////////
FlightPlanController::FlightPlanController(QObject *parent, LaunchConfig* config)
: QObject(parent)
{
_config = config;
connect(_config, &LaunchConfig::collect, this, &FlightPlanController::onCollectConfig);
connect(_config, &LaunchConfig::save, this, &FlightPlanController::onSave);
connect(_config, &LaunchConfig::restore, this, &FlightPlanController::onRestore);
_delegate.reset(new FPDelegate);
_delegate->p = this; // link back to us
qmlRegisterUncreatableType<LegsModel>("FlightGear", 1, 0, "LegsModel", "singleton");
_fp.reset(new flightgear::FlightPlan);
_fp->addDelegate(_delegate.get());
_legs = new LegsModel();
_legs->setFlightPlan(_fp);
// initial restore
onRestore();
}
FlightPlanController::~FlightPlanController()
{
_fp->removeDelegate(_delegate.get());
}
void FlightPlanController::clearPlan()
{
auto fp = new flightgear::FlightPlan;
_fp->removeDelegate(_delegate.get());
_fp = fp;
_fp->addDelegate(_delegate.get());
_legs->setFlightPlan(fp);
emit infoChanged();
}
bool FlightPlanController::loadFromPath(QString path)
{
auto fp = new flightgear::FlightPlan;
bool ok = fp->load(SGPath(path.toUtf8().data()));
if (!ok) {
qWarning() << "Failed to load flightplan " << path;
return false;
}
_fp->removeDelegate(_delegate.get());
_fp = fp;
_fp->addDelegate(_delegate.get());
_legs->setFlightPlan(fp);
// notify that everything changed
emit infoChanged();
return true;
}
bool FlightPlanController::saveToPath(QString path) const
{
SGPath p(path.toUtf8().data());
return _fp->save(p);
}
void FlightPlanController::onCollectConfig()
{
SGPath p = globals->get_fg_home() / "launcher.fgfp";
_fp->save(p);
_config->setArg("flight-plan", p.utf8Str());
}
void FlightPlanController::onSave()
{
std::ostringstream ss;
_fp->save(ss);
_config->setValueForKey("", "fp", QString::fromStdString(ss.str()));
}
void FlightPlanController::onRestore()
{
std::string planXML = _config->getValueForKey("", "fp", QString()).toString().toStdString();
if (!planXML.empty()) {
std::istringstream ss(planXML);
_fp->load(ss);
emit infoChanged();
}
}
QuantityValue FlightPlanController::cruiseAltitude() const
{
if (_fp->cruiseFlightLevel() > 0)
return {Units::FlightLevel, _fp->cruiseFlightLevel()};
return {Units::FeetMSL, _fp->cruiseAltitudeFt()};
}
void FlightPlanController::setCruiseAltitude(QuantityValue alt)
{
const int ival = static_cast<int>(alt.value);
if (alt.unit == Units::FlightLevel) {
if (_fp->cruiseFlightLevel() == ival) {
return;
}
_fp->setCruiseFlightLevel(ival);
} else if (alt.unit == Units::FeetMSL) {
if (_fp->cruiseAltitudeFt() == ival) {
return;
}
_fp->setCruiseAltitudeFt(ival);
}
emit infoChanged();
}
QmlPositioned *FlightPlanController::departure() const
{
if (!_fp->departureAirport())
return new QmlPositioned;
return new QmlPositioned(_fp->departureAirport());
}
QmlPositioned *FlightPlanController::destination() const
{
if (!_fp->destinationAirport())
return new QmlPositioned;
return new QmlPositioned(_fp->destinationAirport());
}
QmlPositioned *FlightPlanController::alternate() const
{
if (!_fp->alternate())
return new QmlPositioned;
return new QmlPositioned(_fp->alternate());
}
QuantityValue FlightPlanController::cruiseSpeed() const
{
if (_fp->cruiseSpeedMach() > 0.0) {
return {Units::Mach, _fp->cruiseSpeedMach()};
}
return {Units::Knots, _fp->cruiseSpeedKnots()};
}
FlightPlanController::FlightRules FlightPlanController::flightRules() const
{
return static_cast<FlightRules>(_fp->flightRules());
}
FlightPlanController::FlightType FlightPlanController::flightType() const
{
return static_cast<FlightType>(_fp->flightType());
}
void FlightPlanController::setFlightRules(FlightRules r)
{
_fp->setFlightRules(static_cast<flightgear::ICAOFlightRules>(r));
}
void FlightPlanController::setFlightType(FlightType ty)
{
_fp->setFlightType(static_cast<flightgear::ICAOFlightType>(ty));
}
QString FlightPlanController::callsign() const
{
return QString::fromStdString(_fp->callsign());
}
QString FlightPlanController::remarks() const
{
return QString::fromStdString(_fp->remarks());
}
QString FlightPlanController::aircraftType() const
{
return QString::fromStdString(_fp->icaoAircraftType());
}
void FlightPlanController::setCallsign(QString s)
{
const auto stdS = s.toStdString();
if (_fp->callsign() == stdS)
return;
_fp->setCallsign(stdS);
emit infoChanged();
}
void FlightPlanController::setRemarks(QString r)
{
const auto stdR = r.toStdString();
if (_fp->remarks() == stdR)
return;
_fp->setRemarks(stdR);
emit infoChanged();
}
void FlightPlanController::setAircraftType(QString ty)
{
const auto stdT = ty.toStdString();
if (_fp->icaoAircraftType() == stdT)
return;
_fp->setIcaoAircraftType(stdT);
emit infoChanged();
}
int FlightPlanController::estimatedDurationMinutes() const
{
return _fp->estimatedDurationMinutes();
}
QuantityValue FlightPlanController::totalDistanceNm() const
{
return QuantityValue{Units::NauticalMiles, _fp->totalDistanceNm()};
}
bool FlightPlanController::tryParseRoute(QString routeDesc)
{
bool ok = _fp->parseICAORouteString(routeDesc.toStdString());
return ok;
}
bool FlightPlanController::tryGenerateRoute()
{
if (!_fp->departureAirport() || !_fp->destinationAirport()) {
qWarning() << "departure or destination not set";
return false;
}
auto net = Airway::highLevel();
auto fromNode = net->findClosestNode(_fp->departureAirport()->geod());
auto toNode = net->findClosestNode(_fp->destinationAirport()->geod());
if (!fromNode.first) {
qWarning() << "Couldn't find airway network transition for "
<< QString::fromStdString(_fp->departureAirport()->ident());
return false;
}
if (!toNode.first) {
qWarning() << "Couldn't find airway network transition for "
<< QString::fromStdString(_fp->destinationAirport()->ident());
return false;
}
WayptRef fromWp = new NavaidWaypoint(fromNode.first, _fp);
WayptRef toWp = new NavaidWaypoint(toNode.first, _fp);
WayptVec path;
bool ok = net->route(fromWp, toWp, path);
if (!ok) {
qWarning() << "unable to find a route";
return false;
}
_fp->clear();
_fp->insertWayptAtIndex(fromWp, -1);
_fp->insertWayptsAtIndex(path, -1);
_fp->insertWayptAtIndex(toWp, -1);
return true;
}
void FlightPlanController::clearRoute()
{
_fp->clear();
}
QString FlightPlanController::icaoRoute() const
{
return QString::fromStdString(_fp->asICAORouteString());
}
void FlightPlanController::setEstimatedDurationMinutes(int mins)
{
if (_fp->estimatedDurationMinutes() == mins)
return;
_fp->setEstimatedDurationMinutes(mins);
emit infoChanged();
}
void FlightPlanController::computeDuration()
{
_fp->computeDurationMinutes();
emit infoChanged();
}
bool FlightPlanController::loadPlan()
{
QString file = QFileDialog::getOpenFileName(nullptr, tr("Load a flight-plan"),
{}, "*.fgfp");
if (file.isEmpty())
return false;
return loadFromPath(file);
}
void FlightPlanController::savePlan()
{
QString file = QFileDialog::getSaveFileName(nullptr, tr("Save flight-plan"),
{}, "*.fgfp");
if (file.isEmpty())
return;
if (!file.endsWith(".fgfp")) {
file += ".fgfp";
}
saveToPath(file);
}
void FlightPlanController::setDeparture(QmlPositioned *apt)
{
if (!apt) {
_fp->clearDeparture();
} else {
if (apt->inner() == _fp->departureAirport())
return;
_fp->setDeparture(fgpositioned_cast<FGAirport>(apt->inner()));
}
emit infoChanged();
}
void FlightPlanController::setDestination(QmlPositioned *apt)
{
if (apt) {
if (apt->inner() == _fp->destinationAirport())
return;
_fp->setDestination(fgpositioned_cast<FGAirport>(apt->inner()));
} else {
_fp->clearDestination();
}
emit infoChanged();
}
void FlightPlanController::setAlternate(QmlPositioned *apt)
{
if (apt) {
if (apt->inner() == _fp->alternate())
return;
_fp->setAlternate(fgpositioned_cast<FGAirport>(apt->inner()));
} else {
_fp->setAlternate(nullptr);
}
emit infoChanged();
}
void FlightPlanController::setCruiseSpeed(QuantityValue speed)
{
qInfo() << Q_FUNC_INFO << speed.unit << speed.value;
if (speed.unit == Units::Mach) {
if (speed == QuantityValue(Units::Mach, _fp->cruiseSpeedMach())) {
return;
}
_fp->setCruiseSpeedMach(speed.value);
} else if (speed.unit == Units::Knots) {
const int knotsVal = static_cast<int>(speed.value);
if (_fp->cruiseSpeedKnots() == knotsVal) {
return;
}
_fp->setCruiseSpeedKnots(knotsVal);
}
emit infoChanged();
}
#include "FlightPlanController.moc"

View file

@ -0,0 +1,148 @@
#ifndef FLIGHTPLANCONTROLLER_HXX
#define FLIGHTPLANCONTROLLER_HXX
#include <memory>
#include <QObject>
#include <Navaids/FlightPlan.hxx>
#include "UnitsModel.hxx"
class QmlPositioned;
class LegsModel;
class FPDelegate;
class LaunchConfig;
class FlightPlanController : public QObject
{
Q_OBJECT
Q_PROPERTY(QString callsign READ callsign WRITE setCallsign NOTIFY infoChanged)
Q_PROPERTY(QString remarks READ remarks WRITE setRemarks NOTIFY infoChanged)
Q_PROPERTY(QString aircraftType READ aircraftType WRITE setAircraftType NOTIFY infoChanged)
Q_PROPERTY(LegsModel* legs READ legs CONSTANT)
Q_PROPERTY(QString icaoRoute READ icaoRoute NOTIFY waypointsChanged)
Q_ENUMS(FlightRules)
Q_ENUMS(FlightType)
Q_PROPERTY(FlightRules flightRules READ flightRules WRITE setFlightRules NOTIFY infoChanged)
Q_PROPERTY(FlightType flightType READ flightType WRITE setFlightType NOTIFY infoChanged)
// planned departure date + time
Q_PROPERTY(QuantityValue totalDistanceNm READ totalDistanceNm NOTIFY infoChanged)
Q_PROPERTY(int estimatedDurationMinutes READ estimatedDurationMinutes WRITE setEstimatedDurationMinutes NOTIFY infoChanged)
Q_PROPERTY(QuantityValue cruiseAltitude READ cruiseAltitude WRITE setCruiseAltitude NOTIFY infoChanged)
Q_PROPERTY(QuantityValue cruiseSpeed READ cruiseSpeed WRITE setCruiseSpeed NOTIFY infoChanged)
Q_PROPERTY(QmlPositioned* departure READ departure WRITE setDeparture NOTIFY infoChanged)
Q_PROPERTY(QmlPositioned* destination READ destination WRITE setDestination NOTIFY infoChanged)
Q_PROPERTY(QmlPositioned* alternate READ alternate WRITE setAlternate NOTIFY infoChanged)
// equipment
public:
virtual ~FlightPlanController();
// alias these enums to QML
enum FlightRules
{
VFR = 0,
IFR,
IFR_VFR,
VFR_IFR
};
enum FlightType
{
Scheduled = 0,
NonScheduled,
GeneralAviation,
Military,
Other
};
explicit FlightPlanController(QObject *parent,
LaunchConfig* config);
bool loadFromPath(QString path);
bool saveToPath(QString path) const;
QuantityValue cruiseAltitude() const;
void setCruiseAltitude(QuantityValue alt);
QmlPositioned* departure() const;
QmlPositioned* destination() const;
QmlPositioned* alternate() const;
QuantityValue cruiseSpeed() const;
FlightRules flightRules() const;
FlightType flightType() const;
QString callsign() const;
QString remarks() const;
QString aircraftType() const;
int estimatedDurationMinutes() const;
QuantityValue totalDistanceNm() const;
Q_INVOKABLE bool tryParseRoute(QString routeDesc);
Q_INVOKABLE bool tryGenerateRoute();
Q_INVOKABLE void clearRoute();
LegsModel* legs() const
{ return _legs; }
QString icaoRoute() const;
flightgear::FlightPlanRef flightplan() const
{ return _fp; }
Q_INVOKABLE bool loadPlan();
signals:
void infoChanged();
void waypointsChanged();
public slots:
void setFlightType(FlightType ty);
void setFlightRules(FlightRules r);
void setCallsign(QString s);
void setRemarks(QString r);
void setAircraftType(QString ty);
void setDeparture(QmlPositioned* destinationAirport);
void setDestination(QmlPositioned* destinationAirport);
void setAlternate(QmlPositioned* apt);
void setCruiseSpeed(QuantityValue cruiseSpeed);
void setEstimatedDurationMinutes(int mins);
void computeDuration();
void clearPlan();
void savePlan();
private slots:
void onCollectConfig();
void onSave();
void onRestore();
private:
friend class FPDelegate;
flightgear::FlightPlanRef _fp;
LegsModel* _legs = nullptr;
std::unique_ptr<FPDelegate> _delegate;
LaunchConfig* _config = nullptr;
};
#endif // FLIGHTPLANCONTROLLER_HXX

View file

@ -46,9 +46,11 @@
#include "PixmapImageItem.hxx"
#include "AirportDiagram.hxx"
#include "NavaidDiagram.hxx"
#include "RouteDiagram.hxx"
#include "QmlRadioButtonHelper.hxx"
#include "UnitsModel.hxx"
#include "NavaidSearchModel.hxx"
#include "FlightPlanController.hxx"
using namespace simgear::pkg;
@ -66,6 +68,8 @@ LauncherController::LauncherController(QObject *parent, QWindow* window) :
connect(m_config, &LaunchConfig::save, this, &LauncherController::saveAircraft);
connect(m_config, &LaunchConfig::restore, this, &LauncherController::restoreAircraft);
m_flightPlan = new FlightPlanController(this, m_config);
m_location->setLaunchConfig(m_config);
connect(m_location, &LocationController::descriptionChanged,
this, &LauncherController::summaryChanged);
@ -126,6 +130,7 @@ void LauncherController::initQML()
{
qmlRegisterUncreatableType<LauncherController>("FlightGear.Launcher", 1, 0, "LauncherController", "no");
qmlRegisterUncreatableType<LocationController>("FlightGear.Launcher", 1, 0, "LocationController", "no");
qmlRegisterUncreatableType<FlightPlanController>("FlightGear.Launcher", 1, 0, "FlightPlanController", "no");
qmlRegisterType<LauncherArgumentTokenizer>("FlightGear.Launcher", 1, 0, "ArgumentTokenizer");
qmlRegisterUncreatableType<QAbstractItemModel>("FlightGear.Launcher", 1, 0, "QAIM", "no");
@ -156,6 +161,7 @@ void LauncherController::initQML()
qmlRegisterType<PixmapImageItem>("FlightGear", 1, 0, "PixmapImage");
qmlRegisterType<AirportDiagram>("FlightGear", 1, 0, "AirportDiagram");
qmlRegisterType<NavaidDiagram>("FlightGear", 1, 0, "NavaidDiagram");
qmlRegisterType<RouteDiagram>("FlightGear", 1, 0, "RouteDiagram");
qmlRegisterType<QmlRadioButtonGroup>("FlightGear", 1, 0, "RadioButtonGroup");
qmlRegisterSingletonType(QUrl("qrc:///qml/OverlayShared.qml"), "FlightGear", 1, 0, "OverlayShared");

View file

@ -41,6 +41,7 @@ class AircraftItemModel;
class QQuickItem;
class LaunchConfig;
class LocationController;
class FlightPlanController;
class LauncherController : public QObject
{
@ -54,6 +55,7 @@ class LauncherController : public QObject
Q_PROPERTY(AircraftItemModel* baseAircraftModel MEMBER m_aircraftModel CONSTANT)
Q_PROPERTY(LocationController* location MEMBER m_location CONSTANT)
Q_PROPERTY(FlightPlanController* flightPlan MEMBER m_flightPlan CONSTANT)
Q_PROPERTY(MPServersModel* mpServersModel MEMBER m_serversModel CONSTANT)
@ -246,6 +248,7 @@ private:
AircraftProxyModel* m_aircraftWithUpdatesModel;
MPServersModel* m_serversModel = nullptr;
LocationController* m_location = nullptr;
FlightPlanController* m_flightPlan = nullptr;
QUrl m_selectedAircraft;
QString m_aircraftState;

141
src/GUI/RouteDiagram.cxx Normal file
View file

@ -0,0 +1,141 @@
// RouteDiagram.cxx - GUI diagram of a route
//
// Written by James Turner, started August 2018.
//
// Copyright (C) 2018 James Turner <james@flightgear.org>
//
// 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, Fifth Floor, Boston, MA 02110-1301, USA.
#include "RouteDiagram.hxx"
#include <QPainter>
#include <QDebug>
#include <QVector2D>
#include <QMouseEvent>
#include <Navaids/NavDataCache.hxx>
#include "FlightPlanController.hxx"
using namespace flightgear;
RouteDiagram::RouteDiagram(QQuickItem* pr) :
BaseDiagram(pr)
{
}
void RouteDiagram::setFlightplan(FlightPlanController *fp)
{
if (fp == m_flightplan)
return;
if (m_flightplan) {
// disconnect from old signal
disconnect(m_flightplan, nullptr, this, nullptr);
}
m_flightplan = fp;
emit flightplanChanged(fp);
if (fp) {
connect(fp, &FlightPlanController::infoChanged, this, &RouteDiagram::fpChanged);
connect(fp, &FlightPlanController::waypointsChanged, this, &RouteDiagram::fpChanged);
}
fpChanged();
update();
}
int RouteDiagram::numLegs() const
{
if (!m_flightplan)
return 0;
FlightPlanRef fp = m_flightplan->flightplan();
if (!fp)
return 0;
return fp->numLegs();
}
void RouteDiagram::setActiveLegIndex(int activeLegIndex)
{
if (m_activeLegIndex == activeLegIndex)
return;
m_activeLegIndex = activeLegIndex;
emit legIndexChanged(m_activeLegIndex);
const double halfLegDistance = m_path->distanceForIndex(m_activeLegIndex) * 0.5;
m_projectionCenter = m_path->positionForDistanceFrom(m_activeLegIndex, halfLegDistance);
recomputeBounds(true);
update();
}
void RouteDiagram::paintContents(QPainter *painter)
{
if (!m_flightplan)
return;
FlightPlanRef fp = m_flightplan->flightplan();
QVector<QLineF> lines;
QVector<QLineF> activeLines;
for (int l=0; l < fp->numLegs(); ++l) {
QPointF previous;
bool isFirst = true;
for (auto g : m_path->pathForIndex(l)) {
QPointF p = project(g);
if (isFirst) {
isFirst = false;
} else if (l == m_activeLegIndex) {
activeLines.append(QLineF(previous, p));
} else {
lines.append(QLineF(previous, p));
}
previous = p;
}
}
QPen linePen(Qt::magenta, 2);
linePen.setCosmetic(true);
painter->setPen(linePen);
painter->drawLines(lines);
linePen.setColor(Qt::yellow);
painter->setPen(linePen);
painter->drawLines(activeLines);
}
void RouteDiagram::doComputeBounds()
{
FlightPlanRef fp = m_flightplan->flightplan();
const SGGeodVec gv(m_path->pathForIndex(m_activeLegIndex));
std::for_each(gv.begin(), gv.end(), [this](const SGGeod& g)
{this->extendBounds(this->project(g)); }
);
}
void RouteDiagram::fpChanged()
{
FlightPlanRef fp = m_flightplan->flightplan();
m_path.reset(new RoutePath(fp));
if (fp) {
const double halfLegDistance = m_path->distanceForIndex(m_activeLegIndex) * 0.5;
m_projectionCenter = m_path->positionForDistanceFrom(m_activeLegIndex, halfLegDistance);
}
recomputeBounds(true);
update();
}

81
src/GUI/RouteDiagram.hxx Normal file
View file

@ -0,0 +1,81 @@
// RouteDiagram.hxx - show a route graphically
//
// Written by James Turner, started August 2018.
//
// Copyright (C) 2018 James Turner <james@flightgear.org>
//
// 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, Fifth Floor, Boston, MA 02110-1301, USA.
#ifndef GUI_ROUTE_DIAGRAM_HXX
#define GUI_ROUTE_DIAGRAM_HXX
#include "BaseDiagram.hxx"
#include "QmlPositioned.hxx"
#include "UnitsModel.hxx"
#include <Navaids/navrecord.hxx>
#include <Navaids/routePath.hxx>
#include <simgear/math/sg_geodesy.hxx>
class FlightPlanController;
class RouteDiagram : public BaseDiagram
{
Q_OBJECT
Q_PROPERTY(FlightPlanController* flightplan READ flightplan WRITE setFlightplan NOTIFY flightplanChanged)
Q_PROPERTY(int activeLegIndex READ activeLegIndex WRITE setActiveLegIndex NOTIFY legIndexChanged)
Q_PROPERTY(int numLegs READ numLegs NOTIFY flightplanChanged)
public:
RouteDiagram(QQuickItem* pr = nullptr);
FlightPlanController* flightplan() const
{
return m_flightplan;
}
void setFlightplan(FlightPlanController* fp);
int numLegs() const;
int activeLegIndex() const
{
return m_activeLegIndex;
}
public slots:
void setActiveLegIndex(int activeLegIndex);
signals:
void flightplanChanged(FlightPlanController* flightplan);
void legIndexChanged(int activeLegIndex);
protected:
void paintContents(QPainter *) override;
void doComputeBounds() override;
private:
void fpChanged();
FlightPlanController* m_flightplan = nullptr;
std::unique_ptr<RoutePath> m_path;
int m_activeLegIndex = 0;
};
#endif // of GUI_ROUTE_DIAGRAM_HXX

View file

@ -10,15 +10,58 @@ Item {
height: parent.height
width: parent.width - scrollbar.width
flickableDirection: Flickable.VerticalFlick
contentHeight: contents.childrenRect.height
contentHeight: contents.childrenRect.height + Style.margin * 2
Component.onCompleted: {
if (_launcher.flightPlan.cruiseSpeed.value === 0.0) {
_launcher.flightPlan.cruiseSpeed = _launcher.selectedAircraftInfo.cruiseSpeed
}
if (_launcher.flightPlan.cruiseAltitude.value === 0.0) {
_launcher.flightPlan.cruiseAltitude = _launcher.selectedAircraftInfo.cruiseAltitude
}
_launcher.flightPlan.aircraftType = _launcher.selectedAircraftInfo.icaoType
route.text = _launcher.flightPlan.icaoRoute
}
Column
{
id: contents
width: parent.width - (Style.margin * 2)
x: Style.margin
y: Style.margin
spacing: Style.margin
Row {
width: parent.width
spacing: Style.margin
height: childrenRect.height
Button {
text: qsTr("Load");
onClicked: {
var ok = _launcher.flightPlan.loadPlan();
if (ok) {
route.text = _launcher.flightPlan.icaoRoute;
}
}
}
Button {
text: qsTr("Save");
onClicked: _launcher.flightPlan.savePlan();
}
Button {
text: qsTr("Clear");
onClicked: {
_launcher.flightPlan.clearPlan();
route.text = "";
}
}
}
HeaderBox {
title: qsTr("Aircraft & flight information")
width: parent.width
@ -35,13 +78,17 @@ Item {
text: qsTr("Callsign / Flight No.")
anchors.verticalCenter: parent.verticalCenter
}
LineEdit {
// Aircraft identication - callsign (share with MP)
LineEdit {
id: aircraftIdent
placeholder: "D-FGFS"
suggestedWidthString: "XXXXXX";
anchors.verticalCenter: parent.verticalCenter
text: _launcher.flightPlan.callsign
onTextChanged: {
_launcher.flightPlan.callsign = text
}
}
Item { width: Style.strutSize; height: 1 }
@ -54,6 +101,11 @@ Item {
placeholder: "B738"
suggestedWidthString: "XXXX";
anchors.verticalCenter: parent.verticalCenter
text: _launcher.flightPlan.aircraftType
onTextChanged: {
_launcher.flightPlan.aircraftType = text
}
}
}
@ -66,6 +118,14 @@ Item {
id: flightRules
label: qsTr("Flight rules:")
model: ["VFR", "IFR"] // initially IFR (Y), initially VFR (Z)
Component.onCompleted: {
select(_launcher.flightPlan.flightRules);
}
onCurrentIndexChanged: {
_launcher.flightPlan.flightRules = currentIndex;
}
}
Item { width: Style.strutSize; height: 1 }
@ -78,6 +138,14 @@ Item {
qsTr("General aviation"),
qsTr("Military"),
qsTr("Other")]
Component.onCompleted: {
select(_launcher.flightPlan.flightType);
}
onCurrentIndexChanged: {
_launcher.flightPlan.flightType = currentIndex;
}
}
}
@ -111,6 +179,22 @@ Item {
AirportEntry {
label: qsTr("Departure airport:")
Component.onCompleted: {
selectAirport(_launcher.flightPlan.departure.guid)
}
onPickAirport: {
selectAirport(guid)
_launcher.flightPlan.departure = airport
}
onClickedName: {
detailLoader.airportGuid = airport.guid
detailLoader.sourceComponent = airportDetails;
}
KeyNavigation.tab: departureTime
}
// padding
@ -132,14 +216,23 @@ Item {
NumericalEdit {
label: qsTr("Cruise speed:")
unitsMode: Units.Speed
quantity: _launcher.flightPlan.cruiseSpeed
onCommit: {
_launcher.flightPlan.cruiseSpeed = newValue
}
KeyNavigation.tab: cruiseAltitude
}
// padding
Item { width: Style.strutSize; height: 1 }
NumericalEdit {
id: cruiseAltitude
label: qsTr("Cruise altitude:")
unitsMode: Units.AltitudeIncludingMeters
quantity: _launcher.flightPlan.cruiseAltitude
onCommit: _launcher.flightPlan.cruiseAltitude = newValue
}
}
@ -151,7 +244,69 @@ Item {
PlainTextEditBox {
id: route
width: parent.width
enabled: _launcher.flightPlan.departure.valid && _launcher.flightPlan.destination.valid
onEditingFinished: {
var ok = _launcher.flightPlan.tryParseRoute(text);
}
}
Row {
height: generateRouteButton.height
width: parent.width
spacing: Style.margin
Button {
id: generateRouteButton
text: qsTr("Generate route")
enabled: route.enabled
onClicked: {
var ok = _launcher.flightPlan.tryGenerateRoute();
if (ok) {
route.text = _launcher.flightPlan.icaoRoute;
}
}
anchors.verticalCenter: parent.verticalCenter
}
PopupChoice {
id: routeNetwork
label: qsTr("Using")
model: [qsTr("High-level (Jet) airways"),
qsTr("Low-level (Victor) airways"),
qsTr("High- & low-level airways")]
anchors.verticalCenter: parent.verticalCenter
}
Button {
text: qsTr("View route")
onClicked: {
detailLoader.airportGuid = 0
detailLoader.sourceComponent = routeDetails;
}
anchors.verticalCenter: parent.verticalCenter
}
Button {
text: qsTr("Clear route")
onClicked: {
_launcher.flightPlan.clearRoute();
route.text = "";
}
anchors.verticalCenter: parent.verticalCenter
}
}
RouteLegsView
{
id: legsView
width: parent.width
onClickedLeg: {
detailLoader.airportGuid = 0
detailLoader.legIndex = index
detailLoader.sourceComponent = routeDetails;
}
}
Row {
@ -162,6 +317,20 @@ Item {
AirportEntry {
id: destinationICAO
label: qsTr("Destination airport:")
Component.onCompleted: {
selectAirport(_launcher.flightPlan.destination.guid)
}
onPickAirport: {
selectAirport(guid)
_launcher.flightPlan.destination = airport
}
onClickedName: {
detailLoader.airportGuid = airport.guid
detailLoader.sourceComponent = airportDetails;
}
}
Item { width: Style.strutSize; height: 1 }
@ -169,15 +338,47 @@ Item {
TimeEdit {
id: enrouteEstimate
label: qsTr("Estimated enroute time:")
Component.onCompleted: {
setDurationMinutes(_launcher.flightPlan.estimatedDurationMinutes)
}
onValueChanged: {
_launcher.flightPlan.estimatedDurationMinutes = value.getHours() * 60 + value.getMinutes();
}
}
Item { width: Style.strutSize; height: 1 }
StyledText
{
text: qsTr("Total distance: %1").arg(_launcher.flightPlan.totalDistanceNm);
}
}
Row {
height: childrenRect.height
width: parent.width
spacing: Style.margin
AirportEntry {
id: alternate1
label: qsTr("Alternate airport:")
Component.onCompleted: {
selectAirport(_launcher.flightPlan.alternate.guid)
}
onPickAirport: {
selectAirport(guid)
_launcher.flightPlan.alternate = airport
}
onClickedName: {
detailLoader.airportGuid = airport.guid
detailLoader.sourceComponent = airportDetails;
}
}
}
HeaderBox {
@ -193,10 +394,12 @@ Item {
PlainTextEditBox {
id: remarks
width: parent.width
text: _launcher.flightPlan.remarks
onEditingFinished: {
_launcher.flightPlan.remarks = text;
}
}
// speak to Act-pie guy about passing all this over MP props?
} // of main column
} // of flickable
@ -208,4 +411,50 @@ Item {
flickable: flick
visible: flick.contentHeight > flick.height
}
Component {
id: airportDetails
PlanAirportView {
id: airportView
}
}
Component {
id: routeDetails
PlanRouteDetails {
id: routeView
}
}
Loader {
id: detailLoader
anchors.fill: parent
visible: sourceComponent != null
property var airportGuid
property int legIndex
onStatusChanged: {
if (status == Loader.Ready) {
if (item.hasOwnProperty("location")) {
item.location = airportGuid
}
if (item.hasOwnProperty("legIndex")) {
item.legIndex = legIndex
}
}
}
}
Button {
id: backButton
anchors { left: parent.left; top: parent.top; margins: Style.margin }
width: Style.strutSize
visible: detailLoader.visible
text: "< Back"
onClicked: {
detailLoader.sourceComponent = null
}
}
}

View file

@ -149,7 +149,7 @@ Item {
MenuItem { text:qsTr("Save configuration as..."); shortcut: "Ctrl+S";
onTriggered: _launcher.saveConfigAs(); },
MenuDivider {},
MenuItem { text:qsTr("Flight-planning"); onTriggered: root.enterFlightPlan(); shortcut: "Ctrl+P"; enabled: false},
MenuItem { text:qsTr("Flight-planning"); onTriggered: root.enterFlightPlan(); shortcut: "Ctrl+P"; enabled: true},
MenuDivider {},
MenuItem { text:qsTr("View command line"); onTriggered: _launcher.viewCommandLine(); shortcut: "Ctrl+L"},
MenuItem { text:qsTr("Select data files location..."); onTriggered: _launcher.requestChangeDataPath(); },

View file

@ -0,0 +1,28 @@
import QtQuick 2.4
import FlightGear 1.0
import FlightGear.Launcher 1.0
import "."
Item {
id: root
property alias location: airportData.guid
Positioned {
id: airportData
}
AirportDiagram {
id: diagram
anchors.fill: parent
airport: airportData.guid
onClicked: {
if (pos === null)
return;
}
approachExtensionEnabled: false
}
}

View file

@ -0,0 +1,43 @@
import QtQuick 2.4
import FlightGear 1.0
import FlightGear.Launcher 1.0
import "."
Item {
id: root
property alias legIndex: diagram.activeLegIndex
RouteDiagram {
id: diagram
anchors.fill: parent
flightplan: _launcher.flightPlan
}
Button {
id: previousButton
text: qsTr("Previous Leg")
enabled: diagram.activeLegIndex > 0
onClicked: {
diagram.activeLegIndex = diagram.activeLegIndex - 1
}
anchors.right: root.horizontalCenter
anchors.bottom: root.bottom
anchors.margins: Style.margin
}
Button {
text: qsTr("Next Leg")
width: previousButton.width
enabled: diagram.activeLegIndex < (diagram.numLegs - 1)
onClicked: {
diagram.activeLegIndex = diagram.activeLegIndex + 1
}
anchors.left: root.horizontalCenter
anchors.bottom: root.bottom
anchors.margins: Style.margin
}
}

View file

@ -0,0 +1,130 @@
import QtQuick 2.4
import FlightGear.Launcher 1.0
import FlightGear 1.0
import "."
Rectangle
{
id: root
implicitHeight: childrenRect.height + Style.margin * 2
border.width: 1
border.color: Style.minorFrameColor
signal clickedLeg(var index)
TextMetrics {
id: legDistanceWidth
font.pixelSize: Style.baseFontPixelSize
text: "0000Nm"
}
TextMetrics {
id: legBearingWidth
font.pixelSize: Style.baseFontPixelSize
text: "000*True"
}
TextMetrics {
id: legIdentWidth
font.pixelSize: Style.baseFontPixelSize
text: "XXXXX"
}
TextMetrics {
id: legViaWidth
font.pixelSize: Style.baseFontPixelSize
text: "via XXXXX"
}
readonly property int legDistanceColumnStart: root.width - (legDistanceWidth.width + (Style.margin * 2))
readonly property int legBearingColumnStart: legDistanceColumnStart - (legBearingWidth.width + Style.margin)
// description string fills the middle space, gets elided
readonly property int legDescriptionColumnStart: legIdentWidth.width + legViaWidth.width + Style.margin * 3
readonly property int legDescriptStringWidth: legBearingColumnStart - legDescriptionColumnStart
Column {
width: parent.width - Style.margin * 2
x: Style.margin
y: Style.margin
Repeater {
id: routeLegs
width: parent.width
model: _launcher.flightPlan.legs
delegate: Rectangle {
id: delegateRect
height: rowLabel.height + Style.margin
width: routeLegs.width
color: (model.index % 2) ? "#dfdfdf" : "white"
readonly property string description: {
var s = model.toName;
if (model.wpType === "navaid") {
var freq = model.frequency
if (freq.isValid())
s += " (" + freq.toString() + ")"
}
return s;
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
id: rowLabel
text: model.label
x: Style.margin
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
id: rowAirway
text: {
var awy = model.via;
if (awy === undefined) return "";
return "via " + awy;
}
x: Style.margin * 2 + legIdentWidth.width
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
visible: model.wpType === "navaid"
text: delegateRect.description
x: legDescriptionColumnStart
width: legDescriptStringWidth
elide: Text.ElideRight
}
StyledText {
x: legBearingColumnStart
anchors.verticalCenter: parent.verticalCenter
visible: (model.index > 0)
text: model.track.toString()
}
StyledText {
x: legDistanceColumnStart
anchors.verticalCenter: parent.verticalCenter
visible: (model.index > 0)
text: model.distance.toString()
}
MouseArea {
anchors.fill: parent
onClicked: {
root.clickedLeg(model.index)
}
}
} // of delegate rect
}
}
}

View file

@ -118,6 +118,9 @@
<file>qml/Overlay.qml</file>
<file>qml/OverlayShared.qml</file>
<file>qml/Weblink.qml</file>
<file>qml/PlanAirportView.qml</file>
<file>qml/PlanRouteDetails.qml</file>
<file>qml/RouteLegsView.qml</file>
</qresource>
<qresource prefix="/preview">
<file alias="close-icon">preview-close.png</file>