diff --git a/src/GUI/CMakeLists.txt b/src/GUI/CMakeLists.txt index 531223902..b8eb51e5e 100644 --- a/src/GUI/CMakeLists.txt +++ b/src/GUI/CMakeLists.txt @@ -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) diff --git a/src/GUI/FlightPlanController.cxx b/src/GUI/FlightPlanController.cxx new file mode 100644 index 000000000..1d4bba39f --- /dev/null +++ b/src/GUI/FlightPlanController.cxx @@ -0,0 +1,552 @@ +#include "FlightPlanController.hxx" + +#include +#include +#include +#include +#include + +#include + +#include
+#include +#include +#include +#include + +#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(leg->waypoint()); + return QString::fromStdString(via->airway()); + } + + if (wp->flag(WPT_VIA)) { + AirwayRef awy = static_cast(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(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 roleNames() const override + { + QHash 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("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(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(_fp->flightRules()); +} + +FlightPlanController::FlightType FlightPlanController::flightType() const +{ + return static_cast(_fp->flightType()); +} + +void FlightPlanController::setFlightRules(FlightRules r) +{ + _fp->setFlightRules(static_cast(r)); +} + +void FlightPlanController::setFlightType(FlightType ty) +{ + _fp->setFlightType(static_cast(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(apt->inner())); + } + + emit infoChanged(); +} + +void FlightPlanController::setDestination(QmlPositioned *apt) +{ + if (apt) { + if (apt->inner() == _fp->destinationAirport()) + return; + + _fp->setDestination(fgpositioned_cast(apt->inner())); + } else { + _fp->clearDestination(); + + } + emit infoChanged(); +} + +void FlightPlanController::setAlternate(QmlPositioned *apt) +{ + if (apt) { + if (apt->inner() == _fp->alternate()) + return; + + _fp->setAlternate(fgpositioned_cast(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(speed.value); + if (_fp->cruiseSpeedKnots() == knotsVal) { + return; + } + + _fp->setCruiseSpeedKnots(knotsVal); + } + + emit infoChanged(); +} + +#include "FlightPlanController.moc" diff --git a/src/GUI/FlightPlanController.hxx b/src/GUI/FlightPlanController.hxx new file mode 100644 index 000000000..8505d36f7 --- /dev/null +++ b/src/GUI/FlightPlanController.hxx @@ -0,0 +1,148 @@ +#ifndef FLIGHTPLANCONTROLLER_HXX +#define FLIGHTPLANCONTROLLER_HXX + +#include + +#include + +#include + +#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 _delegate; + LaunchConfig* _config = nullptr; +}; + +#endif // FLIGHTPLANCONTROLLER_HXX diff --git a/src/GUI/LauncherController.cxx b/src/GUI/LauncherController.cxx index 6ce3b528d..5a4854bba 100644 --- a/src/GUI/LauncherController.cxx +++ b/src/GUI/LauncherController.cxx @@ -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("FlightGear.Launcher", 1, 0, "LauncherController", "no"); qmlRegisterUncreatableType("FlightGear.Launcher", 1, 0, "LocationController", "no"); + qmlRegisterUncreatableType("FlightGear.Launcher", 1, 0, "FlightPlanController", "no"); qmlRegisterType("FlightGear.Launcher", 1, 0, "ArgumentTokenizer"); qmlRegisterUncreatableType("FlightGear.Launcher", 1, 0, "QAIM", "no"); @@ -156,6 +161,7 @@ void LauncherController::initQML() qmlRegisterType("FlightGear", 1, 0, "PixmapImage"); qmlRegisterType("FlightGear", 1, 0, "AirportDiagram"); qmlRegisterType("FlightGear", 1, 0, "NavaidDiagram"); + qmlRegisterType("FlightGear", 1, 0, "RouteDiagram"); qmlRegisterType("FlightGear", 1, 0, "RadioButtonGroup"); qmlRegisterSingletonType(QUrl("qrc:///qml/OverlayShared.qml"), "FlightGear", 1, 0, "OverlayShared"); diff --git a/src/GUI/LauncherController.hxx b/src/GUI/LauncherController.hxx index 5bbd28b9b..4c5c7f8a5 100644 --- a/src/GUI/LauncherController.hxx +++ b/src/GUI/LauncherController.hxx @@ -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; diff --git a/src/GUI/RouteDiagram.cxx b/src/GUI/RouteDiagram.cxx new file mode 100644 index 000000000..1af9fb875 --- /dev/null +++ b/src/GUI/RouteDiagram.cxx @@ -0,0 +1,141 @@ +// RouteDiagram.cxx - GUI diagram of a route +// +// Written by James Turner, started August 2018. +// +// Copyright (C) 2018 James Turner +// +// 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 +#include +#include +#include + +#include + +#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 lines; + QVector 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(); +} diff --git a/src/GUI/RouteDiagram.hxx b/src/GUI/RouteDiagram.hxx new file mode 100644 index 000000000..7564c4708 --- /dev/null +++ b/src/GUI/RouteDiagram.hxx @@ -0,0 +1,81 @@ +// RouteDiagram.hxx - show a route graphically +// +// Written by James Turner, started August 2018. +// +// Copyright (C) 2018 James Turner +// +// 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 +#include + +#include + +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 m_path; + int m_activeLegIndex = 0; +}; + +#endif // of GUI_ROUTE_DIAGRAM_HXX diff --git a/src/GUI/qml/FlightPlan.qml b/src/GUI/qml/FlightPlan.qml index 4d2469a68..4f430ce38 100644 --- a/src/GUI/qml/FlightPlan.qml +++ b/src/GUI/qml/FlightPlan.qml @@ -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 + } + } } diff --git a/src/GUI/qml/Launcher.qml b/src/GUI/qml/Launcher.qml index 0fae0003d..c15bef507 100644 --- a/src/GUI/qml/Launcher.qml +++ b/src/GUI/qml/Launcher.qml @@ -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(); }, diff --git a/src/GUI/qml/PlanAirportView.qml b/src/GUI/qml/PlanAirportView.qml new file mode 100644 index 000000000..06637ad5d --- /dev/null +++ b/src/GUI/qml/PlanAirportView.qml @@ -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 + } + +} diff --git a/src/GUI/qml/PlanRouteDetails.qml b/src/GUI/qml/PlanRouteDetails.qml new file mode 100644 index 000000000..712b71cb9 --- /dev/null +++ b/src/GUI/qml/PlanRouteDetails.qml @@ -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 + } +} diff --git a/src/GUI/qml/RouteLegsView.qml b/src/GUI/qml/RouteLegsView.qml new file mode 100644 index 000000000..9d641fd1f --- /dev/null +++ b/src/GUI/qml/RouteLegsView.qml @@ -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 + } + + } + +} + diff --git a/src/GUI/resources.qrc b/src/GUI/resources.qrc index 720d966bf..42c97fb5a 100644 --- a/src/GUI/resources.qrc +++ b/src/GUI/resources.qrc @@ -118,6 +118,9 @@ qml/Overlay.qml qml/OverlayShared.qml qml/Weblink.qml + qml/PlanAirportView.qml + qml/PlanRouteDetails.qml + qml/RouteLegsView.qml preview-close.png