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