]> git.mxchange.org Git - flightgear.git/blob - src/GUI/AircraftModel.cxx
The QUrl header is needed for compilation on Linux
[flightgear.git] / src / GUI / AircraftModel.cxx
1 // AircraftModel.cxx - part of GUI launcher using Qt5
2 //
3 // Written by James Turner, started March 2015.
4 //
5 // Copyright (C) 2014 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 "AircraftModel.hxx"
22
23 #include <QDir>
24 #include <QThread>
25 #include <QMutex>
26 #include <QMutexLocker>
27 #include <QDataStream>
28 #include <QSettings>
29 #include <QDebug>
30
31 // Simgear
32 #include <simgear/props/props_io.hxx>
33 #include <simgear/structure/exception.hxx>
34 #include <simgear/misc/sg_path.hxx>
35
36 // FlightGear
37 #include <Main/globals.hxx>
38
39 AircraftItem::AircraftItem()
40 {
41     // oh for C++11 initialisers
42     for (int i=0; i<4; ++i) ratings[i] = 0;
43 }
44
45 AircraftItem::AircraftItem(QDir dir, QString filePath)
46 {
47     for (int i=0; i<4; ++i) ratings[i] = 0;
48
49     SGPropertyNode root;
50     readProperties(filePath.toStdString(), &root);
51
52     if (!root.hasChild("sim")) {
53         throw sg_io_exception(std::string("Malformed -set.xml file"), filePath.toStdString());
54     }
55
56     SGPropertyNode_ptr sim = root.getNode("sim");
57
58     path = filePath;
59     pathModTime = QFileInfo(path).lastModified();
60
61     description = sim->getStringValue("description");
62     authors =  sim->getStringValue("author");
63
64     if (sim->hasChild("rating")) {
65         SGPropertyNode_ptr ratingsNode = sim->getNode("rating");
66         ratings[0] = ratingsNode->getIntValue("FDM");
67         ratings[1] = ratingsNode->getIntValue("systems");
68         ratings[2] = ratingsNode->getIntValue("cockpit");
69         ratings[3] = ratingsNode->getIntValue("model");
70
71     }
72
73     if (sim->hasChild("variant-of")) {
74         variantOf = sim->getStringValue("variant-of");
75     }
76 }
77
78 QString AircraftItem::baseName() const
79 {
80     QString fn = QFileInfo(path).fileName();
81     fn.truncate(fn.count() - 8);
82     return fn;
83 }
84
85 void AircraftItem::fromDataStream(QDataStream& ds)
86 {
87     ds >> path >> description >> authors >> variantOf;
88     for (int i=0; i<4; ++i) ds >> ratings[i];
89     ds >> pathModTime;
90 }
91
92 void AircraftItem::toDataStream(QDataStream& ds) const
93 {
94     ds << path << description << authors << variantOf;
95     for (int i=0; i<4; ++i) ds << ratings[i];
96     ds << pathModTime;
97 }
98
99 QPixmap AircraftItem::thumbnail() const
100 {
101     if (m_thumbnail.isNull()) {
102         QFileInfo info(path);
103         QDir dir = info.dir();
104         if (dir.exists("thumbnail.jpg")) {
105             m_thumbnail.load(dir.filePath("thumbnail.jpg"));
106             // resize to the standard size
107             if (m_thumbnail.height() > 128) {
108                 m_thumbnail = m_thumbnail.scaledToHeight(128);
109             }
110         }
111     }
112
113     return m_thumbnail;
114 }
115
116
117 static int CACHE_VERSION = 2;
118
119 class AircraftScanThread : public QThread
120 {
121     Q_OBJECT
122 public:
123     AircraftScanThread(QStringList dirsToScan) :
124         m_dirs(dirsToScan),
125         m_done(false)
126     {
127     }
128
129     ~AircraftScanThread()
130     {
131     }
132
133     /** thread-safe access to items already scanned */
134     QList<AircraftItem*> items()
135     {
136         QList<AircraftItem*> result;
137         QMutexLocker g(&m_lock);
138         result.swap(m_items);
139         g.unlock();
140         return result;
141     }
142
143     void setDone()
144     {
145         m_done = true;
146     }
147 Q_SIGNALS:
148     void addedItems();
149
150 protected:
151     virtual void run()
152     {
153         readCache();
154
155         Q_FOREACH(QString d, m_dirs) {
156             scanAircraftDir(QDir(d));
157             if (m_done) {
158                 return;
159             }
160         }
161
162         writeCache();
163     }
164
165 private:
166     void readCache()
167     {
168         QSettings settings;
169         QByteArray cacheData = settings.value("aircraft-cache").toByteArray();
170         if (!cacheData.isEmpty()) {
171             QDataStream ds(cacheData);
172             quint32 count, cacheVersion;
173             ds >> cacheVersion >> count;
174
175             if (cacheVersion != CACHE_VERSION) {
176                 return; // mis-matched cache, version, drop
177             }
178
179              for (int i=0; i<count; ++i) {
180                 AircraftItem* item = new AircraftItem;
181                 item->fromDataStream(ds);
182
183                 QFileInfo finfo(item->path);
184                 if (!finfo.exists() || (finfo.lastModified() != item->pathModTime)) {
185                     delete item;
186                 } else {
187                     // corresponding -set.xml file still exists and is
188                     // unmodified
189                     m_cachedItems[item->path] = item;
190                 }
191             } // of cached item iteration
192         }
193     }
194
195     void writeCache()
196     {
197         QSettings settings;
198         QByteArray cacheData;
199         {
200             QDataStream ds(&cacheData, QIODevice::WriteOnly);
201             quint32 count = m_nextCache.count();
202             ds << CACHE_VERSION << count;
203
204             Q_FOREACH(AircraftItem* item, m_nextCache.values()) {
205                 item->toDataStream(ds);
206             }
207         }
208
209         settings.setValue("aircraft-cache", cacheData);
210     }
211
212     void scanAircraftDir(QDir path)
213     {
214         QTime t;
215         t.start();
216
217         QStringList filters;
218         filters << "*-set.xml";
219         Q_FOREACH(QFileInfo child, path.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)) {
220             QDir childDir(child.absoluteFilePath());
221             QMap<QString, AircraftItem*> baseAircraft;
222             QList<AircraftItem*> variants;
223
224             Q_FOREACH(QFileInfo xmlChild, childDir.entryInfoList(filters, QDir::Files)) {
225                 try {
226                     QString absolutePath = xmlChild.absoluteFilePath();
227                     AircraftItem* item = NULL;
228
229                     if (m_cachedItems.contains(absolutePath)) {
230                         item = m_cachedItems.value(absolutePath);
231                     } else {
232                         item = new AircraftItem(childDir, absolutePath);
233                     }
234
235                     m_nextCache[absolutePath] = item;
236
237                     if (item->variantOf.isNull()) {
238                         baseAircraft.insert(item->baseName(), item);
239                     } else {
240                         variants.append(item);
241                     }
242                 } catch (sg_exception& e) {
243                     continue;
244                 }
245
246                 if (m_done) {
247                     return;
248                 }
249             } // of set.xml iteration
250
251             // bind variants to their principals
252             Q_FOREACH(AircraftItem* item, variants) {
253                 if (!baseAircraft.contains(item->variantOf)) {
254                     qWarning() << "can't find principal aircraft " << item->variantOf << " for variant:" << item->path;
255                     delete item;
256                     continue;
257                 }
258
259                 baseAircraft.value(item->variantOf)->variants.append(item);
260             }
261
262             // lock mutex while we modify the items array
263             {
264                 QMutexLocker g(&m_lock);
265                 m_items.append(baseAircraft.values());
266             }
267
268             emit addedItems();
269         } // of subdir iteration
270
271         qDebug() << "scan of" << path << "took" << t.elapsed();
272     }
273
274     QMutex m_lock;
275     QStringList m_dirs;
276     QList<AircraftItem*> m_items;
277
278     QMap<QString, AircraftItem* > m_cachedItems;
279     QMap<QString, AircraftItem* > m_nextCache;
280
281     bool m_done;
282 };
283
284 AircraftItemModel::AircraftItemModel(QObject* pr, simgear::pkg::RootRef& rootRef) :
285     QAbstractListModel(pr),
286     m_scanThread(NULL),
287     m_packageRoot(rootRef)
288 {
289 }
290
291 AircraftItemModel::~AircraftItemModel()
292 {
293     abandonCurrentScan();
294 }
295
296 void AircraftItemModel::setPaths(QStringList paths)
297 {
298     m_paths = paths;
299 }
300
301 void AircraftItemModel::scanDirs()
302 {
303     abandonCurrentScan();
304
305     beginResetModel();
306     qDeleteAll(m_items);
307     m_items.clear();
308     m_activeVariant.clear();
309     endResetModel();
310
311     QStringList dirs = m_paths;
312
313     Q_FOREACH(std::string ap, globals->get_aircraft_paths()) {
314         dirs << QString::fromStdString(ap);
315     }
316
317     SGPath rootAircraft(globals->get_fg_root());
318     rootAircraft.append("Aircraft");
319     dirs << QString::fromStdString(rootAircraft.str());
320
321     m_scanThread = new AircraftScanThread(dirs);
322     connect(m_scanThread, &AircraftScanThread::finished, this,
323             &AircraftItemModel::onScanFinished);
324     connect(m_scanThread, &AircraftScanThread::addedItems,
325             this, &AircraftItemModel::onScanResults);
326     m_scanThread->start();
327
328 }
329
330 void AircraftItemModel::abandonCurrentScan()
331 {
332     if (m_scanThread) {
333         m_scanThread->setDone();
334         m_scanThread->wait(1000);
335         delete m_scanThread;
336         m_scanThread = NULL;
337     }
338 }
339
340 QVariant AircraftItemModel::data(const QModelIndex& index, int role) const
341   {
342       if (role == AircraftVariantRole) {
343           return m_activeVariant.at(index.row());
344       }
345
346       const AircraftItem* item(m_items.at(index.row()));
347
348       if (role == AircraftVariantCountRole) {
349           return item->variants.count();
350       }
351
352       if (role >= AircraftVariantDescriptionRole) {
353           int variantIndex = role - AircraftVariantDescriptionRole;
354           return item->variants.at(variantIndex)->description;
355       }
356
357       quint32 variantIndex = m_activeVariant.at(index.row());
358       if (variantIndex) {
359           if (variantIndex <= item->variants.count()) {
360               // show the selected variant
361               item = item->variants.at(variantIndex - 1);
362           }
363       }
364
365       if (role == Qt::DisplayRole) {
366           return item->description;
367       } else if (role == Qt::DecorationRole) {
368           return item->thumbnail();
369       } else if (role == AircraftPathRole) {
370           return item->path;
371       } else if (role == AircraftAuthorsRole) {
372           return item->authors;
373       } else if ((role >= AircraftRatingRole) && (role < AircraftVariantDescriptionRole)) {
374           return item->ratings[role - AircraftRatingRole];
375       } else if (role == Qt::ToolTipRole) {
376           return item->path;
377       }
378
379       return QVariant();
380   }
381
382 bool AircraftItemModel::setData(const QModelIndex &index, const QVariant &value, int role)
383   {
384       if (role == AircraftVariantRole) {
385           m_activeVariant[index.row()] = value.toInt();
386           emit dataChanged(index, index);
387           return true;
388       }
389
390       return false;
391   }
392
393 QModelIndex AircraftItemModel::indexOfAircraftPath(QString path) const
394 {
395     for (int row=0; row <m_items.size(); ++row) {
396         const AircraftItem* item(m_items.at(row));
397         if (item->path == path) {
398             return index(row);
399         }
400     }
401
402     return QModelIndex();
403 }
404
405 void AircraftItemModel::onScanResults()
406 {
407     QList<AircraftItem*> newItems = m_scanThread->items();
408     if (newItems.isEmpty())
409         return;
410
411     int firstRow = m_items.count();
412     int lastRow = firstRow + newItems.count() - 1;
413     beginInsertRows(QModelIndex(), firstRow, lastRow);
414     m_items.append(newItems);
415
416     // default variants in all cases
417     for (int i=0; i< newItems.count(); ++i) {
418         m_activeVariant.append(0);
419     }
420     endInsertRows();
421 }
422
423 void AircraftItemModel::onScanFinished()
424 {
425     delete m_scanThread;
426     m_scanThread = NULL;
427 }
428
429 #include "AircraftModel.moc"