1 // NavaidSearchModel.cxx - expose navaids via a QabstractListModel
2 //
3 // Written by James Turner, started July 2018.
4 //
5 // Copyright (C) 2018 James Turner <james@flightgear.org>
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 "NavaidSearchModel.hxx"
22 
23 #include <QTimer>
24 
25 #include "AirportDiagram.hxx"
26 #include <Navaids/navrecord.hxx>
27 #include "QmlPositioned.hxx"
28 
29 using namespace flightgear;
30 
fixNavaidName(QString s)31 QString fixNavaidName(QString s)
32 {
33     // split into words
34     QStringList words = s.split(QChar(' '));
35     QStringList changedWords;
36     Q_FOREACH(QString w, words) {
37         if (w.isEmpty())
38             continue;
39 
40         QString up = w.toUpper();
41 
42         // expand common abbreviations
43         // note these are not translated, since they are abbreivations
44         // for English-langauge airports, mostly in the US/Canada
45         if (up == "FLD") {
46             changedWords.append("Field");
47             continue;
48         }
49 
50         if (up == "CO") {
51             changedWords.append("County");
52             continue;
53         }
54 
55         if ((up == "MUNI") || (up == "MUN")) {
56             changedWords.append("Municipal");
57             continue;
58         }
59 
60         if (up == "MEM") {
61             changedWords.append("Memorial");
62             continue;
63         }
64 
65         if (up == "RGNL") {
66             changedWords.append("Regional");
67             continue;
68         }
69 
70         if (up == "CTR") {
71             changedWords.append("Center");
72             continue;
73         }
74 
75         if (up == "INTL") {
76             changedWords.append("International");
77             continue;
78         }
79 
80         // occurs in many Australian airport names in our DB
81         if (up == "(NSW)") {
82             changedWords.append("(New South Wales)");
83             continue;
84         }
85 
86         if ((up == "VOR") || (up == "NDB")
87                 || (up == "VOR-DME") || (up == "VORTAC")
88                 || (up == "NDB-DME")
89                 || (up == "AFB") || (up == "RAF"))
90         {
91             changedWords.append(w);
92             continue;
93         }
94 
95         if ((up =="[X]") || (up == "[H]") || (up == "[S]")) {
96             continue; // consume
97         }
98 
99         QChar firstChar = w.at(0).toUpper();
100         w = w.mid(1).toLower();
101         w.prepend(firstChar);
102 
103         changedWords.append(w);
104     }
105 
106     return changedWords.join(QChar(' '));
107 }
108 
109 
110 class IdentSearchFilter : public FGPositioned::TypeFilter
111 {
112 public:
IdentSearchFilter(LauncherController::AircraftType aircraft,bool airportsOnly)113     IdentSearchFilter(LauncherController::AircraftType aircraft, bool airportsOnly)
114     {
115         if (!airportsOnly) {
116             addType(FGPositioned::VOR);
117             addType(FGPositioned::FIX);
118             addType(FGPositioned::NDB);
119         }
120 
121         addType(FGPositioned::AIRPORT);
122 
123         switch (aircraft) {
124         case LauncherController::Airplane:
125             break;
126 
127         case LauncherController::Helicopter:
128             addType(FGPositioned::HELIPORT);
129             break;
130 
131         case LauncherController::Seaplane:
132             addType(FGPositioned::SEAPORT);
133             break;
134 
135         default:
136             addType(FGPositioned::HELIPORT);
137             addType(FGPositioned::SEAPORT);
138         }
139     }
140 };
141 
clear()142 void NavaidSearchModel::clear()
143 {
144     beginResetModel();
145     m_items.clear();
146     m_ids.clear();
147     m_searchActive = false;
148     m_search.reset();
149     endResetModel();
150     emit searchActiveChanged();
151     emit haveExistingSearchChanged();
152 }
153 
guidAtIndex(int index) const154 qlonglong NavaidSearchModel::guidAtIndex(int index) const
155 {
156     const size_t uIndex = static_cast<size_t>(index);
157     if ((index < 0) || (uIndex >= m_ids.size()))
158         return 0;
159 
160     return m_ids.at(uIndex);
161 }
162 
NavaidSearchModel(QObject * parent)163 NavaidSearchModel::NavaidSearchModel(QObject *parent) :
164     QAbstractListModel(parent)
165 {
166 
167 }
168 
setSearch(QString t,NavaidSearchModel::AircraftType aircraft)169 void NavaidSearchModel::setSearch(QString t, NavaidSearchModel::AircraftType aircraft)
170 {
171     beginResetModel();
172 
173     m_items.clear();
174     m_ids.clear();
175 
176     std::string term(t.toUpper().toStdString());
177 
178     IdentSearchFilter filter(static_cast<LauncherController::AircraftType>(aircraft), m_airportsOnly);
179     FGPositionedList exactMatches = NavDataCache::instance()->findAllWithIdent(term, &filter, true);
180 
181     m_ids.reserve(exactMatches.size());
182     m_items.reserve(exactMatches.size());
183     for (auto match : exactMatches) {
184         m_ids.push_back(match->guid());
185         m_items.push_back(match);
186     }
187 
188     resort();
189     endResetModel();
190 
191     m_search.reset(new NavDataCache::ThreadedGUISearch(term, m_airportsOnly));
192     QTimer::singleShot(100, this, SLOT(onSearchResultsPoll()));
193     m_searchActive = true;
194     emit searchActiveChanged();
195     emit haveExistingSearchChanged();
196 }
197 
haveExistingSearch() const198 bool NavaidSearchModel::haveExistingSearch() const
199 {
200     return m_searchActive || (!m_items.empty());
201 }
202 
rowCount(const QModelIndex &) const203 int NavaidSearchModel::rowCount(const QModelIndex &) const
204 {
205     if (m_maxResults > 0)
206         return std::min(static_cast<int>(m_ids.size()), m_maxResults);
207 
208     return static_cast<int>(m_ids.size());
209 }
210 
data(const QModelIndex & index,int role) const211 QVariant NavaidSearchModel::data(const QModelIndex &index, int role) const
212 {
213     if (!index.isValid())
214         return QVariant();
215 
216     FGPositionedRef pos = itemAtRow(index.row());
217     switch (role) {
218     case GuidRole: return static_cast<qlonglong>(pos->guid());
219     case IdentRole: return QString::fromStdString(pos->ident());
220     case NameRole:
221         return fixNavaidName(QString::fromStdString(pos->name()));
222 
223     case NavFrequencyRole: {
224         FGNavRecord* nav = fgpositioned_cast<FGNavRecord>(pos);
225         return nav ? nav->get_freq() : 0;
226     }
227 
228     case TypeRole: return static_cast<QmlPositioned::Type>(pos->type());
229     case IconRole:
230         return AirportDiagram::iconForPositioned(pos,
231                                                  AirportDiagram::SmallIcons | AirportDiagram::LargeAirportPlans);
232     }
233 
234     return {};
235 }
236 
itemAtRow(unsigned int row) const237 FGPositionedRef NavaidSearchModel::itemAtRow(unsigned int row) const
238 {
239     FGPositionedRef pos = m_items[row];
240     if (!pos.valid()) {
241         pos = NavDataCache::instance()->loadById(m_ids[row]);
242         m_items[row] = pos;
243     }
244 
245     return pos;
246 }
247 
setItems(const FGPositionedList & items)248 void NavaidSearchModel::setItems(const FGPositionedList &items)
249 {
250     beginResetModel();
251     m_searchActive = false;
252     m_items = items;
253 
254     m_ids.clear();
255     for (unsigned int i=0; i < items.size(); ++i) {
256         m_ids.push_back(m_items[i]->guid());
257     }
258 
259     // don't sort in this case
260     endResetModel();
261     emit searchActiveChanged();
262 }
263 
roleNames() const264 QHash<int, QByteArray> NavaidSearchModel::roleNames() const
265 {
266     QHash<int, QByteArray> result = QAbstractListModel::roleNames();
267 
268     result[GeodRole] = "geod";
269     result[GuidRole] = "guid";
270     result[IdentRole] = "ident";
271     result[NameRole] = "name";
272     result[IconRole] = "icon";
273     result[TypeRole] = "type";
274     result[NavFrequencyRole] = "frequency";
275     return result;
276 }
277 
exactMatch() const278 qlonglong NavaidSearchModel::exactMatch() const
279 {
280     if (m_searchActive || (m_ids.size() != 1))
281         return 0; // no exact match
282 
283     return m_ids.back(); // which is also the front
284 }
285 
numResults() const286 int NavaidSearchModel::numResults() const
287 {
288     return static_cast<int>(m_ids.size());
289 }
290 
onSearchResultsPoll()291 void NavaidSearchModel::onSearchResultsPoll()
292 {
293     if (m_search.isNull()) {
294         return;
295     }
296 
297     PositionedIDVec newIds = m_search->results();
298     if (!newIds.empty()) {
299         beginResetModel(); // reset the model since we will re-sort
300         for (auto id : newIds) {
301             m_ids.push_back(id);
302             m_items.push_back({}); // null ref
303         }
304         resort();
305         endResetModel();
306     }
307 
308     if (m_search->isComplete()) {
309         m_searchActive = false;
310         m_search.reset();
311         emit searchComplete();
312         emit searchActiveChanged();
313         emit haveExistingSearchChanged();
314     } else {
315         QTimer::singleShot(100, this, SLOT(onSearchResultsPoll()));
316     }
317 }
318 
resort()319 void NavaidSearchModel::resort()
320 {
321     if (!m_airportsOnly) {
322         return;
323     }
324 
325     // clear m_items
326     std::fill(m_items.begin(), m_items.end(), FGPositionedRef{});
327 
328     // build runway length cache
329     std::map<PositionedID, double> longestRunwayCache;
330     for (auto a : m_ids) {
331         const FGAirportRef apt = FGPositioned::loadById<FGAirport>(a);
332         if (apt) {
333             const auto rwy = apt->longestRunway();
334             if (rwy) {
335                 longestRunwayCache[a] = rwy->lengthFt();
336             }
337         }
338     }
339 
340     std::sort(m_ids.begin(), m_ids.end(),
341               [&longestRunwayCache](const PositionedID a, const PositionedID& b)
342     {
343         return longestRunwayCache[a] > longestRunwayCache[b];
344     });
345 }
346