]> git.mxchange.org Git - flightgear.git/blob - src/GUI/LocationWidget.cxx
Start-paused for in-air starts
[flightgear.git] / src / GUI / LocationWidget.cxx
1 // LocationWidget.cxx - GUI launcher dialog using Qt5
2 //
3 // Written by James Turner, started October 2015.
4 //
5 // Copyright (C) 2015 James Turner <zakalawe@mac.com>
6 //
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.
11 //
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.
16 //
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.
20
21 #include "LocationWidget.hxx"
22 #include "ui_LocationWidget.h"
23
24 #include <QSettings>
25 #include <QAbstractListModel>
26 #include <QTimer>
27 #include <QDebug>
28 #include <QToolButton>
29
30 #include "AirportDiagram.hxx"
31 #include "NavaidDiagram.hxx"
32
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
41 const int MAX_RECENT_AIRPORTS = 32;
42
43 using namespace flightgear;
44
45 QString fixNavaidName(QString s)
46 {
47     // split into words
48     QStringList words = s.split(QChar(' '));
49     QStringList changedWords;
50     Q_FOREACH(QString w, words) {
51         QString up = w.toUpper();
52
53         // expand common abbreviations
54         if (up == "FLD") {
55             changedWords.append("Field");
56             continue;
57         }
58
59         if (up == "MUNI") {
60             changedWords.append("Municipal");
61             continue;
62         }
63
64         if (up == "RGNL") {
65             changedWords.append("Regional");
66             continue;
67         }
68
69         if (up == "CTR") {
70             changedWords.append("Center");
71             continue;
72         }
73
74         if (up == "INTL") {
75             changedWords.append("International");
76             continue;
77         }
78
79         // occurs in many Australian airport names in our DB
80         if (up == "(NSW)") {
81             changedWords.append("(New South Wales)");
82             continue;
83         }
84
85         if ((up == "VOR") || (up == "NDB") || (up == "VOR-DME") || (up == "VORTAC") || (up == "NDB-DME")) {
86             changedWords.append(w);
87             continue;
88         }
89
90         QChar firstChar = w.at(0).toUpper();
91         w = w.mid(1).toLower();
92         w.prepend(firstChar);
93
94         changedWords.append(w);
95     }
96
97     return changedWords.join(QChar(' '));
98 }
99
100 QString formatGeodAsString(const SGGeod& geod)
101 {
102     QChar ns = (geod.getLatitudeDeg() > 0.0) ? 'N' : 'S';
103     QChar ew = (geod.getLongitudeDeg() > 0.0) ? 'E' : 'W';
104
105     return QString::number(fabs(geod.getLongitudeDeg()), 'f',2 ) + ew + " " +
106             QString::number(fabs(geod.getLatitudeDeg()), 'f',2 ) + ns;
107 }
108
109 class IdentSearchFilter : public FGPositioned::TypeFilter
110 {
111 public:
112     IdentSearchFilter()
113     {
114         addType(FGPositioned::AIRPORT);
115         addType(FGPositioned::SEAPORT);
116         addType(FGPositioned::HELIPAD);
117         addType(FGPositioned::VOR);
118         addType(FGPositioned::FIX);
119         addType(FGPositioned::NDB);
120     }
121 };
122
123 class NavSearchModel : public QAbstractListModel
124 {
125     Q_OBJECT
126 public:
127     NavSearchModel() :
128         m_searchActive(false)
129     {
130     }
131
132     void setSearch(QString t)
133     {
134         beginResetModel();
135
136         m_items.clear();
137         m_ids.clear();
138
139         std::string term(t.toUpper().toStdString());
140
141         IdentSearchFilter filter;
142         FGPositionedList exactMatches = NavDataCache::instance()->findAllWithIdent(term, &filter, true);
143
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]);
147         }
148         endResetModel();
149
150
151         m_search.reset(new NavDataCache::ThreadedGUISearch(term));
152         QTimer::singleShot(100, this, &NavSearchModel::onSearchResultsPoll);
153         m_searchActive = true;
154         endResetModel();
155     }
156
157     bool isSearchActive() const
158     {
159         return m_searchActive;
160     }
161
162     virtual int rowCount(const QModelIndex&) const
163     {
164         // if empty, return 1 for special 'no matches'?
165         return m_ids.size();
166     }
167
168     virtual QVariant data(const QModelIndex& index, int role) const
169     {
170         if (!index.isValid())
171             return QVariant();
172
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()));
179             } else {
180                 QString name = fixNavaidName(QString::fromStdString(pos->name()));
181                 return QString("%1: %2").arg(QString::fromStdString(pos->ident())).arg(name);
182             }
183         }
184
185         if (role == Qt::EditRole) {
186             return QString::fromStdString(pos->ident());
187         }
188
189         if (role == Qt::UserRole) {
190             return static_cast<qlonglong>(m_ids[index.row()]);
191         }
192
193         return QVariant();
194     }
195
196     FGPositionedRef itemAtRow(unsigned int row) const
197     {
198         FGPositionedRef pos = m_items[row];
199         if (!pos.valid()) {
200             pos = NavDataCache::instance()->loadById(m_ids[row]);
201             m_items[row] = pos;
202         }
203
204         return pos;
205     }
206 Q_SIGNALS:
207     void searchComplete();
208
209 private:
210
211
212     void onSearchResultsPoll()
213     {
214         PositionedIDVec newIds = m_search->results();
215
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
220         }
221         endInsertRows();
222
223         if (m_search->isComplete()) {
224             m_searchActive = false;
225             m_search.reset();
226             emit searchComplete();
227         } else {
228             QTimer::singleShot(100, this, &NavSearchModel::onSearchResultsPoll);
229         }
230     }
231
232 private:
233     PositionedIDVec m_ids;
234     mutable FGPositionedList m_items;
235     bool m_searchActive;
236     QScopedPointer<NavDataCache::ThreadedGUISearch> m_search;
237 };
238
239
240 LocationWidget::LocationWidget(QWidget *parent) :
241     QWidget(parent),
242     m_ui(new Ui::LocationWidget)
243 {
244     m_ui->setupUi(this);
245
246
247     QIcon historyIcon(":/history-icon");
248     m_ui->searchHistory->setIcon(historyIcon);
249
250     m_ui->searchIcon->setPixmap(QPixmap(":/search-icon"));
251
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);
258
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()));
271
272     connect(m_ui->airportDiagram, &AirportDiagram::clickedRunway,
273             this, &LocationWidget::onAirportDiagramClicked);
274
275     connect(m_ui->locationSearchEdit, &QLineEdit::returnPressed,
276             this, &LocationWidget::onSearch);
277
278     connect(m_ui->searchHistory, &QPushButton::clicked,
279             this, &LocationWidget::onPopupHistory);
280
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()));
291
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();
296
297     connect(m_backButton, &QAbstractButton::clicked,
298             this, &LocationWidget::onBackToSearch);
299
300 // force various pieces of UI into sync
301     onOffsetEnabledToggled(m_ui->offsetGroup->isChecked());
302     onOffsetBearingTrueChanged(m_ui->trueBearing->isChecked());
303     onBackToSearch();
304 }
305
306 LocationWidget::~LocationWidget()
307 {
308     delete m_ui;
309 }
310
311 void LocationWidget::restoreSettings()
312 {
313     QSettings settings;
314     Q_FOREACH(QVariant v, settings.value("recent-locations").toList()) {
315         m_recentAirports.push_back(v.toLongLong());
316     }
317
318     if (!m_recentAirports.empty()) {
319         setBaseLocation(NavDataCache::instance()->loadById(m_recentAirports.front()));
320     }
321
322     updateDescription();
323 }
324
325 bool LocationWidget::shouldStartPaused() const
326 {
327     if (!m_location) {
328         return false; // defaults to on-ground at KSFO
329     }
330
331     if (FGAirport::isAirportType(m_location.ptr())) {
332         return m_ui->onFinalCheckbox->isChecked();
333     } else {
334         // navaid, start paused
335         return true;
336     }
337
338     return false;
339 }
340
341 void LocationWidget::saveSettings()
342 {
343     QSettings settings;
344
345     QVariantList locations;
346     Q_FOREACH(PositionedID v, m_recentAirports) {
347         locations.push_back(v);
348     }
349
350     settings.setValue("recent-airports", locations);
351 }
352
353 void LocationWidget::setLocationOptions()
354 {
355     flightgear::Options* opt = flightgear::Options::sharedInstance();
356
357     if (!m_location) {
358         return;
359     }
360
361     if (FGAirport::isAirportType(m_location.ptr())) {
362         FGAirport* apt = static_cast<FGAirport*>(m_location.ptr());
363         opt->addOption("airport", apt->ident());
364
365         if (m_ui->runwayRadio->isChecked()) {
366             int index = m_ui->runwayCombo->itemData(m_ui->runwayCombo->currentIndex()).toInt();
367             if (index >= 0) {
368                 // explicit runway choice
369                 opt->addOption("runway", apt->getRunwayByIndex(index)->ident());
370             }
371
372             if (m_ui->onFinalCheckbox->isChecked()) {
373                 opt->addOption("glideslope", "3.0");
374                 opt->addOption("offset-distance", "10.0"); // in nautical miles
375             }
376         } else if (m_ui->parkingRadio->isChecked()) {
377             // parking selection
378             opt->addOption("parkpos", m_ui->parkingCombo->currentText().toStdString());
379         }
380         // of location is an airport
381     }
382
383     FGPositioned::Type ty = m_location->type();
384     switch (ty) {
385     case FGPositioned::VOR:
386     case FGPositioned::NDB:
387     case FGPositioned::FIX:
388         // set disambiguation property
389         globals->get_props()->setIntValue("/sim/presets/navaid-id",
390                                           static_cast<int>(m_location->guid()));
391
392         // we always set 'fix', but really this is just to force positionInit
393         // code to check for the navaid-id value above.
394         opt->addOption("fix", m_location->ident());
395         break;
396     default:
397         break;
398     }
399 }
400
401 void LocationWidget::onSearch()
402 {
403     QString search = m_ui->locationSearchEdit->text();
404     m_searchModel->setSearch(search);
405
406     if (m_searchModel->isSearchActive()) {
407         m_ui->searchStatusText->setText(QString("Searching for '%1'").arg(search));
408         m_ui->searchIcon->setVisible(true);
409     } else if (m_searchModel->rowCount(QModelIndex()) == 1) {
410         setBaseLocation(m_searchModel->itemAtRow(0));
411     }
412 }
413
414 void LocationWidget::onSearchComplete()
415 {
416     QString search = m_ui->locationSearchEdit->text();
417     m_ui->searchIcon->setVisible(false);
418     m_ui->searchStatusText->setText(QString("Results for '%1'").arg(search));
419
420     int numResults = m_searchModel->rowCount(QModelIndex());
421     if (numResults == 0) {
422         m_ui->searchStatusText->setText(QString("No matches for '%1'").arg(search));
423     } else if (numResults == 1) {
424         setBaseLocation(m_searchModel->itemAtRow(0));
425     }
426 }
427
428 void LocationWidget::onLocationChanged()
429 {
430     bool locIsAirport = FGAirport::isAirportType(m_location.ptr());
431     m_backButton->show();
432
433     if (locIsAirport) {
434         m_ui->stack->setCurrentIndex(0);
435         FGAirport* apt = static_cast<FGAirport*>(m_location.ptr());
436         m_ui->airportDiagram->setAirport(apt);
437
438         m_ui->runwayRadio->setChecked(true); // default back to runway mode
439         // unless multiplayer is enabled ?
440         m_ui->airportDiagram->setEnabled(true);
441
442         m_ui->runwayCombo->clear();
443         m_ui->runwayCombo->addItem("Automatic", -1);
444         for (unsigned int r=0; r<apt->numRunways(); ++r) {
445             FGRunwayRef rwy = apt->getRunwayByIndex(r);
446             // add runway with index as data role
447             m_ui->runwayCombo->addItem(QString::fromStdString(rwy->ident()), r);
448
449             m_ui->airportDiagram->addRunway(rwy);
450         }
451
452         m_ui->parkingCombo->clear();
453         FGAirportDynamics* dynamics = apt->getDynamics();
454         PositionedIDVec parkings = NavDataCache::instance()->airportItemsOfType(m_location->guid(),
455                                                                                 FGPositioned::PARKING);
456         if (parkings.empty()) {
457             m_ui->parkingCombo->setEnabled(false);
458             m_ui->parkingRadio->setEnabled(false);
459         } else {
460             m_ui->parkingCombo->setEnabled(true);
461             m_ui->parkingRadio->setEnabled(true);
462             Q_FOREACH(PositionedID parking, parkings) {
463                 FGParking* park = dynamics->getParking(parking);
464                 m_ui->parkingCombo->addItem(QString::fromStdString(park->getName()),
465                                             static_cast<qlonglong>(parking));
466
467                 m_ui->airportDiagram->addParking(park);
468             }
469         }
470
471
472     } else {// of location is airport
473         // navaid
474         m_ui->stack->setCurrentIndex(1);
475         m_ui->navaidDiagram->setNavaid(m_location);
476     }
477 }
478
479 void LocationWidget::onOffsetEnabledToggled(bool on)
480 {
481     m_ui->offsetDistanceLabel->setEnabled(on);
482 }
483
484 void LocationWidget::onAirportDiagramClicked(FGRunwayRef rwy)
485 {
486     if (rwy) {
487         m_ui->runwayRadio->setChecked(true);
488         int rwyIndex = m_ui->runwayCombo->findText(QString::fromStdString(rwy->ident()));
489         m_ui->runwayCombo->setCurrentIndex(rwyIndex);
490         m_ui->airportDiagram->setSelectedRunway(rwy);
491     }
492
493     updateDescription();
494 }
495
496 QString LocationWidget::locationDescription() const
497 {
498     if (!m_location)
499         return QString("No location selected");
500
501     bool locIsAirport = FGAirport::isAirportType(m_location.ptr());
502     QString ident = QString::fromStdString(m_location->ident()),
503         name = QString::fromStdString(m_location->name());
504
505     name = fixNavaidName(name);
506
507     if (locIsAirport) {
508         FGAirport* apt = static_cast<FGAirport*>(m_location.ptr());
509         QString locationOnAirport;
510
511         if (m_ui->runwayRadio->isChecked()) {
512             bool onFinal = m_ui->onFinalCheckbox->isChecked();
513             int comboIndex = m_ui->runwayCombo->currentIndex();
514             QString runwayName = (comboIndex == 0) ?
515                 "active runway" :
516                 QString("runway %1").arg(m_ui->runwayCombo->currentText());
517
518             if (onFinal) {
519                 int finalDistance = m_ui->approachDistanceSpin->value();
520                 locationOnAirport = QString("on %2-mile final to %1").arg(runwayName).arg(finalDistance);
521             } else {
522                 locationOnAirport = QString("on %1").arg(runwayName);
523             }
524         } else if (m_ui->parkingRadio->isChecked()) {
525             locationOnAirport = QString("at parking position %1").arg(m_ui->parkingCombo->currentText());
526         }
527
528         return QString("%2 (%1): %3").arg(ident).arg(name).arg(locationOnAirport);
529     } else {
530         QString navaidType;
531         switch (m_location->type()) {
532         case FGPositioned::VOR:
533             navaidType = QString("VOR"); break;
534         case FGPositioned::NDB:
535             navaidType = QString("NDB"); break;
536         case FGPositioned::FIX:
537             return QString("at waypoint %1").arg(ident);
538         default:
539             // unsupported type
540             break;
541         }
542
543         return QString("at %1 %2 (%3)").arg(navaidType).arg(ident).arg(name);
544     }
545
546     return QString("Implement Me");
547 }
548
549
550 void LocationWidget::updateDescription()
551 {
552     bool locIsAirport = FGAirport::isAirportType(m_location.ptr());
553     if (locIsAirport) {
554         FGAirport* apt = static_cast<FGAirport*>(m_location.ptr());
555
556         if (m_ui->runwayRadio->isChecked()) {
557             int comboIndex = m_ui->runwayCombo->currentIndex();
558             int runwayIndex = m_ui->runwayCombo->itemData(comboIndex).toInt();
559             // we can't figure out the active runway in the launcher (yet)
560             FGRunwayRef rwy = (runwayIndex >= 0) ?
561                 apt->getRunwayByIndex(runwayIndex) : FGRunwayRef();
562             m_ui->airportDiagram->setSelectedRunway(rwy);
563         }
564
565         if (m_ui->onFinalCheckbox->isChecked()) {
566             m_ui->airportDiagram->setApproachExtensionDistance(m_ui->approachDistanceSpin->value());
567         } else {
568             m_ui->airportDiagram->setApproachExtensionDistance(0.0);
569         }
570     } else {
571
572     }
573
574 #if 0
575
576     QString locationOnAirport;
577     if (m_ui->runwayRadio->isChecked()) {
578
579
580     } else if (m_ui->parkingRadio->isChecked()) {
581         locationOnAirport =  QString("at parking position %1").arg(m_ui->parkingCombo->currentText());
582     }
583
584     m_ui->airportDescription->setText();
585 #endif
586
587     emit descriptionChanged(locationDescription());
588 }
589
590 void LocationWidget::onSearchResultSelected(const QModelIndex& index)
591 {
592     qDebug() << "selected result:" << index.data();
593     setBaseLocation(m_searchModel->itemAtRow(index.row()));
594 }
595
596 void LocationWidget::onOffsetBearingTrueChanged(bool on)
597 {
598     m_ui->offsetBearingLabel->setText(on ? tr("True bearing:") :
599                                       tr("Magnetic bearing:"));
600 }
601
602
603 void LocationWidget::onPopupHistory()
604 {
605     if (m_recentAirports.isEmpty()) {
606         return;
607     }
608
609 #if 0
610     QMenu m;
611     Q_FOREACH(QString aptCode, m_recentAirports) {
612         FGAirportRef apt = FGAirport::findByIdent(aptCode.toStdString());
613         QString name = QString::fromStdString(apt->name());
614         QAction* act = m.addAction(QString("%1 - %2").arg(aptCode).arg(name));
615         act->setData(aptCode);
616     }
617
618     QPoint popupPos = m_ui->airportHistory->mapToGlobal(m_ui->airportHistory->rect().bottomLeft());
619     QAction* triggered = m.exec(popupPos);
620     if (triggered) {
621         FGAirportRef apt = FGAirport::findByIdent(triggered->data().toString().toStdString());
622         setAirport(apt);
623         m_ui->airportEdit->clear();
624         m_ui->locationStack->setCurrentIndex(0);
625     }
626 #endif
627 }
628
629 void LocationWidget::setBaseLocation(FGPositionedRef ref)
630 {
631     if (m_location == ref)
632         return;
633
634     m_location = ref;
635     onLocationChanged();
636
637 #if 0
638     if (ref.valid()) {
639         // maintain the recent airport list
640         QString icao = QString::fromStdString(ref->ident());
641         if (m_recentAirports.contains(icao)) {
642             // move to front
643             m_recentAirports.removeOne(icao);
644             m_recentAirports.push_front(icao);
645         } else {
646             // insert and trim list if necessary
647             m_recentAirports.push_front(icao);
648             if (m_recentAirports.size() > MAX_RECENT_AIRPORTS) {
649                 m_recentAirports.pop_back();
650             }
651         }
652     }
653 #endif
654     updateDescription();
655 }
656
657 void LocationWidget::onOffsetDataChanged()
658 {
659     qDebug() << "implement me";
660 }
661
662 void LocationWidget::onBackToSearch()
663 {
664     m_ui->stack->setCurrentIndex(2);
665     m_backButton->hide();
666 }
667
668 #include "LocationWidget.moc"