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>
30 #include "AirportDiagram.hxx"
31 #include "NavaidDiagram.hxx"
33 #include <Airports/airport.hxx>
34 #include <Airports/dynamics.hxx> // for parking
35 #include <Main/globals.hxx>
36 #include <Navaids/NavDataCache.hxx>
37 #include <Navaids/navrecord.hxx>
38 #include <Main/options.hxx>
39 #include <Main/fg_init.hxx>
40 #include <Main/fg_props.hxx> // for fgSetDouble
42 const int MAX_RECENT_AIRPORTS = 32;
44 using namespace flightgear;
46 QString fixNavaidName(QString s)
49 QStringList words = s.split(QChar(' '));
50 QStringList changedWords;
51 Q_FOREACH(QString w, words) {
52 QString up = w.toUpper();
54 // expand common abbreviations
56 changedWords.append("Field");
61 changedWords.append("Municipal");
66 changedWords.append("Regional");
71 changedWords.append("Center");
76 changedWords.append("International");
80 // occurs in many Australian airport names in our DB
82 changedWords.append("(New South Wales)");
86 if ((up == "VOR") || (up == "NDB") || (up == "VOR-DME") || (up == "VORTAC") || (up == "NDB-DME")) {
87 changedWords.append(w);
91 QChar firstChar = w.at(0).toUpper();
92 w = w.mid(1).toLower();
95 changedWords.append(w);
98 return changedWords.join(QChar(' '));
101 QString formatGeodAsString(const SGGeod& geod)
103 QChar ns = (geod.getLatitudeDeg() > 0.0) ? 'N' : 'S';
104 QChar ew = (geod.getLongitudeDeg() > 0.0) ? 'E' : 'W';
106 return QString::number(fabs(geod.getLongitudeDeg()), 'f',2 ) + ew + " " +
107 QString::number(fabs(geod.getLatitudeDeg()), 'f',2 ) + ns;
110 bool parseStringAsGeod(const QString& s, SGGeod& result)
112 int commaPos = s.indexOf(QChar(','));
117 double lon = s.leftRef(commaPos).toDouble(&ok);
121 double lat = s.midRef(commaPos+1).toDouble(&ok);
125 result = SGGeod::fromDeg(lon, lat);
129 class IdentSearchFilter : public FGPositioned::TypeFilter
134 addType(FGPositioned::AIRPORT);
135 addType(FGPositioned::SEAPORT);
136 addType(FGPositioned::HELIPAD);
137 addType(FGPositioned::VOR);
138 addType(FGPositioned::FIX);
139 addType(FGPositioned::NDB);
143 class NavSearchModel : public QAbstractListModel
148 m_searchActive(false)
152 void setSearch(QString t)
159 std::string term(t.toUpper().toStdString());
161 IdentSearchFilter filter;
162 FGPositionedList exactMatches = NavDataCache::instance()->findAllWithIdent(term, &filter, true);
164 for (unsigned int i=0; i<exactMatches.size(); ++i) {
165 m_ids.push_back(exactMatches[i]->guid());
166 m_items.push_back(exactMatches[i]);
171 m_search.reset(new NavDataCache::ThreadedGUISearch(term));
172 QTimer::singleShot(100, this, &NavSearchModel::onSearchResultsPoll);
173 m_searchActive = true;
177 bool isSearchActive() const
179 return m_searchActive;
182 virtual int rowCount(const QModelIndex&) const
184 // if empty, return 1 for special 'no matches'?
188 virtual QVariant data(const QModelIndex& index, int role) const
190 if (!index.isValid())
193 FGPositionedRef pos = itemAtRow(index.row());
194 if (role == Qt::DisplayRole) {
195 if (pos->type() == FGPositioned::FIX) {
196 // fixes don't have a name, show position instead
197 return QString("Fix %1 (%2)").arg(QString::fromStdString(pos->ident()))
198 .arg(formatGeodAsString(pos->geod()));
200 QString name = fixNavaidName(QString::fromStdString(pos->name()));
201 return QString("%1: %2").arg(QString::fromStdString(pos->ident())).arg(name);
205 if (role == Qt::EditRole) {
206 return QString::fromStdString(pos->ident());
209 if (role == Qt::UserRole) {
210 return static_cast<qlonglong>(m_ids[index.row()]);
216 FGPositionedRef itemAtRow(unsigned int row) const
218 FGPositionedRef pos = m_items[row];
220 pos = NavDataCache::instance()->loadById(m_ids[row]);
227 void searchComplete();
232 void onSearchResultsPoll()
234 PositionedIDVec newIds = m_search->results();
236 beginInsertRows(QModelIndex(), m_ids.size(), newIds.size() - 1);
237 for (unsigned int i=m_ids.size(); i < newIds.size(); ++i) {
238 m_ids.push_back(newIds[i]);
239 m_items.push_back(FGPositionedRef()); // null ref
243 if (m_search->isComplete()) {
244 m_searchActive = false;
246 emit searchComplete();
248 QTimer::singleShot(100, this, &NavSearchModel::onSearchResultsPoll);
253 PositionedIDVec m_ids;
254 mutable FGPositionedList m_items;
256 QScopedPointer<NavDataCache::ThreadedGUISearch> m_search;
260 LocationWidget::LocationWidget(QWidget *parent) :
262 m_ui(new Ui::LocationWidget)
267 QIcon historyIcon(":/history-icon");
268 m_ui->searchHistory->setIcon(historyIcon);
270 m_ui->searchIcon->setPixmap(QPixmap(":/search-icon"));
272 m_searchModel = new NavSearchModel;
273 m_ui->searchResultsList->setModel(m_searchModel);
274 connect(m_ui->searchResultsList, &QListView::clicked,
275 this, &LocationWidget::onSearchResultSelected);
276 connect(m_searchModel, &NavSearchModel::searchComplete,
277 this, &LocationWidget::onSearchComplete);
279 connect(m_ui->runwayCombo, SIGNAL(currentIndexChanged(int)),
280 this, SLOT(updateDescription()));
281 connect(m_ui->parkingCombo, SIGNAL(currentIndexChanged(int)),
282 this, SLOT(updateDescription()));
283 connect(m_ui->runwayRadio, SIGNAL(toggled(bool)),
284 this, SLOT(updateDescription()));
285 connect(m_ui->parkingRadio, SIGNAL(toggled(bool)),
286 this, SLOT(updateDescription()));
287 connect(m_ui->onFinalCheckbox, SIGNAL(toggled(bool)),
288 this, SLOT(updateDescription()));
289 connect(m_ui->approachDistanceSpin, SIGNAL(valueChanged(int)),
290 this, SLOT(updateDescription()));
292 connect(m_ui->airportDiagram, &AirportDiagram::clickedRunway,
293 this, &LocationWidget::onAirportDiagramClicked);
295 connect(m_ui->locationSearchEdit, &QLineEdit::returnPressed,
296 this, &LocationWidget::onSearch);
298 connect(m_ui->searchHistory, &QPushButton::clicked,
299 this, &LocationWidget::onPopupHistory);
301 connect(m_ui->trueBearing, &QCheckBox::toggled,
302 this, &LocationWidget::onOffsetBearingTrueChanged);
303 connect(m_ui->offsetGroup, &QGroupBox::toggled,
304 this, &LocationWidget::onOffsetEnabledToggled);
305 connect(m_ui->trueBearing, &QCheckBox::toggled, this,
306 &LocationWidget::onOffsetDataChanged);
307 connect(m_ui->offsetBearingSpinbox, SIGNAL(valueChanged(int)),
308 this, SLOT(onOffsetDataChanged()));
309 connect(m_ui->offsetNmSpinbox, SIGNAL(valueChanged(double)),
310 this, SLOT(onOffsetDataChanged()));
312 m_backButton = new QToolButton(this);
313 m_backButton->setGeometry(0, 0, 32, 32);
314 m_backButton->setIcon(QIcon(":/search-icon"));
315 m_backButton->raise();
317 connect(m_backButton, &QAbstractButton::clicked,
318 this, &LocationWidget::onBackToSearch);
320 // force various pieces of UI into sync
321 onOffsetEnabledToggled(m_ui->offsetGroup->isChecked());
322 onOffsetBearingTrueChanged(m_ui->trueBearing->isChecked());
326 LocationWidget::~LocationWidget()
331 void LocationWidget::restoreSettings()
334 Q_FOREACH(QVariant v, settings.value("recent-locations").toList()) {
335 m_recentAirports.push_back(v.toLongLong());
338 if (!m_recentAirports.empty()) {
339 setBaseLocation(NavDataCache::instance()->loadById(m_recentAirports.front()));
345 bool LocationWidget::shouldStartPaused() const
348 return false; // defaults to on-ground at KSFO
351 if (FGAirport::isAirportType(m_location.ptr())) {
352 return m_ui->onFinalCheckbox->isChecked();
354 // navaid, start paused
361 void LocationWidget::saveSettings()
365 QVariantList locations;
366 Q_FOREACH(PositionedID v, m_recentAirports) {
367 locations.push_back(v);
370 settings.setValue("recent-airports", locations);
373 void LocationWidget::setLocationOptions()
375 flightgear::Options* opt = flightgear::Options::sharedInstance();
377 if (m_locationIsLatLon) {
378 // bypass the options mechanism because converting to deg:min:sec notation
379 // just to parse back again is nasty.
380 fgSetDouble("/sim/presets/latitude-deg", m_geodLocation.getLatitudeDeg());
381 fgSetDouble("/position/latitude-deg", m_geodLocation.getLatitudeDeg());
382 fgSetDouble("/sim/presets/longitude-deg", m_geodLocation.getLongitudeDeg());
383 fgSetDouble("/position/longitude-deg", m_geodLocation.getLongitudeDeg());
391 if (FGAirport::isAirportType(m_location.ptr())) {
392 FGAirport* apt = static_cast<FGAirport*>(m_location.ptr());
393 opt->addOption("airport", apt->ident());
395 if (m_ui->runwayRadio->isChecked()) {
396 int index = m_ui->runwayCombo->itemData(m_ui->runwayCombo->currentIndex()).toInt();
398 // explicit runway choice
399 opt->addOption("runway", apt->getRunwayByIndex(index)->ident());
402 if (m_ui->onFinalCheckbox->isChecked()) {
403 opt->addOption("glideslope", "3.0");
404 opt->addOption("offset-distance", "10.0"); // in nautical miles
406 } else if (m_ui->parkingRadio->isChecked()) {
408 opt->addOption("parkpos", m_ui->parkingCombo->currentText().toStdString());
410 // of location is an airport
413 FGPositioned::Type ty = m_location->type();
415 case FGPositioned::VOR:
416 case FGPositioned::NDB:
417 case FGPositioned::FIX:
418 // set disambiguation property
419 globals->get_props()->setIntValue("/sim/presets/navaid-id",
420 static_cast<int>(m_location->guid()));
422 // we always set 'fix', but really this is just to force positionInit
423 // code to check for the navaid-id value above.
424 opt->addOption("fix", m_location->ident());
431 void LocationWidget::onSearch()
433 QString search = m_ui->locationSearchEdit->text();
435 m_locationIsLatLon = parseStringAsGeod(search, m_geodLocation);
436 if (m_locationIsLatLon) {
437 m_ui->searchIcon->setVisible(false);
438 m_ui->searchStatusText->setText(QString("Position '%1'").arg(formatGeodAsString(m_geodLocation)));
445 m_searchModel->setSearch(search);
447 if (m_searchModel->isSearchActive()) {
448 m_ui->searchStatusText->setText(QString("Searching for '%1'").arg(search));
449 m_ui->searchIcon->setVisible(true);
450 } else if (m_searchModel->rowCount(QModelIndex()) == 1) {
451 setBaseLocation(m_searchModel->itemAtRow(0));
455 void LocationWidget::onSearchComplete()
457 QString search = m_ui->locationSearchEdit->text();
458 m_ui->searchIcon->setVisible(false);
459 m_ui->searchStatusText->setText(QString("Results for '%1'").arg(search));
461 int numResults = m_searchModel->rowCount(QModelIndex());
462 if (numResults == 0) {
463 m_ui->searchStatusText->setText(QString("No matches for '%1'").arg(search));
464 } else if (numResults == 1) {
465 setBaseLocation(m_searchModel->itemAtRow(0));
469 void LocationWidget::onLocationChanged()
471 bool locIsAirport = FGAirport::isAirportType(m_location.ptr());
472 m_backButton->show();
475 m_ui->stack->setCurrentIndex(0);
476 FGAirport* apt = static_cast<FGAirport*>(m_location.ptr());
477 m_ui->airportDiagram->setAirport(apt);
479 m_ui->runwayRadio->setChecked(true); // default back to runway mode
480 // unless multiplayer is enabled ?
481 m_ui->airportDiagram->setEnabled(true);
483 m_ui->runwayCombo->clear();
484 m_ui->runwayCombo->addItem("Automatic", -1);
485 for (unsigned int r=0; r<apt->numRunways(); ++r) {
486 FGRunwayRef rwy = apt->getRunwayByIndex(r);
487 // add runway with index as data role
488 m_ui->runwayCombo->addItem(QString::fromStdString(rwy->ident()), r);
490 m_ui->airportDiagram->addRunway(rwy);
493 m_ui->parkingCombo->clear();
494 FGAirportDynamics* dynamics = apt->getDynamics();
495 PositionedIDVec parkings = NavDataCache::instance()->airportItemsOfType(m_location->guid(),
496 FGPositioned::PARKING);
497 if (parkings.empty()) {
498 m_ui->parkingCombo->setEnabled(false);
499 m_ui->parkingRadio->setEnabled(false);
501 m_ui->parkingCombo->setEnabled(true);
502 m_ui->parkingRadio->setEnabled(true);
503 Q_FOREACH(PositionedID parking, parkings) {
504 FGParking* park = dynamics->getParking(parking);
505 m_ui->parkingCombo->addItem(QString::fromStdString(park->getName()),
506 static_cast<qlonglong>(parking));
508 m_ui->airportDiagram->addParking(park);
512 } else if (m_locationIsLatLon) {
513 m_ui->stack->setCurrentIndex(1);
514 m_ui->navaidDiagram->setGeod(m_geodLocation);
517 m_ui->stack->setCurrentIndex(1);
518 m_ui->navaidDiagram->setNavaid(m_location);
522 void LocationWidget::onOffsetEnabledToggled(bool on)
524 m_ui->navaidDiagram->setOffsetEnabled(on);
527 void LocationWidget::onAirportDiagramClicked(FGRunwayRef rwy)
530 m_ui->runwayRadio->setChecked(true);
531 int rwyIndex = m_ui->runwayCombo->findText(QString::fromStdString(rwy->ident()));
532 m_ui->runwayCombo->setCurrentIndex(rwyIndex);
533 m_ui->airportDiagram->setSelectedRunway(rwy);
539 QString LocationWidget::locationDescription() const
542 if (m_locationIsLatLon) {
543 return QString("at position %1").arg(formatGeodAsString(m_geodLocation));
546 return QString("No location selected");
549 bool locIsAirport = FGAirport::isAirportType(m_location.ptr());
550 QString ident = QString::fromStdString(m_location->ident()),
551 name = QString::fromStdString(m_location->name());
553 name = fixNavaidName(name);
556 //FGAirport* apt = static_cast<FGAirport*>(m_location.ptr());
557 QString locationOnAirport;
559 if (m_ui->runwayRadio->isChecked()) {
560 bool onFinal = m_ui->onFinalCheckbox->isChecked();
561 int comboIndex = m_ui->runwayCombo->currentIndex();
562 QString runwayName = (comboIndex == 0) ?
564 QString("runway %1").arg(m_ui->runwayCombo->currentText());
567 int finalDistance = m_ui->approachDistanceSpin->value();
568 locationOnAirport = QString("on %2-mile final to %1").arg(runwayName).arg(finalDistance);
570 locationOnAirport = QString("on %1").arg(runwayName);
572 } else if (m_ui->parkingRadio->isChecked()) {
573 locationOnAirport = QString("at parking position %1").arg(m_ui->parkingCombo->currentText());
576 return QString("%2 (%1): %3").arg(ident).arg(name).arg(locationOnAirport);
579 switch (m_location->type()) {
580 case FGPositioned::VOR:
581 navaidType = QString("VOR"); break;
582 case FGPositioned::NDB:
583 navaidType = QString("NDB"); break;
584 case FGPositioned::FIX:
585 return QString("at waypoint %1").arg(ident);
591 return QString("at %1 %2 (%3)").arg(navaidType).arg(ident).arg(name);
594 return QString("No location selected");
598 void LocationWidget::updateDescription()
600 bool locIsAirport = FGAirport::isAirportType(m_location.ptr());
602 FGAirport* apt = static_cast<FGAirport*>(m_location.ptr());
604 if (m_ui->runwayRadio->isChecked()) {
605 int comboIndex = m_ui->runwayCombo->currentIndex();
606 int runwayIndex = m_ui->runwayCombo->itemData(comboIndex).toInt();
607 // we can't figure out the active runway in the launcher (yet)
608 FGRunwayRef rwy = (runwayIndex >= 0) ?
609 apt->getRunwayByIndex(runwayIndex) : FGRunwayRef();
610 m_ui->airportDiagram->setSelectedRunway(rwy);
613 if (m_ui->onFinalCheckbox->isChecked()) {
614 m_ui->airportDiagram->setApproachExtensionDistance(m_ui->approachDistanceSpin->value());
616 m_ui->airportDiagram->setApproachExtensionDistance(0.0);
624 QString locationOnAirport;
625 if (m_ui->runwayRadio->isChecked()) {
628 } else if (m_ui->parkingRadio->isChecked()) {
629 locationOnAirport = QString("at parking position %1").arg(m_ui->parkingCombo->currentText());
632 m_ui->airportDescription->setText();
635 emit descriptionChanged(locationDescription());
638 void LocationWidget::onSearchResultSelected(const QModelIndex& index)
640 setBaseLocation(m_searchModel->itemAtRow(index.row()));
643 void LocationWidget::onOffsetBearingTrueChanged(bool on)
645 m_ui->offsetBearingLabel->setText(on ? tr("True bearing:") :
646 tr("Magnetic bearing:"));
650 void LocationWidget::onPopupHistory()
652 if (m_recentAirports.isEmpty()) {
658 Q_FOREACH(QString aptCode, m_recentAirports) {
659 FGAirportRef apt = FGAirport::findByIdent(aptCode.toStdString());
660 QString name = QString::fromStdString(apt->name());
661 QAction* act = m.addAction(QString("%1 - %2").arg(aptCode).arg(name));
662 act->setData(aptCode);
665 QPoint popupPos = m_ui->airportHistory->mapToGlobal(m_ui->airportHistory->rect().bottomLeft());
666 QAction* triggered = m.exec(popupPos);
668 FGAirportRef apt = FGAirport::findByIdent(triggered->data().toString().toStdString());
670 m_ui->airportEdit->clear();
671 m_ui->locationStack->setCurrentIndex(0);
676 void LocationWidget::setBaseLocation(FGPositionedRef ref)
678 if (m_location == ref)
686 // maintain the recent airport list
687 QString icao = QString::fromStdString(ref->ident());
688 if (m_recentAirports.contains(icao)) {
690 m_recentAirports.removeOne(icao);
691 m_recentAirports.push_front(icao);
693 // insert and trim list if necessary
694 m_recentAirports.push_front(icao);
695 if (m_recentAirports.size() > MAX_RECENT_AIRPORTS) {
696 m_recentAirports.pop_back();
704 void LocationWidget::onOffsetDataChanged()
706 m_ui->navaidDiagram->setOffsetEnabled(m_ui->offsetGroup->isChecked());
707 m_ui->navaidDiagram->setOffsetBearingDeg(m_ui->offsetBearingSpinbox->value());
708 m_ui->navaidDiagram->setOffsetDistanceNm(m_ui->offsetNmSpinbox->value());
711 void LocationWidget::onBackToSearch()
713 m_ui->stack->setCurrentIndex(2);
714 m_backButton->hide();
717 #include "LocationWidget.moc"