1 // LocationWidget.cxx - GUI launcher dialog using Qt5
3 // Written by James Turner, started October 2015.
5 // Copyright (C) 2015 James Turner <zakalawe@mac.com>
7 // This program is free software; you can redistribute it and/or
8 // modify it under the terms of the GNU General Public License as
9 // published by the Free Software Foundation; either version 2 of the
10 // License, or (at your option) any later version.
12 // This program is distributed in the hope that it will be useful, but
13 // WITHOUT ANY WARRANTY; without even the implied warranty of
14 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 // General Public License for more details.
17 // You should have received a copy of the GNU General Public License
18 // along with this program; if not, write to the Free Software
19 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21 #include "LocationWidget.hxx"
22 #include "ui_LocationWidget.h"
25 #include <QAbstractListModel>
28 #include <QToolButton>
32 #include "AirportDiagram.hxx"
33 #include "NavaidDiagram.hxx"
35 #include <Airports/airport.hxx>
36 #include <Airports/dynamics.hxx> // for parking
37 #include <Main/globals.hxx>
38 #include <Navaids/NavDataCache.hxx>
39 #include <Navaids/navrecord.hxx>
40 #include <Main/options.hxx>
41 #include <Main/fg_init.hxx>
42 #include <Main/fg_props.hxx> // for fgSetDouble
44 const int MAX_RECENT_AIRPORTS = 32;
46 using namespace flightgear;
48 QString fixNavaidName(QString s)
51 QStringList words = s.split(QChar(' '));
52 QStringList changedWords;
53 Q_FOREACH(QString w, words) {
54 QString up = w.toUpper();
56 // expand common abbreviations
58 changedWords.append("Field");
63 changedWords.append("Municipal");
68 changedWords.append("Regional");
73 changedWords.append("Center");
78 changedWords.append("International");
82 // occurs in many Australian airport names in our DB
84 changedWords.append("(New South Wales)");
88 if ((up == "VOR") || (up == "NDB") || (up == "VOR-DME") || (up == "VORTAC") || (up == "NDB-DME")) {
89 changedWords.append(w);
93 QChar firstChar = w.at(0).toUpper();
94 w = w.mid(1).toLower();
97 changedWords.append(w);
100 return changedWords.join(QChar(' '));
103 QString formatGeodAsString(const SGGeod& geod)
105 QChar ns = (geod.getLatitudeDeg() > 0.0) ? 'N' : 'S';
106 QChar ew = (geod.getLongitudeDeg() > 0.0) ? 'E' : 'W';
108 return QString::number(fabs(geod.getLongitudeDeg()), 'f',2 ) + ew + " " +
109 QString::number(fabs(geod.getLatitudeDeg()), 'f',2 ) + ns;
112 bool parseStringAsGeod(const QString& s, SGGeod& result)
114 int commaPos = s.indexOf(QChar(','));
119 double lon = s.leftRef(commaPos).toDouble(&ok);
123 double lat = s.midRef(commaPos+1).toDouble(&ok);
127 result = SGGeod::fromDeg(lon, lat);
131 class IdentSearchFilter : public FGPositioned::TypeFilter
136 addType(FGPositioned::AIRPORT);
137 addType(FGPositioned::SEAPORT);
138 addType(FGPositioned::HELIPAD);
139 addType(FGPositioned::VOR);
140 addType(FGPositioned::FIX);
141 addType(FGPositioned::NDB);
145 class NavSearchModel : public QAbstractListModel
150 m_searchActive(false)
154 void setSearch(QString t)
161 std::string term(t.toUpper().toStdString());
163 IdentSearchFilter filter;
164 FGPositionedList exactMatches = NavDataCache::instance()->findAllWithIdent(term, &filter, true);
166 for (unsigned int i=0; i<exactMatches.size(); ++i) {
167 m_ids.push_back(exactMatches[i]->guid());
168 m_items.push_back(exactMatches[i]);
173 m_search.reset(new NavDataCache::ThreadedGUISearch(term));
174 QTimer::singleShot(100, this, &NavSearchModel::onSearchResultsPoll);
175 m_searchActive = true;
179 bool isSearchActive() const
181 return m_searchActive;
184 virtual int rowCount(const QModelIndex&) const
186 // if empty, return 1 for special 'no matches'?
190 virtual QVariant data(const QModelIndex& index, int role) const
192 if (!index.isValid())
195 FGPositionedRef pos = itemAtRow(index.row());
196 if (role == Qt::DisplayRole) {
197 if (pos->type() == FGPositioned::FIX) {
198 // fixes don't have a name, show position instead
199 return QString("Fix %1 (%2)").arg(QString::fromStdString(pos->ident()))
200 .arg(formatGeodAsString(pos->geod()));
202 QString name = fixNavaidName(QString::fromStdString(pos->name()));
203 return QString("%1: %2").arg(QString::fromStdString(pos->ident())).arg(name);
207 if (role == Qt::DecorationRole) {
208 return AirportDiagram::iconForPositioned(pos);
211 if (role == Qt::EditRole) {
212 return QString::fromStdString(pos->ident());
215 if (role == Qt::UserRole) {
216 return static_cast<qlonglong>(m_ids[index.row()]);
222 FGPositionedRef itemAtRow(unsigned int row) const
224 FGPositionedRef pos = m_items[row];
226 pos = NavDataCache::instance()->loadById(m_ids[row]);
233 void searchComplete();
237 void onSearchResultsPoll()
239 PositionedIDVec newIds = m_search->results();
241 beginInsertRows(QModelIndex(), m_ids.size(), newIds.size() - 1);
242 for (unsigned int i=m_ids.size(); i < newIds.size(); ++i) {
243 m_ids.push_back(newIds[i]);
244 m_items.push_back(FGPositionedRef()); // null ref
248 if (m_search->isComplete()) {
249 m_searchActive = false;
251 emit searchComplete();
253 QTimer::singleShot(100, this, &NavSearchModel::onSearchResultsPoll);
258 PositionedIDVec m_ids;
259 mutable FGPositionedList m_items;
261 QScopedPointer<NavDataCache::ThreadedGUISearch> m_search;
265 LocationWidget::LocationWidget(QWidget *parent) :
267 m_ui(new Ui::LocationWidget)
272 QIcon historyIcon(":/history-icon");
273 m_ui->searchHistory->setIcon(historyIcon);
276 m_ui->searchIcon->setMovie(new QMovie(":/spinner", format, this));
278 m_searchModel = new NavSearchModel;
279 m_ui->searchResultsList->setModel(m_searchModel);
280 connect(m_ui->searchResultsList, &QListView::clicked,
281 this, &LocationWidget::onSearchResultSelected);
282 connect(m_searchModel, &NavSearchModel::searchComplete,
283 this, &LocationWidget::onSearchComplete);
285 connect(m_ui->runwayCombo, SIGNAL(currentIndexChanged(int)),
286 this, SLOT(updateDescription()));
287 connect(m_ui->parkingCombo, SIGNAL(currentIndexChanged(int)),
288 this, SLOT(updateDescription()));
289 connect(m_ui->runwayRadio, SIGNAL(toggled(bool)),
290 this, SLOT(updateDescription()));
291 connect(m_ui->parkingRadio, SIGNAL(toggled(bool)),
292 this, SLOT(updateDescription()));
293 connect(m_ui->onFinalCheckbox, SIGNAL(toggled(bool)),
294 this, SLOT(updateDescription()));
295 connect(m_ui->approachDistanceSpin, SIGNAL(valueChanged(int)),
296 this, SLOT(updateDescription()));
298 connect(m_ui->airportDiagram, &AirportDiagram::clickedRunway,
299 this, &LocationWidget::onAirportDiagramClicked);
301 connect(m_ui->locationSearchEdit, &QLineEdit::returnPressed,
302 this, &LocationWidget::onSearch);
304 connect(m_ui->searchHistory, &QPushButton::clicked,
305 this, &LocationWidget::onPopupHistory);
307 connect(m_ui->trueBearing, &QCheckBox::toggled,
308 this, &LocationWidget::onOffsetBearingTrueChanged);
309 connect(m_ui->offsetGroup, &QGroupBox::toggled,
310 this, &LocationWidget::onOffsetEnabledToggled);
311 connect(m_ui->trueBearing, &QCheckBox::toggled, this,
312 &LocationWidget::onOffsetDataChanged);
313 connect(m_ui->offsetBearingSpinbox, SIGNAL(valueChanged(int)),
314 this, SLOT(onOffsetDataChanged()));
315 connect(m_ui->offsetNmSpinbox, SIGNAL(valueChanged(double)),
316 this, SLOT(onOffsetDataChanged()));
318 m_backButton = new QToolButton(this);
319 m_backButton->setGeometry(0, 0, 64, 32);
320 m_backButton->setText("<< Back");
321 //m_backButton->setIcon(QIcon(":/search-icon"));
322 m_backButton->raise();
324 connect(m_backButton, &QAbstractButton::clicked,
325 this, &LocationWidget::onBackToSearch);
327 // force various pieces of UI into sync
328 onOffsetEnabledToggled(m_ui->offsetGroup->isChecked());
329 onOffsetBearingTrueChanged(m_ui->trueBearing->isChecked());
333 LocationWidget::~LocationWidget()
338 void LocationWidget::restoreSettings()
341 Q_FOREACH(QVariant v, settings.value("recent-locations").toList()) {
342 m_recentAirports.push_back(v.toLongLong());
345 if (!m_recentAirports.empty()) {
346 setBaseLocation(NavDataCache::instance()->loadById(m_recentAirports.front()));
352 bool LocationWidget::shouldStartPaused() const
355 return false; // defaults to on-ground at KSFO
358 if (FGAirport::isAirportType(m_location.ptr())) {
359 return m_ui->onFinalCheckbox->isChecked();
361 // navaid, start paused
368 void LocationWidget::saveSettings()
372 QVariantList locations;
373 Q_FOREACH(PositionedID v, m_recentAirports) {
374 locations.push_back(v);
377 settings.setValue("recent-airports", locations);
380 void LocationWidget::setLocationOptions()
382 flightgear::Options* opt = flightgear::Options::sharedInstance();
384 std::string altStr = QString::number(m_ui->altitudeSpinbox->value()).toStdString();
385 std::string vcStr = QString::number(m_ui->airspeedSpinbox->value()).toStdString();
387 if (m_locationIsLatLon) {
388 // bypass the options mechanism because converting to deg:min:sec notation
389 // just to parse back again is nasty.
390 fgSetDouble("/sim/presets/latitude-deg", m_geodLocation.getLatitudeDeg());
391 fgSetDouble("/position/latitude-deg", m_geodLocation.getLatitudeDeg());
392 fgSetDouble("/sim/presets/longitude-deg", m_geodLocation.getLongitudeDeg());
393 fgSetDouble("/position/longitude-deg", m_geodLocation.getLongitudeDeg());
395 opt->addOption("altitude", altStr);
396 opt->addOption("vc", vcStr);
404 if (FGAirport::isAirportType(m_location.ptr())) {
405 FGAirport* apt = static_cast<FGAirport*>(m_location.ptr());
406 opt->addOption("airport", apt->ident());
408 if (m_ui->runwayRadio->isChecked()) {
409 int index = m_ui->runwayCombo->itemData(m_ui->runwayCombo->currentIndex()).toInt();
411 // explicit runway choice
412 FGRunwayRef runway = apt->getRunwayByIndex(index);
413 opt->addOption("runway", runway->ident());
415 // set nav-radio 1 based on selected runway
417 double mhz = runway->ILS()->get_freq() / 100.0;
418 QString navOpt = QString("%1:%2").arg(runway->headingDeg()).arg(mhz);
419 opt->addOption("nav1", navOpt.toStdString());
423 if (m_ui->onFinalCheckbox->isChecked()) {
424 opt->addOption("glideslope", "3.0");
425 double offsetNm = m_ui->approachDistanceSpin->value();
426 opt->addOption("offset-distance", QString::number(offsetNm).toStdString());
429 } else if (m_ui->parkingRadio->isChecked()) {
431 opt->addOption("parkpos", m_ui->parkingCombo->currentText().toStdString());
433 // of location is an airport
435 // location is a navaid
436 // note setting the ident here is ambigious, we really only need and
437 // want the 'navaid-id' property. However setting the 'real' option
438 // gives a better UI experience (eg existing Position in Air dialog)
439 FGPositioned::Type ty = m_location->type();
441 case FGPositioned::VOR:
442 opt->addOption("vor", m_location->ident());
446 case FGPositioned::NDB:
447 opt->addOption("ndb", m_location->ident());
451 case FGPositioned::FIX:
452 opt->addOption("fix", m_location->ident());
458 opt->addOption("altitude", altStr);
459 opt->addOption("vc", vcStr);
461 // set disambiguation property
462 globals->get_props()->setIntValue("/sim/presets/navaid-id",
463 static_cast<int>(m_location->guid()));
468 void LocationWidget::setNavRadioOption()
470 flightgear::Options* opt = flightgear::Options::sharedInstance();
472 if (m_location->type() == FGPositioned::VOR) {
473 FGNavRecordRef nav(static_cast<FGNavRecord*>(m_location.ptr()));
474 double mhz = nav->get_freq() / 100.0;
475 int heading = 0; // add heading support
476 QString navOpt = QString("%1:%2").arg(heading).arg(mhz);
477 opt->addOption("nav1", navOpt.toStdString());
479 FGNavRecordRef nav(static_cast<FGNavRecord*>(m_location.ptr()));
480 int khz = nav->get_freq() / 100;
482 QString adfOpt = QString("%1:%2").arg(heading).arg(khz);
483 qDebug() << "ADF opt is:" << adfOpt;
484 opt->addOption("adf1", adfOpt.toStdString());
488 void LocationWidget::onSearch()
490 QString search = m_ui->locationSearchEdit->text();
492 m_locationIsLatLon = parseStringAsGeod(search, m_geodLocation);
493 if (m_locationIsLatLon) {
494 m_ui->searchIcon->setVisible(false);
495 m_ui->searchStatusText->setText(QString("Position '%1'").arg(formatGeodAsString(m_geodLocation)));
502 m_searchModel->setSearch(search);
504 if (m_searchModel->isSearchActive()) {
505 m_ui->searchStatusText->setText(QString("Searching for '%1'").arg(search));
506 qDebug() << "setting icon visible";
507 m_ui->searchIcon->setVisible(true);
508 m_ui->searchIcon->movie()->start();
509 } else if (m_searchModel->rowCount(QModelIndex()) == 1) {
510 setBaseLocation(m_searchModel->itemAtRow(0));
514 void LocationWidget::onSearchComplete()
516 QString search = m_ui->locationSearchEdit->text();
517 m_ui->searchIcon->setVisible(false);
518 m_ui->searchStatusText->setText(QString("Results for '%1'").arg(search));
520 int numResults = m_searchModel->rowCount(QModelIndex());
521 if (numResults == 0) {
522 m_ui->searchStatusText->setText(QString("No matches for '%1'").arg(search));
523 } else if (numResults == 1) {
524 setBaseLocation(m_searchModel->itemAtRow(0));
528 void LocationWidget::onLocationChanged()
530 bool locIsAirport = FGAirport::isAirportType(m_location.ptr());
531 m_backButton->show();
534 m_ui->stack->setCurrentIndex(0);
535 FGAirport* apt = static_cast<FGAirport*>(m_location.ptr());
536 m_ui->airportDiagram->setAirport(apt);
538 m_ui->runwayRadio->setChecked(true); // default back to runway mode
539 // unless multiplayer is enabled ?
540 m_ui->airportDiagram->setEnabled(true);
542 m_ui->runwayCombo->clear();
543 m_ui->runwayCombo->addItem("Automatic", -1);
544 for (unsigned int r=0; r<apt->numRunways(); ++r) {
545 FGRunwayRef rwy = apt->getRunwayByIndex(r);
546 // add runway with index as data role
547 m_ui->runwayCombo->addItem(QString::fromStdString(rwy->ident()), r);
549 m_ui->airportDiagram->addRunway(rwy);
552 m_ui->parkingCombo->clear();
553 FGAirportDynamics* dynamics = apt->getDynamics();
554 PositionedIDVec parkings = NavDataCache::instance()->airportItemsOfType(m_location->guid(),
555 FGPositioned::PARKING);
556 if (parkings.empty()) {
557 m_ui->parkingCombo->setEnabled(false);
558 m_ui->parkingRadio->setEnabled(false);
560 m_ui->parkingCombo->setEnabled(true);
561 m_ui->parkingRadio->setEnabled(true);
562 Q_FOREACH(PositionedID parking, parkings) {
563 FGParking* park = dynamics->getParking(parking);
564 m_ui->parkingCombo->addItem(QString::fromStdString(park->getName()),
565 static_cast<qlonglong>(parking));
567 m_ui->airportDiagram->addParking(park);
571 } else if (m_locationIsLatLon) {
572 m_ui->stack->setCurrentIndex(1);
573 m_ui->navaidDiagram->setGeod(m_geodLocation);
576 m_ui->stack->setCurrentIndex(1);
577 m_ui->navaidDiagram->setNavaid(m_location);
581 void LocationWidget::onOffsetEnabledToggled(bool on)
583 m_ui->navaidDiagram->setOffsetEnabled(on);
586 void LocationWidget::onAirportDiagramClicked(FGRunwayRef rwy)
589 m_ui->runwayRadio->setChecked(true);
590 int rwyIndex = m_ui->runwayCombo->findText(QString::fromStdString(rwy->ident()));
591 m_ui->runwayCombo->setCurrentIndex(rwyIndex);
592 m_ui->airportDiagram->setSelectedRunway(rwy);
598 QString LocationWidget::locationDescription() const
601 if (m_locationIsLatLon) {
602 return QString("at position %1").arg(formatGeodAsString(m_geodLocation));
605 return QString("No location selected");
608 bool locIsAirport = FGAirport::isAirportType(m_location.ptr());
609 QString ident = QString::fromStdString(m_location->ident()),
610 name = QString::fromStdString(m_location->name());
612 name = fixNavaidName(name);
615 //FGAirport* apt = static_cast<FGAirport*>(m_location.ptr());
616 QString locationOnAirport;
618 if (m_ui->runwayRadio->isChecked()) {
619 bool onFinal = m_ui->onFinalCheckbox->isChecked();
620 int comboIndex = m_ui->runwayCombo->currentIndex();
621 QString runwayName = (comboIndex == 0) ?
623 QString("runway %1").arg(m_ui->runwayCombo->currentText());
626 int finalDistance = m_ui->approachDistanceSpin->value();
627 locationOnAirport = QString("on %2-mile final to %1").arg(runwayName).arg(finalDistance);
629 locationOnAirport = QString("on %1").arg(runwayName);
631 } else if (m_ui->parkingRadio->isChecked()) {
632 locationOnAirport = QString("at parking position %1").arg(m_ui->parkingCombo->currentText());
635 return QString("%2 (%1): %3").arg(ident).arg(name).arg(locationOnAirport);
638 switch (m_location->type()) {
639 case FGPositioned::VOR:
640 navaidType = QString("VOR"); break;
641 case FGPositioned::NDB:
642 navaidType = QString("NDB"); break;
643 case FGPositioned::FIX:
644 return QString("at waypoint %1").arg(ident);
650 return QString("at %1 %2 (%3)").arg(navaidType).arg(ident).arg(name);
653 return QString("No location selected");
657 void LocationWidget::updateDescription()
659 bool locIsAirport = FGAirport::isAirportType(m_location.ptr());
661 FGAirport* apt = static_cast<FGAirport*>(m_location.ptr());
663 if (m_ui->runwayRadio->isChecked()) {
664 int comboIndex = m_ui->runwayCombo->currentIndex();
665 int runwayIndex = m_ui->runwayCombo->itemData(comboIndex).toInt();
666 // we can't figure out the active runway in the launcher (yet)
667 FGRunwayRef rwy = (runwayIndex >= 0) ?
668 apt->getRunwayByIndex(runwayIndex) : FGRunwayRef();
669 m_ui->airportDiagram->setSelectedRunway(rwy);
672 if (m_ui->onFinalCheckbox->isChecked()) {
673 m_ui->airportDiagram->setApproachExtensionDistance(m_ui->approachDistanceSpin->value());
675 m_ui->airportDiagram->setApproachExtensionDistance(0.0);
683 QString locationOnAirport;
684 if (m_ui->runwayRadio->isChecked()) {
687 } else if (m_ui->parkingRadio->isChecked()) {
688 locationOnAirport = QString("at parking position %1").arg(m_ui->parkingCombo->currentText());
691 m_ui->airportDescription->setText();
694 emit descriptionChanged(locationDescription());
697 void LocationWidget::onSearchResultSelected(const QModelIndex& index)
699 setBaseLocation(m_searchModel->itemAtRow(index.row()));
702 void LocationWidget::onOffsetBearingTrueChanged(bool on)
704 m_ui->offsetBearingLabel->setText(on ? tr("True bearing:") :
705 tr("Magnetic bearing:"));
709 void LocationWidget::onPopupHistory()
711 if (m_recentAirports.isEmpty()) {
717 Q_FOREACH(QString aptCode, m_recentAirports) {
718 FGAirportRef apt = FGAirport::findByIdent(aptCode.toStdString());
719 QString name = QString::fromStdString(apt->name());
720 QAction* act = m.addAction(QString("%1 - %2").arg(aptCode).arg(name));
721 act->setData(aptCode);
724 QPoint popupPos = m_ui->airportHistory->mapToGlobal(m_ui->airportHistory->rect().bottomLeft());
725 QAction* triggered = m.exec(popupPos);
727 FGAirportRef apt = FGAirport::findByIdent(triggered->data().toString().toStdString());
729 m_ui->airportEdit->clear();
730 m_ui->locationStack->setCurrentIndex(0);
735 void LocationWidget::setBaseLocation(FGPositionedRef ref)
737 if (m_location == ref)
745 // maintain the recent airport list
746 QString icao = QString::fromStdString(ref->ident());
747 if (m_recentAirports.contains(icao)) {
749 m_recentAirports.removeOne(icao);
750 m_recentAirports.push_front(icao);
752 // insert and trim list if necessary
753 m_recentAirports.push_front(icao);
754 if (m_recentAirports.size() > MAX_RECENT_AIRPORTS) {
755 m_recentAirports.pop_back();
763 void LocationWidget::onOffsetDataChanged()
765 m_ui->navaidDiagram->setOffsetEnabled(m_ui->offsetGroup->isChecked());
766 m_ui->navaidDiagram->setOffsetBearingDeg(m_ui->offsetBearingSpinbox->value());
767 m_ui->navaidDiagram->setOffsetDistanceNm(m_ui->offsetNmSpinbox->value());
770 void LocationWidget::onBackToSearch()
772 m_ui->stack->setCurrentIndex(2);
773 m_backButton->hide();
776 #include "LocationWidget.moc"