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>
41 const int MAX_RECENT_AIRPORTS = 32;
43 using namespace flightgear;
45 QString fixNavaidName(QString s)
48 QStringList words = s.split(QChar(' '));
49 QStringList changedWords;
50 Q_FOREACH(QString w, words) {
51 QString up = w.toUpper();
53 // expand common abbreviations
55 changedWords.append("Field");
60 changedWords.append("Municipal");
65 changedWords.append("Regional");
70 changedWords.append("Center");
75 changedWords.append("International");
79 // occurs in many Australian airport names in our DB
81 changedWords.append("(New South Wales)");
85 if ((up == "VOR") || (up == "NDB") || (up == "VOR-DME") || (up == "VORTAC") || (up == "NDB-DME")) {
86 changedWords.append(w);
90 QChar firstChar = w.at(0).toUpper();
91 w = w.mid(1).toLower();
94 changedWords.append(w);
97 return changedWords.join(QChar(' '));
100 QString formatGeodAsString(const SGGeod& geod)
102 QChar ns = (geod.getLatitudeDeg() > 0.0) ? 'N' : 'S';
103 QChar ew = (geod.getLongitudeDeg() > 0.0) ? 'E' : 'W';
105 return QString::number(fabs(geod.getLongitudeDeg()), 'f',2 ) + ew + " " +
106 QString::number(fabs(geod.getLatitudeDeg()), 'f',2 ) + ns;
109 class IdentSearchFilter : public FGPositioned::TypeFilter
114 addType(FGPositioned::AIRPORT);
115 addType(FGPositioned::SEAPORT);
116 addType(FGPositioned::HELIPAD);
117 addType(FGPositioned::VOR);
118 addType(FGPositioned::FIX);
119 addType(FGPositioned::NDB);
123 class NavSearchModel : public QAbstractListModel
128 m_searchActive(false)
132 void setSearch(QString t)
139 std::string term(t.toUpper().toStdString());
141 IdentSearchFilter filter;
142 FGPositionedList exactMatches = NavDataCache::instance()->findAllWithIdent(term, &filter, true);
144 for (unsigned int i=0; i<exactMatches.size(); ++i) {
145 m_ids.push_back(exactMatches[i]->guid());
146 m_items.push_back(exactMatches[i]);
151 m_search.reset(new NavDataCache::ThreadedGUISearch(term));
152 QTimer::singleShot(100, this, &NavSearchModel::onSearchResultsPoll);
153 m_searchActive = true;
157 bool isSearchActive() const
159 return m_searchActive;
162 virtual int rowCount(const QModelIndex&) const
164 // if empty, return 1 for special 'no matches'?
168 virtual QVariant data(const QModelIndex& index, int role) const
170 if (!index.isValid())
173 FGPositionedRef pos = itemAtRow(index.row());
174 if (role == Qt::DisplayRole) {
175 if (pos->type() == FGPositioned::FIX) {
176 // fixes don't have a name, show position instead
177 return QString("Fix %1 (%2)").arg(QString::fromStdString(pos->ident()))
178 .arg(formatGeodAsString(pos->geod()));
180 QString name = fixNavaidName(QString::fromStdString(pos->name()));
181 return QString("%1: %2").arg(QString::fromStdString(pos->ident())).arg(name);
185 if (role == Qt::EditRole) {
186 return QString::fromStdString(pos->ident());
189 if (role == Qt::UserRole) {
190 return static_cast<qlonglong>(m_ids[index.row()]);
196 FGPositionedRef itemAtRow(unsigned int row) const
198 FGPositionedRef pos = m_items[row];
200 pos = NavDataCache::instance()->loadById(m_ids[row]);
207 void searchComplete();
212 void onSearchResultsPoll()
214 PositionedIDVec newIds = m_search->results();
216 beginInsertRows(QModelIndex(), m_ids.size(), newIds.size() - 1);
217 for (unsigned int i=m_ids.size(); i < newIds.size(); ++i) {
218 m_ids.push_back(newIds[i]);
219 m_items.push_back(FGPositionedRef()); // null ref
223 if (m_search->isComplete()) {
224 m_searchActive = false;
226 emit searchComplete();
228 QTimer::singleShot(100, this, &NavSearchModel::onSearchResultsPoll);
233 PositionedIDVec m_ids;
234 mutable FGPositionedList m_items;
236 QScopedPointer<NavDataCache::ThreadedGUISearch> m_search;
240 LocationWidget::LocationWidget(QWidget *parent) :
242 m_ui(new Ui::LocationWidget)
247 QIcon historyIcon(":/history-icon");
248 m_ui->searchHistory->setIcon(historyIcon);
250 m_ui->searchIcon->setPixmap(QPixmap(":/search-icon"));
252 m_searchModel = new NavSearchModel;
253 m_ui->searchResultsList->setModel(m_searchModel);
254 connect(m_ui->searchResultsList, &QListView::clicked,
255 this, &LocationWidget::onSearchResultSelected);
256 connect(m_searchModel, &NavSearchModel::searchComplete,
257 this, &LocationWidget::onSearchComplete);
259 connect(m_ui->runwayCombo, SIGNAL(currentIndexChanged(int)),
260 this, SLOT(updateDescription()));
261 connect(m_ui->parkingCombo, SIGNAL(currentIndexChanged(int)),
262 this, SLOT(updateDescription()));
263 connect(m_ui->runwayRadio, SIGNAL(toggled(bool)),
264 this, SLOT(updateDescription()));
265 connect(m_ui->parkingRadio, SIGNAL(toggled(bool)),
266 this, SLOT(updateDescription()));
267 connect(m_ui->onFinalCheckbox, SIGNAL(toggled(bool)),
268 this, SLOT(updateDescription()));
269 connect(m_ui->approachDistanceSpin, SIGNAL(valueChanged(int)),
270 this, SLOT(updateDescription()));
272 connect(m_ui->airportDiagram, &AirportDiagram::clickedRunway,
273 this, &LocationWidget::onAirportDiagramClicked);
275 connect(m_ui->locationSearchEdit, &QLineEdit::returnPressed,
276 this, &LocationWidget::onSearch);
278 connect(m_ui->searchHistory, &QPushButton::clicked,
279 this, &LocationWidget::onPopupHistory);
281 connect(m_ui->trueBearing, &QCheckBox::toggled,
282 this, &LocationWidget::onOffsetBearingTrueChanged);
283 connect(m_ui->offsetGroup, &QGroupBox::toggled,
284 this, &LocationWidget::onOffsetEnabledToggled);
285 connect(m_ui->trueBearing, &QCheckBox::toggled, this,
286 &LocationWidget::onOffsetDataChanged);
287 connect(m_ui->offsetBearingSpinbox, SIGNAL(valueChanged(int)),
288 this, SLOT(onOffsetDataChanged()));
289 connect(m_ui->offsetNmSpinbox, SIGNAL(valueChanged(double)),
290 this, SLOT(onOffsetDataChanged()));
292 m_backButton = new QToolButton(this);
293 m_backButton->setGeometry(0, 0, 32, 32);
294 m_backButton->setIcon(QIcon(":/search-icon"));
295 m_backButton->raise();
297 connect(m_backButton, &QAbstractButton::clicked,
298 this, &LocationWidget::onBackToSearch);
300 // force various pieces of UI into sync
301 onOffsetEnabledToggled(m_ui->offsetGroup->isChecked());
305 LocationWidget::~LocationWidget()
310 void LocationWidget::restoreSettings()
313 Q_FOREACH(QVariant v, settings.value("recent-locations").toList()) {
314 m_recentAirports.push_back(v.toLongLong());
317 if (!m_recentAirports.empty()) {
318 setBaseLocation(NavDataCache::instance()->loadById(m_recentAirports.front()));
324 bool LocationWidget::shouldStartPaused() const
326 qWarning() << Q_FUNC_INFO << "implement me";
330 void LocationWidget::saveSettings()
334 QVariantList locations;
335 Q_FOREACH(PositionedID v, m_recentAirports) {
336 locations.push_back(v);
339 settings.setValue("recent-airports", locations);
342 void LocationWidget::setLocationOptions()
344 flightgear::Options* opt = flightgear::Options::sharedInstance();
350 if (FGAirport::isAirportType(m_location.ptr())) {
351 FGAirport* apt = static_cast<FGAirport*>(m_location.ptr());
352 opt->addOption("airport", apt->ident());
354 if (m_ui->runwayRadio->isChecked()) {
355 int index = m_ui->runwayCombo->itemData(m_ui->runwayCombo->currentIndex()).toInt();
357 // explicit runway choice
358 opt->addOption("runway", apt->getRunwayByIndex(index)->ident());
361 if (m_ui->onFinalCheckbox->isChecked()) {
362 opt->addOption("glideslope", "3.0");
363 opt->addOption("offset-distance", "10.0"); // in nautical miles
365 } else if (m_ui->parkingRadio->isChecked()) {
367 opt->addOption("parkpos", m_ui->parkingCombo->currentText().toStdString());
369 // of location is an airport
372 FGPositioned::Type ty = m_location->type();
374 case FGPositioned::VOR:
375 case FGPositioned::NDB:
376 case FGPositioned::FIX:
377 // set disambiguation property
378 globals->get_props()->setIntValue("/sim/presets/navaid-id",
379 static_cast<int>(m_location->guid()));
381 // we always set 'fix', but really this is just to force positionInit
382 // code to check for the navaid-id value above.
383 opt->addOption("fix", m_location->ident());
390 void LocationWidget::onSearch()
392 QString search = m_ui->locationSearchEdit->text();
393 m_searchModel->setSearch(search);
395 if (m_searchModel->isSearchActive()) {
396 m_ui->searchStatusText->setText(QString("Searching for '%1'").arg(search));
397 m_ui->searchIcon->setVisible(true);
398 } else if (m_searchModel->rowCount(QModelIndex()) == 1) {
399 setBaseLocation(m_searchModel->itemAtRow(0));
403 void LocationWidget::onSearchComplete()
405 QString search = m_ui->locationSearchEdit->text();
406 m_ui->searchIcon->setVisible(false);
407 m_ui->searchStatusText->setText(QString("Results for '%1'").arg(search));
409 int numResults = m_searchModel->rowCount(QModelIndex());
410 if (numResults == 0) {
411 m_ui->searchStatusText->setText(QString("No matches for '%1'").arg(search));
412 } else if (numResults == 1) {
413 setBaseLocation(m_searchModel->itemAtRow(0));
417 void LocationWidget::onLocationChanged()
419 bool locIsAirport = FGAirport::isAirportType(m_location.ptr());
420 m_backButton->show();
423 m_ui->stack->setCurrentIndex(0);
424 FGAirport* apt = static_cast<FGAirport*>(m_location.ptr());
425 m_ui->airportDiagram->setAirport(apt);
427 m_ui->runwayRadio->setChecked(true); // default back to runway mode
428 // unless multiplayer is enabled ?
429 m_ui->airportDiagram->setEnabled(true);
431 m_ui->runwayCombo->clear();
432 m_ui->runwayCombo->addItem("Automatic", -1);
433 for (unsigned int r=0; r<apt->numRunways(); ++r) {
434 FGRunwayRef rwy = apt->getRunwayByIndex(r);
435 // add runway with index as data role
436 m_ui->runwayCombo->addItem(QString::fromStdString(rwy->ident()), r);
438 m_ui->airportDiagram->addRunway(rwy);
441 m_ui->parkingCombo->clear();
442 FGAirportDynamics* dynamics = apt->getDynamics();
443 PositionedIDVec parkings = NavDataCache::instance()->airportItemsOfType(m_location->guid(),
444 FGPositioned::PARKING);
445 if (parkings.empty()) {
446 m_ui->parkingCombo->setEnabled(false);
447 m_ui->parkingRadio->setEnabled(false);
449 m_ui->parkingCombo->setEnabled(true);
450 m_ui->parkingRadio->setEnabled(true);
451 Q_FOREACH(PositionedID parking, parkings) {
452 FGParking* park = dynamics->getParking(parking);
453 m_ui->parkingCombo->addItem(QString::fromStdString(park->getName()),
454 static_cast<qlonglong>(parking));
456 m_ui->airportDiagram->addParking(park);
461 } else {// of location is airport
463 m_ui->stack->setCurrentIndex(1);
464 m_ui->navaidDiagram->setNavaid(m_location);
468 void LocationWidget::onOffsetEnabledToggled(bool on)
470 m_ui->offsetDistanceLabel->setEnabled(on);
471 // m_ui->offsetNmSpinbox->setEnabled(on);
472 // m_ui->offsetBearingSpinbox->setEnabled(on);
473 // m_ui->trueBearing->setEnabled(on);
474 // m_ui->offsetBearingLabel->setEnabled(on);
475 // m_ui->offsetDistanceLabel->setEnabled(on);
478 void LocationWidget::onAirportDiagramClicked(FGRunwayRef rwy)
481 m_ui->runwayRadio->setChecked(true);
482 int rwyIndex = m_ui->runwayCombo->findText(QString::fromStdString(rwy->ident()));
483 m_ui->runwayCombo->setCurrentIndex(rwyIndex);
484 m_ui->airportDiagram->setSelectedRunway(rwy);
490 QString LocationWidget::locationDescription() const
493 return QString("No location selected");
495 bool locIsAirport = FGAirport::isAirportType(m_location.ptr());
496 QString ident = QString::fromStdString(m_location->ident()),
497 name = QString::fromStdString(m_location->name());
500 FGAirport* apt = static_cast<FGAirport*>(m_location.ptr());
501 QString locationOnAirport;
503 if (m_ui->runwayRadio->isChecked()) {
504 bool onFinal = m_ui->onFinalCheckbox->isChecked();
505 int comboIndex = m_ui->runwayCombo->currentIndex();
506 QString runwayName = (comboIndex == 0) ?
508 QString("runway %1").arg(m_ui->runwayCombo->currentText());
511 int finalDistance = m_ui->approachDistanceSpin->value();
512 locationOnAirport = QString("on %2-mile final to %1").arg(runwayName).arg(finalDistance);
514 locationOnAirport = QString("on %1").arg(runwayName);
516 } else if (m_ui->parkingRadio->isChecked()) {
517 locationOnAirport = QString("at parking position %1").arg(m_ui->parkingCombo->currentText());
520 return QString("%2 (%1): %3").arg(ident).arg(name).arg(locationOnAirport);
523 switch (m_location->type()) {
524 case FGPositioned::VOR:
525 navaidType = QString("VOR"); break;
526 case FGPositioned::NDB:
527 navaidType = QString("NDB"); break;
528 case FGPositioned::FIX:
529 return QString("at waypoint %1").arg(ident);
535 return QString("at %1 %2 (%3").arg(navaidType).arg(ident).arg(name);
538 return QString("Implement Me");
542 void LocationWidget::updateDescription()
544 bool locIsAirport = FGAirport::isAirportType(m_location.ptr());
546 FGAirport* apt = static_cast<FGAirport*>(m_location.ptr());
548 if (m_ui->runwayRadio->isChecked()) {
549 int comboIndex = m_ui->runwayCombo->currentIndex();
550 int runwayIndex = m_ui->runwayCombo->itemData(comboIndex).toInt();
551 // we can't figure out the active runway in the launcher (yet)
552 FGRunwayRef rwy = (runwayIndex >= 0) ?
553 apt->getRunwayByIndex(runwayIndex) : FGRunwayRef();
554 m_ui->airportDiagram->setSelectedRunway(rwy);
557 if (m_ui->onFinalCheckbox->isChecked()) {
558 m_ui->airportDiagram->setApproachExtensionDistance(m_ui->approachDistanceSpin->value());
560 m_ui->airportDiagram->setApproachExtensionDistance(0.0);
568 QString locationOnAirport;
569 if (m_ui->runwayRadio->isChecked()) {
572 } else if (m_ui->parkingRadio->isChecked()) {
573 locationOnAirport = QString("at parking position %1").arg(m_ui->parkingCombo->currentText());
576 m_ui->airportDescription->setText();
579 emit descriptionChanged(locationDescription());
582 void LocationWidget::onSearchResultSelected(const QModelIndex& index)
584 qDebug() << "selected result:" << index.data();
585 setBaseLocation(m_searchModel->itemAtRow(index.row()));
588 void LocationWidget::onOffsetBearingTrueChanged(bool on)
590 m_ui->offsetBearingLabel->setText(on ? tr("True bearing:") :
591 tr("Magnetic bearing:"));
595 void LocationWidget::onPopupHistory()
597 if (m_recentAirports.isEmpty()) {
603 Q_FOREACH(QString aptCode, m_recentAirports) {
604 FGAirportRef apt = FGAirport::findByIdent(aptCode.toStdString());
605 QString name = QString::fromStdString(apt->name());
606 QAction* act = m.addAction(QString("%1 - %2").arg(aptCode).arg(name));
607 act->setData(aptCode);
610 QPoint popupPos = m_ui->airportHistory->mapToGlobal(m_ui->airportHistory->rect().bottomLeft());
611 QAction* triggered = m.exec(popupPos);
613 FGAirportRef apt = FGAirport::findByIdent(triggered->data().toString().toStdString());
615 m_ui->airportEdit->clear();
616 m_ui->locationStack->setCurrentIndex(0);
621 void LocationWidget::setBaseLocation(FGPositionedRef ref)
623 if (m_location == ref)
631 // maintain the recent airport list
632 QString icao = QString::fromStdString(ref->ident());
633 if (m_recentAirports.contains(icao)) {
635 m_recentAirports.removeOne(icao);
636 m_recentAirports.push_front(icao);
638 // insert and trim list if necessary
639 m_recentAirports.push_front(icao);
640 if (m_recentAirports.size() > MAX_RECENT_AIRPORTS) {
641 m_recentAirports.pop_back();
649 void LocationWidget::onOffsetDataChanged()
651 qDebug() << "implement me";
654 void LocationWidget::onBackToSearch()
656 m_ui->stack->setCurrentIndex(2);
657 m_backButton->hide();
660 #include "LocationWidget.moc"