// LocationWidget.cxx - GUI launcher dialog using Qt5 // // Written by James Turner, started October 2015. // // Copyright (C) 2015 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 "LocationWidget.hxx" #include "ui_LocationWidget.h" #include #include #include #include #include #include #include #include "AirportDiagram.hxx" #include "NavaidDiagram.hxx" #include #include // for parking #include
#include #include #include
#include
#include
// for fgSetDouble using namespace flightgear; QString fixNavaidName(QString s) { // split into words QStringList words = s.split(QChar(' ')); QStringList changedWords; Q_FOREACH(QString w, words) { QString up = w.toUpper(); // expand common abbreviations if (up == "FLD") { changedWords.append("Field"); continue; } if (up == "MUNI") { changedWords.append("Municipal"); continue; } if (up == "RGNL") { changedWords.append("Regional"); continue; } if (up == "CTR") { changedWords.append("Center"); continue; } if (up == "INTL") { changedWords.append("International"); continue; } // occurs in many Australian airport names in our DB if (up == "(NSW)") { changedWords.append("(New South Wales)"); continue; } if ((up == "VOR") || (up == "NDB") || (up == "VOR-DME") || (up == "VORTAC") || (up == "NDB-DME")) { changedWords.append(w); continue; } QChar firstChar = w.at(0).toUpper(); w = w.mid(1).toLower(); w.prepend(firstChar); changedWords.append(w); } return changedWords.join(QChar(' ')); } QString formatGeodAsString(const SGGeod& geod) { QChar ns = (geod.getLatitudeDeg() > 0.0) ? 'N' : 'S'; QChar ew = (geod.getLongitudeDeg() > 0.0) ? 'E' : 'W'; return QString::number(fabs(geod.getLongitudeDeg()), 'f',2 ) + ew + " " + QString::number(fabs(geod.getLatitudeDeg()), 'f',2 ) + ns; } bool parseStringAsGeod(const QString& s, SGGeod& result) { int commaPos = s.indexOf(QChar(',')); if (commaPos < 0) return false; bool ok; double lon = s.leftRef(commaPos).toDouble(&ok); if (!ok) return false; double lat = s.midRef(commaPos+1).toDouble(&ok); if (!ok) return false; result = SGGeod::fromDeg(lon, lat); return true; } class IdentSearchFilter : public FGPositioned::TypeFilter { public: IdentSearchFilter() { addType(FGPositioned::AIRPORT); addType(FGPositioned::SEAPORT); addType(FGPositioned::HELIPAD); addType(FGPositioned::VOR); addType(FGPositioned::FIX); addType(FGPositioned::NDB); } }; class NavSearchModel : public QAbstractListModel { Q_OBJECT public: NavSearchModel() : m_searchActive(false) { } void setSearch(QString t) { beginResetModel(); m_items.clear(); m_ids.clear(); std::string term(t.toUpper().toStdString()); IdentSearchFilter filter; FGPositionedList exactMatches = NavDataCache::instance()->findAllWithIdent(term, &filter, true); for (unsigned int i=0; iguid()); m_items.push_back(exactMatches[i]); } endResetModel(); m_search.reset(new NavDataCache::ThreadedGUISearch(term)); QTimer::singleShot(100, this, &NavSearchModel::onSearchResultsPoll); m_searchActive = true; endResetModel(); } bool isSearchActive() const { return m_searchActive; } virtual int rowCount(const QModelIndex&) const { // if empty, return 1 for special 'no matches'? return m_ids.size(); } virtual QVariant data(const QModelIndex& index, int role) const { if (!index.isValid()) return QVariant(); FGPositionedRef pos = itemAtRow(index.row()); if (role == Qt::DisplayRole) { if (pos->type() == FGPositioned::FIX) { // fixes don't have a name, show position instead return QString("Fix %1 (%2)").arg(QString::fromStdString(pos->ident())) .arg(formatGeodAsString(pos->geod())); } else { QString name = fixNavaidName(QString::fromStdString(pos->name())); return QString("%1: %2").arg(QString::fromStdString(pos->ident())).arg(name); } } if (role == Qt::DecorationRole) { return AirportDiagram::iconForPositioned(pos, true); } if (role == Qt::EditRole) { return QString::fromStdString(pos->ident()); } if (role == Qt::UserRole) { return static_cast(m_ids[index.row()]); } return QVariant(); } FGPositionedRef itemAtRow(unsigned int row) const { FGPositionedRef pos = m_items[row]; if (!pos.valid()) { pos = NavDataCache::instance()->loadById(m_ids[row]); m_items[row] = pos; } return pos; } Q_SIGNALS: void searchComplete(); private: void onSearchResultsPoll() { PositionedIDVec newIds = m_search->results(); beginInsertRows(QModelIndex(), m_ids.size(), newIds.size() - 1); for (unsigned int i=m_ids.size(); i < newIds.size(); ++i) { m_ids.push_back(newIds[i]); m_items.push_back(FGPositionedRef()); // null ref } endInsertRows(); if (m_search->isComplete()) { m_searchActive = false; m_search.reset(); emit searchComplete(); } else { QTimer::singleShot(100, this, &NavSearchModel::onSearchResultsPoll); } } private: PositionedIDVec m_ids; mutable FGPositionedList m_items; bool m_searchActive; QScopedPointer m_search; }; LocationWidget::LocationWidget(QWidget *parent) : QWidget(parent), m_ui(new Ui::LocationWidget) { m_ui->setupUi(this); QIcon historyIcon(":/history-icon"); m_ui->searchHistory->setIcon(historyIcon); QByteArray format; m_ui->searchIcon->setMovie(new QMovie(":/spinner", format, this)); m_searchModel = new NavSearchModel; m_ui->searchResultsList->setModel(m_searchModel); connect(m_ui->searchResultsList, &QListView::clicked, this, &LocationWidget::onSearchResultSelected); connect(m_searchModel, &NavSearchModel::searchComplete, this, &LocationWidget::onSearchComplete); connect(m_ui->runwayCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(updateDescription())); connect(m_ui->parkingCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(updateDescription())); connect(m_ui->runwayRadio, SIGNAL(toggled(bool)), this, SLOT(updateDescription())); connect(m_ui->parkingRadio, SIGNAL(toggled(bool)), this, SLOT(updateDescription())); connect(m_ui->onFinalCheckbox, SIGNAL(toggled(bool)), this, SLOT(updateDescription())); connect(m_ui->approachDistanceSpin, SIGNAL(valueChanged(int)), this, SLOT(updateDescription())); connect(m_ui->airportDiagram, &AirportDiagram::clickedRunway, this, &LocationWidget::onAirportDiagramClicked); connect(m_ui->locationSearchEdit, &QLineEdit::returnPressed, this, &LocationWidget::onSearch); // disabled for now m_ui->searchHistory->hide(); connect(m_ui->searchHistory, &QPushButton::clicked, this, &LocationWidget::onPopupHistory); connect(m_ui->trueBearing, &QCheckBox::toggled, this, &LocationWidget::onOffsetBearingTrueChanged); connect(m_ui->offsetGroup, &QGroupBox::toggled, this, &LocationWidget::onOffsetEnabledToggled); connect(m_ui->trueBearing, &QCheckBox::toggled, this, &LocationWidget::onOffsetDataChanged); connect(m_ui->offsetBearingSpinbox, SIGNAL(valueChanged(int)), this, SLOT(onOffsetDataChanged())); connect(m_ui->offsetNmSpinbox, SIGNAL(valueChanged(double)), this, SLOT(onOffsetDataChanged())); m_backButton = new QToolButton(this); m_backButton->setGeometry(0, 0, 64, 32); m_backButton->setText("<< Back"); //m_backButton->setIcon(QIcon(":/search-icon")); m_backButton->raise(); connect(m_backButton, &QAbstractButton::clicked, this, &LocationWidget::onBackToSearch); // force various pieces of UI into sync onOffsetEnabledToggled(m_ui->offsetGroup->isChecked()); onOffsetBearingTrueChanged(m_ui->trueBearing->isChecked()); onBackToSearch(); } LocationWidget::~LocationWidget() { delete m_ui; } void LocationWidget::restoreSettings() { QSettings settings; if (settings.contains("location-lat")) { m_locationIsLatLon = true; m_geodLocation = SGGeod::fromDeg(settings.value("location-lon").toDouble(), settings.value("location-lat").toDouble()); } else if (settings.contains("location-id")) { m_location = NavDataCache::instance()->loadById(settings.value("location-id").toULongLong()); } m_ui->altitudeSpinbox->setValue(settings.value("altitude").toInt()); m_ui->airspeedSpinbox->setValue(settings.value("speed").toInt()); m_ui->offsetGroup->setChecked(settings.value("offset-enabled").toBool()); m_ui->offsetBearingSpinbox->setValue(settings.value("offset-bearing").toInt()); m_ui->offsetNmSpinbox->setValue(settings.value("offset-distance").toInt()); onLocationChanged(); updateDescription(); } bool LocationWidget::shouldStartPaused() const { if (!m_location) { return false; // defaults to on-ground at KSFO } if (FGAirport::isAirportType(m_location.ptr())) { return m_ui->onFinalCheckbox->isChecked(); } else { // navaid, start paused return true; } return false; } void LocationWidget::saveSettings() { QSettings settings; settings.remove("location-id"); if (m_locationIsLatLon) { settings.setValue("location-lat", m_geodLocation.getLatitudeDeg()); settings.setValue("location-lon", m_geodLocation.getLongitudeDeg()); } else if (m_location) { settings.setValue("location-id", m_location->guid()); } settings.setValue("altitude", m_ui->altitudeSpinbox->value()); settings.setValue("speed", m_ui->airspeedSpinbox->value()); settings.setValue("offset-enabled", m_ui->offsetGroup->isChecked()); settings.setValue("offset-bearing", m_ui->offsetBearingSpinbox->value()); settings.setValue("offset-distance", m_ui->offsetNmSpinbox->value()); } void LocationWidget::setLocationOptions() { flightgear::Options* opt = flightgear::Options::sharedInstance(); std::string altStr = QString::number(m_ui->altitudeSpinbox->value()).toStdString(); std::string vcStr = QString::number(m_ui->airspeedSpinbox->value()).toStdString(); if (m_locationIsLatLon) { // bypass the options mechanism because converting to deg:min:sec notation // just to parse back again is nasty. fgSetDouble("/sim/presets/latitude-deg", m_geodLocation.getLatitudeDeg()); fgSetDouble("/position/latitude-deg", m_geodLocation.getLatitudeDeg()); fgSetDouble("/sim/presets/longitude-deg", m_geodLocation.getLongitudeDeg()); fgSetDouble("/position/longitude-deg", m_geodLocation.getLongitudeDeg()); opt->addOption("altitude", altStr); opt->addOption("vc", vcStr); return; } if (!m_location) { return; } if (FGAirport::isAirportType(m_location.ptr())) { FGAirport* apt = static_cast(m_location.ptr()); opt->addOption("airport", apt->ident()); if (m_ui->runwayRadio->isChecked()) { int index = m_ui->runwayCombo->itemData(m_ui->runwayCombo->currentIndex()).toInt(); if (index >= 0) { // explicit runway choice FGRunwayRef runway = apt->getRunwayByIndex(index); opt->addOption("runway", runway->ident()); // set nav-radio 1 based on selected runway if (runway->ILS()) { double mhz = runway->ILS()->get_freq() / 100.0; QString navOpt = QString("%1:%2").arg(runway->headingDeg()).arg(mhz); opt->addOption("nav1", navOpt.toStdString()); } } if (m_ui->onFinalCheckbox->isChecked()) { opt->addOption("glideslope", "3.0"); double offsetNm = m_ui->approachDistanceSpin->value(); opt->addOption("offset-distance", QString::number(offsetNm).toStdString()); } } else if (m_ui->parkingRadio->isChecked()) { // parking selection opt->addOption("parkpos", m_ui->parkingCombo->currentText().toStdString()); } // of location is an airport } else { // location is a navaid // note setting the ident here is ambigious, we really only need and // want the 'navaid-id' property. However setting the 'real' option // gives a better UI experience (eg existing Position in Air dialog) FGPositioned::Type ty = m_location->type(); switch (ty) { case FGPositioned::VOR: opt->addOption("vor", m_location->ident()); setNavRadioOption(); break; case FGPositioned::NDB: opt->addOption("ndb", m_location->ident()); setNavRadioOption(); break; case FGPositioned::FIX: opt->addOption("fix", m_location->ident()); break; default: break; } opt->addOption("altitude", altStr); opt->addOption("vc", vcStr); // set disambiguation property globals->get_props()->setIntValue("/sim/presets/navaid-id", static_cast(m_location->guid())); } } void LocationWidget::setNavRadioOption() { flightgear::Options* opt = flightgear::Options::sharedInstance(); if (m_location->type() == FGPositioned::VOR) { FGNavRecordRef nav(static_cast(m_location.ptr())); double mhz = nav->get_freq() / 100.0; int heading = 0; // add heading support QString navOpt = QString("%1:%2").arg(heading).arg(mhz); opt->addOption("nav1", navOpt.toStdString()); } else { FGNavRecordRef nav(static_cast(m_location.ptr())); int khz = nav->get_freq() / 100; int heading = 0; QString adfOpt = QString("%1:%2").arg(heading).arg(khz); qDebug() << "ADF opt is:" << adfOpt; opt->addOption("adf1", adfOpt.toStdString()); } } void LocationWidget::onSearch() { QString search = m_ui->locationSearchEdit->text(); m_locationIsLatLon = parseStringAsGeod(search, m_geodLocation); if (m_locationIsLatLon) { m_ui->searchIcon->setVisible(false); m_ui->searchStatusText->setText(QString("Position '%1'").arg(formatGeodAsString(m_geodLocation))); m_location.clear(); onLocationChanged(); updateDescription(); return; } m_searchModel->setSearch(search); if (m_searchModel->isSearchActive()) { m_ui->searchStatusText->setText(QString("Searching for '%1'").arg(search)); qDebug() << "setting icon visible"; m_ui->searchIcon->setVisible(true); m_ui->searchIcon->movie()->start(); } else if (m_searchModel->rowCount(QModelIndex()) == 1) { setBaseLocation(m_searchModel->itemAtRow(0)); } } void LocationWidget::onSearchComplete() { QString search = m_ui->locationSearchEdit->text(); m_ui->searchIcon->setVisible(false); m_ui->searchStatusText->setText(QString("Results for '%1'").arg(search)); int numResults = m_searchModel->rowCount(QModelIndex()); if (numResults == 0) { m_ui->searchStatusText->setText(QString("No matches for '%1'").arg(search)); } else if (numResults == 1) { setBaseLocation(m_searchModel->itemAtRow(0)); } } void LocationWidget::onLocationChanged() { bool locIsAirport = FGAirport::isAirportType(m_location.ptr()); m_backButton->show(); if (locIsAirport) { m_ui->stack->setCurrentIndex(0); FGAirport* apt = static_cast(m_location.ptr()); m_ui->airportDiagram->setAirport(apt); m_ui->runwayRadio->setChecked(true); // default back to runway mode // unless multiplayer is enabled ? m_ui->airportDiagram->setEnabled(true); m_ui->runwayCombo->clear(); m_ui->runwayCombo->addItem("Automatic", -1); for (unsigned int r=0; rnumRunways(); ++r) { FGRunwayRef rwy = apt->getRunwayByIndex(r); // add runway with index as data role m_ui->runwayCombo->addItem(QString::fromStdString(rwy->ident()), r); m_ui->airportDiagram->addRunway(rwy); } m_ui->parkingCombo->clear(); FGAirportDynamics* dynamics = apt->getDynamics(); PositionedIDVec parkings = NavDataCache::instance()->airportItemsOfType(m_location->guid(), FGPositioned::PARKING); if (parkings.empty()) { m_ui->parkingCombo->setEnabled(false); m_ui->parkingRadio->setEnabled(false); } else { m_ui->parkingCombo->setEnabled(true); m_ui->parkingRadio->setEnabled(true); Q_FOREACH(PositionedID parking, parkings) { FGParking* park = dynamics->getParking(parking); m_ui->parkingCombo->addItem(QString::fromStdString(park->getName()), static_cast(parking)); m_ui->airportDiagram->addParking(park); } } } else if (m_locationIsLatLon) { m_ui->stack->setCurrentIndex(1); m_ui->navaidDiagram->setGeod(m_geodLocation); } else if (m_location) { // navaid m_ui->stack->setCurrentIndex(1); m_ui->navaidDiagram->setNavaid(m_location); } } void LocationWidget::onOffsetEnabledToggled(bool on) { m_ui->navaidDiagram->setOffsetEnabled(on); } void LocationWidget::onAirportDiagramClicked(FGRunwayRef rwy) { if (rwy) { m_ui->runwayRadio->setChecked(true); int rwyIndex = m_ui->runwayCombo->findText(QString::fromStdString(rwy->ident())); m_ui->runwayCombo->setCurrentIndex(rwyIndex); m_ui->airportDiagram->setSelectedRunway(rwy); } updateDescription(); } QString LocationWidget::locationDescription() const { if (!m_location) { if (m_locationIsLatLon) { return QString("at position %1").arg(formatGeodAsString(m_geodLocation)); } return QString("No location selected"); } bool locIsAirport = FGAirport::isAirportType(m_location.ptr()); QString ident = QString::fromStdString(m_location->ident()), name = QString::fromStdString(m_location->name()); name = fixNavaidName(name); if (locIsAirport) { //FGAirport* apt = static_cast(m_location.ptr()); QString locationOnAirport; if (m_ui->runwayRadio->isChecked()) { bool onFinal = m_ui->onFinalCheckbox->isChecked(); int comboIndex = m_ui->runwayCombo->currentIndex(); QString runwayName = (comboIndex == 0) ? "active runway" : QString("runway %1").arg(m_ui->runwayCombo->currentText()); if (onFinal) { int finalDistance = m_ui->approachDistanceSpin->value(); locationOnAirport = QString("on %2-mile final to %1").arg(runwayName).arg(finalDistance); } else { locationOnAirport = QString("on %1").arg(runwayName); } } else if (m_ui->parkingRadio->isChecked()) { locationOnAirport = QString("at parking position %1").arg(m_ui->parkingCombo->currentText()); } return QString("%2 (%1): %3").arg(ident).arg(name).arg(locationOnAirport); } else { QString navaidType; switch (m_location->type()) { case FGPositioned::VOR: navaidType = QString("VOR"); break; case FGPositioned::NDB: navaidType = QString("NDB"); break; case FGPositioned::FIX: return QString("at waypoint %1").arg(ident); default: // unsupported type break; } return QString("at %1 %2 (%3)").arg(navaidType).arg(ident).arg(name); } return QString("No location selected"); } void LocationWidget::updateDescription() { bool locIsAirport = FGAirport::isAirportType(m_location.ptr()); if (locIsAirport) { FGAirport* apt = static_cast(m_location.ptr()); if (m_ui->runwayRadio->isChecked()) { int comboIndex = m_ui->runwayCombo->currentIndex(); int runwayIndex = m_ui->runwayCombo->itemData(comboIndex).toInt(); // we can't figure out the active runway in the launcher (yet) FGRunwayRef rwy = (runwayIndex >= 0) ? apt->getRunwayByIndex(runwayIndex) : FGRunwayRef(); m_ui->airportDiagram->setSelectedRunway(rwy); } if (m_ui->onFinalCheckbox->isChecked()) { m_ui->airportDiagram->setApproachExtensionDistance(m_ui->approachDistanceSpin->value()); } else { m_ui->airportDiagram->setApproachExtensionDistance(0.0); } } else { } #if 0 QString locationOnAirport; if (m_ui->runwayRadio->isChecked()) { } else if (m_ui->parkingRadio->isChecked()) { locationOnAirport = QString("at parking position %1").arg(m_ui->parkingCombo->currentText()); } m_ui->airportDescription->setText(); #endif emit descriptionChanged(locationDescription()); } void LocationWidget::onSearchResultSelected(const QModelIndex& index) { setBaseLocation(m_searchModel->itemAtRow(index.row())); } void LocationWidget::onOffsetBearingTrueChanged(bool on) { m_ui->offsetBearingLabel->setText(on ? tr("True bearing:") : tr("Magnetic bearing:")); } void LocationWidget::onPopupHistory() { } void LocationWidget::setBaseLocation(FGPositionedRef ref) { if (m_location == ref) return; m_location = ref; onLocationChanged(); updateDescription(); } void LocationWidget::onOffsetDataChanged() { m_ui->navaidDiagram->setOffsetEnabled(m_ui->offsetGroup->isChecked()); m_ui->navaidDiagram->setOffsetBearingDeg(m_ui->offsetBearingSpinbox->value()); m_ui->navaidDiagram->setOffsetDistanceNm(m_ui->offsetNmSpinbox->value()); } void LocationWidget::onBackToSearch() { m_ui->stack->setCurrentIndex(2); m_backButton->hide(); } #include "LocationWidget.moc"