Marble

AbstractDataPluginModel.cpp
1// SPDX-License-Identifier: LGPL-2.1-or-later
2//
3// SPDX-FileCopyrightText: 2009 Bastian Holst <bastianholst@gmx.de>
4//
5
6// Self
7#include "AbstractDataPluginModel.h"
8
9// Qt
10#include <QAbstractListModel>
11#include <QMetaProperty>
12#include <QPointF>
13#include <QRectF>
14#include <QTimer>
15#include <QVariant>
16#include <QtAlgorithms>
17
18// Marble
19#include "AbstractDataPluginItem.h"
20#include "CacheStoragePolicy.h"
21#include "GeoDataCoordinates.h"
22#include "GeoDataLatLonAltBox.h"
23#include "HttpDownloadManager.h"
24#include "MarbleDebug.h"
25#include "MarbleDirs.h"
26#include "MarbleModel.h"
27#include "ViewportParams.h"
28
29#include <cmath>
30
31namespace Marble
32{
33
34const QString descriptionPrefix(QStringLiteral("description_"));
35
36// Time between two tried description file downloads (we decided not to download anything) in ms
37const int timeBetweenTriedDownloads = 500;
38// Time between two real description file downloads in ms
39const int timeBetweenDownloads = 1500;
40
41// The factor describing how much the box has to be changed to download a new description file.
42// A higher factor means more downloads.
43const qreal boxComparisonFactor = 16.0;
44
45// Separator to separate the id of the item from the file type
46const QChar fileIdSeparator = QLatin1Char('_');
47
48class FavoritesModel;
49
50class AbstractDataPluginModelPrivate
51{
52public:
53 AbstractDataPluginModelPrivate(const QString &name, const MarbleModel *marbleModel, AbstractDataPluginModel *parent);
54
55 ~AbstractDataPluginModelPrivate();
56
57 static QString generateFilename(const QString &id, const QString &type);
58 QString generateFilepath(const QString &id, const QString &type) const;
59
60 void updateFavoriteItems();
61
62 AbstractDataPluginModel *m_parent = nullptr;
63 const QString m_name;
64 const MarbleModel *const m_marbleModel;
65 GeoDataLatLonAltBox m_lastBox;
66 GeoDataLatLonAltBox m_downloadedBox;
67 qint32 m_lastNumber;
68 qint32 m_downloadedNumber;
69 QString m_currentPlanetId;
72 QList<AbstractDataPluginItem *> m_displayedItems;
73 QTimer m_downloadTimer;
74 quint32 m_descriptionFileNumber;
75 QHash<QString, QVariant> m_itemSettings;
76 QStringList m_favoriteItems;
77 bool m_favoriteItemsOnly;
78
79 CacheStoragePolicy m_storagePolicy;
80 HttpDownloadManager m_downloadManager;
81 FavoritesModel *m_favoritesModel;
82 QMetaObject m_metaObject;
83 bool m_hasMetaObject;
84 bool m_needsSorting;
85};
86
87class FavoritesModel : public QAbstractListModel
88{
89public:
90 AbstractDataPluginModelPrivate *const d;
91
92 explicit FavoritesModel(AbstractDataPluginModelPrivate *d, QObject *parent = nullptr);
93
94 int rowCount(const QModelIndex &parent = QModelIndex()) const override;
95
96 QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
97
98 void reset();
99
100 QHash<int, QByteArray> roleNames() const override;
101
102private:
103 QHash<int, QByteArray> m_roleNames;
104};
105
106AbstractDataPluginModelPrivate::AbstractDataPluginModelPrivate(const QString &name, const MarbleModel *marbleModel, AbstractDataPluginModel *parent)
107 : m_parent(parent)
108 , m_name(name)
109 , m_marbleModel(marbleModel)
110 , m_lastBox()
111 , m_downloadedBox()
112 , m_lastNumber(0)
113 , m_downloadedNumber(0)
114 , m_currentPlanetId(marbleModel->planetId())
115 , m_downloadTimer(m_parent)
116 , m_descriptionFileNumber(0)
117 , m_itemSettings()
118 , m_favoriteItemsOnly(false)
119 , m_storagePolicy(MarbleDirs::localPath() + QLatin1StringView("/cache/") + m_name + QLatin1Char('/'))
120 , m_downloadManager(&m_storagePolicy)
121 , m_favoritesModel(nullptr)
122 , m_hasMetaObject(false)
123 , m_needsSorting(false)
124{
125}
126
127AbstractDataPluginModelPrivate::~AbstractDataPluginModelPrivate()
128{
129 QList<AbstractDataPluginItem *>::iterator lIt = m_itemSet.begin();
130 QList<AbstractDataPluginItem *>::iterator const lItEnd = m_itemSet.end();
131 for (; lIt != lItEnd; ++lIt) {
132 (*lIt)->deleteLater();
133 }
134
135 QHash<QString, AbstractDataPluginItem *>::iterator hIt = m_downloadingItems.begin();
136 QHash<QString, AbstractDataPluginItem *>::iterator const hItEnd = m_downloadingItems.end();
137 for (; hIt != hItEnd; ++hIt) {
138 (*hIt)->deleteLater();
139 }
140
141 m_storagePolicy.clearCache();
142}
143
144void AbstractDataPluginModelPrivate::updateFavoriteItems()
145{
146 if (m_favoriteItemsOnly) {
147 for (const QString &id : std::as_const(m_favoriteItems)) {
148 if (!m_parent->findItem(id)) {
149 m_parent->getItem(id);
150 }
151 }
152 }
153}
154
155void AbstractDataPluginModel::themeChanged()
156{
157 if (d->m_currentPlanetId != d->m_marbleModel->planetId()) {
158 clear();
159 d->m_currentPlanetId = d->m_marbleModel->planetId();
160 }
161}
162
163static bool lessThanByPointer(const AbstractDataPluginItem *item1, const AbstractDataPluginItem *item2)
164{
165 if (item1 && item2) {
166 // Compare by sticky and favorite status (sticky first, then favorites), last by operator<
167 bool const sticky1 = item1->isSticky();
168 bool const favorite1 = item1->isFavorite();
169 if (sticky1 != item2->isSticky()) {
170 return sticky1;
171 } else if (favorite1 != item2->isFavorite()) {
172 return favorite1;
173 } else {
174 return item1->operator<(item2);
175 }
176 } else {
177 return false;
178 }
179}
180
181FavoritesModel::FavoritesModel(AbstractDataPluginModelPrivate *_d, QObject *parent)
182 : QAbstractListModel(parent)
183 , d(_d)
184{
186 int const size = d->m_hasMetaObject ? d->m_metaObject.propertyCount() : 0;
187 for (int i = 0; i < size; ++i) {
188 QMetaProperty property = d->m_metaObject.property(i);
189 roles[Qt::UserRole + i] = property.name();
190 }
191 roles[Qt::DisplayRole] = "display";
192 roles[Qt::DecorationRole] = "decoration";
193 m_roleNames = roles;
194}
195
196int FavoritesModel::rowCount(const QModelIndex &parent) const
197{
198 if (parent.isValid()) {
199 return 0;
200 }
201
202 int count = 0;
203 for (AbstractDataPluginItem *item : std::as_const(d->m_itemSet)) {
204 if (item->initialized() && item->isFavorite()) {
205 ++count;
206 }
207 }
208
209 return count;
210}
211
212QVariant FavoritesModel::data(const QModelIndex &index, int role) const
213{
214 int const row = index.row();
215 if (row >= 0 && row < rowCount()) {
216 int count = 0;
217 for (AbstractDataPluginItem *item : std::as_const(d->m_itemSet)) {
218 if (item->initialized() && item->isFavorite()) {
219 if (count == row) {
220 QString const roleName = QString::fromUtf8(roleNames().value(role));
221 return item->property(roleName.toLatin1().constData());
222 }
223 ++count;
224 }
225 }
226 }
227
228 return {};
229}
230
231void FavoritesModel::reset()
232{
233 beginResetModel();
234 endResetModel();
235}
236
237QHash<int, QByteArray> FavoritesModel::roleNames() const
238{
239 return m_roleNames;
240}
241
242AbstractDataPluginModel::AbstractDataPluginModel(const QString &name, const MarbleModel *marbleModel, QObject *parent)
243 : QObject(parent)
244 , d(new AbstractDataPluginModelPrivate(name, marbleModel, this))
245{
246 Q_ASSERT(marbleModel != nullptr);
247
248 // Initializing file and download System
249 connect(&d->m_downloadManager, SIGNAL(downloadComplete(QString, QString)), this, SLOT(processFinishedJob(QString, QString)));
250
251 connect(marbleModel, SIGNAL(themeChanged(QString)), this, SLOT(themeChanged()));
252
253 // We want to download a new description file every timeBetweenDownloads ms
254 connect(&d->m_downloadTimer, SIGNAL(timeout()), this, SLOT(handleChangedViewport()), Qt::QueuedConnection);
255 d->m_downloadTimer.start(timeBetweenDownloads);
256}
257
258AbstractDataPluginModel::~AbstractDataPluginModel()
259{
260 delete d;
261}
262
263const MarbleModel *AbstractDataPluginModel::marbleModel() const
264{
265 return d->m_marbleModel;
266}
267
268QList<AbstractDataPluginItem *> AbstractDataPluginModel::items(const ViewportParams *viewport, qint32 number)
269{
270 GeoDataLatLonAltBox currentBox = viewport->viewLatLonAltBox();
272
273 Q_ASSERT(!d->m_displayedItems.contains(nullptr) && "Null item in m_displayedItems. Please report a bug to marble-devel@kde.org");
274 Q_ASSERT(!d->m_itemSet.contains(nullptr) && "Null item in m_itemSet. Please report a bug to marble-devel@kde.org");
275
276 QList<AbstractDataPluginItem *> candidates = d->m_displayedItems + d->m_itemSet;
277
278 if (d->m_needsSorting) {
279 // Both the candidates list and the list of all items need to be sorted
280 std::sort(candidates.begin(), candidates.end(), lessThanByPointer);
281 std::sort(d->m_itemSet.begin(), d->m_itemSet.end(), lessThanByPointer);
282 d->m_needsSorting = false;
283 }
284
287
288 // Items that are already shown have the highest priority
289 for (; i != end && list.size() < number; ++i) {
290 // Only show items that are initialized
291 if (!(*i)->initialized()) {
292 continue;
293 }
294
295 // Hide non-favorite items if necessary
296 if (d->m_favoriteItemsOnly && !(*i)->isFavorite()) {
297 continue;
298 }
299
300 (*i)->setProjection(viewport);
301 if ((*i)->positions().isEmpty()) {
302 continue;
303 }
304
305 if (list.contains(*i)) {
306 continue;
307 }
308
309 // If the item was added initially at a nearer position, they don't have priority,
310 // because we zoomed out since then.
311 bool const alreadyDisplayed = d->m_displayedItems.contains(*i);
312 if (!alreadyDisplayed || (*i)->addedAngularResolution() >= viewport->angularResolution() || (*i)->isSticky()) {
313 bool collides = false;
314 int const length = list.length();
315 for (int j = 0; !collides && j < length; ++j) {
316 for (const QRectF &rect : list[j]->boundingRects()) {
317 for (const QRectF &itemRect : (*i)->boundingRects()) {
318 if (rect.intersects(itemRect))
319 collides = true;
320 }
321 }
322 }
323
324 if (!collides) {
325 list.append(*i);
326 (*i)->setSettings(d->m_itemSettings);
327
328 // We want to save the angular resolution of the first time the item got added.
329 if (!alreadyDisplayed) {
330 (*i)->setAddedAngularResolution(viewport->angularResolution());
331 }
332 }
333 }
334 // TODO: Do we have to cleanup at some point? The list of all items keeps growing
335 }
336
337 d->m_lastBox = currentBox;
338 d->m_lastNumber = number;
339 d->m_displayedItems = list;
340 return list;
341}
342
343QList<AbstractDataPluginItem *> AbstractDataPluginModel::whichItemAt(const QPoint &curpos)
344{
346
347 const QPointF curposF(curpos);
348 for (AbstractDataPluginItem *item : std::as_const(d->m_displayedItems)) {
349 if (item && item->contains(curposF)) {
350 itemsAt.append(item);
351 }
352 }
353
354 return itemsAt;
355}
356
357void AbstractDataPluginModel::parseFile(const QByteArray &file)
358{
359 Q_UNUSED(file);
360}
361
362void AbstractDataPluginModel::downloadItem(const QUrl &url, const QString &type, AbstractDataPluginItem *item)
363{
364 if (!item) {
365 return;
366 }
367
368 QString id = d->generateFilename(item->id(), type);
369
370 d->m_downloadManager.addJob(url, id, id, DownloadBrowse);
371 d->m_downloadingItems.insert(id, item);
372}
373
374void AbstractDataPluginModel::downloadDescriptionFile(const QUrl &url)
375{
376 if (!url.isEmpty()) {
377 QString name(descriptionPrefix);
378 name += QString::number(d->m_descriptionFileNumber);
379
380 d->m_downloadManager.addJob(url, name, name, DownloadBrowse);
381 d->m_descriptionFileNumber++;
382 }
383}
384
385void AbstractDataPluginModel::addItemToList(AbstractDataPluginItem *item)
386{
387 addItemsToList(QList<AbstractDataPluginItem *>() << item);
388}
389
390void AbstractDataPluginModel::addItemsToList(const QList<AbstractDataPluginItem *> &items)
391{
392 bool needsUpdate = false;
393 bool favoriteChanged = false;
394 for (AbstractDataPluginItem *item : items) {
395 if (!item) {
396 continue;
397 }
398
399 // If the item is already in our list, don't add it.
400 if (d->m_itemSet.contains(item)) {
401 continue;
402 }
403
404 if (itemExists(item->id())) {
405 item->deleteLater();
406 continue;
407 }
408
409 mDebug() << "New item " << item->id();
410
411 // This find the right position in the sorted to insert the new item
412 QList<AbstractDataPluginItem *>::iterator i = std::lower_bound(d->m_itemSet.begin(), d->m_itemSet.end(), item, lessThanByPointer);
413 // Insert the item on the right position in the list
414 d->m_itemSet.insert(i, item);
415
416 connect(item, &AbstractDataPluginItem::stickyChanged, this, &AbstractDataPluginModel::scheduleItemSort);
417 connect(item, &QObject::destroyed, this, &AbstractDataPluginModel::removeItem);
418 connect(item, &AbstractDataPluginItem::updated, this, &AbstractDataPluginModel::itemsUpdated);
419 connect(item, &AbstractDataPluginItem::favoriteChanged, this, &AbstractDataPluginModel::favoriteItemChanged);
420
421 if (!needsUpdate && item->initialized()) {
422 needsUpdate = true;
423 }
424
425 if (!favoriteChanged && item->initialized() && item->isFavorite()) {
426 favoriteChanged = true;
427 }
428 }
429
430 if (favoriteChanged && d->m_favoritesModel) {
431 d->m_favoritesModel->reset();
432 }
433
434 if (needsUpdate) {
435 Q_EMIT itemsUpdated();
436 }
437}
438
439void AbstractDataPluginModel::getItem(const QString &)
440{
441 qWarning() << "Retrieving items by identifier is not implemented by this plugin";
442}
443
444void AbstractDataPluginModel::setFavoriteItems(const QStringList &list)
445{
446 if (d->m_favoriteItems != list) {
447 d->m_favoriteItems = list;
448 d->updateFavoriteItems();
449 if (d->m_favoritesModel) {
450 d->m_favoritesModel->reset();
451 }
452 Q_EMIT favoriteItemsChanged(d->m_favoriteItems);
453 }
454}
455
456QStringList AbstractDataPluginModel::favoriteItems() const
457{
458 return d->m_favoriteItems;
459}
460
461void AbstractDataPluginModel::setFavoriteItemsOnly(bool favoriteOnly)
462{
463 if (isFavoriteItemsOnly() != favoriteOnly) {
464 d->m_favoriteItemsOnly = favoriteOnly;
465 d->updateFavoriteItems();
466 Q_EMIT favoriteItemsOnlyChanged();
467 }
468}
469
470bool AbstractDataPluginModel::isFavoriteItemsOnly() const
471{
472 return d->m_favoriteItemsOnly;
473}
474
475QObject *AbstractDataPluginModel::favoritesModel()
476{
477 if (!d->m_favoritesModel) {
478 d->m_favoritesModel = new FavoritesModel(d, this);
479 d->updateFavoriteItems();
480 }
481
482 return d->m_favoritesModel;
483}
484
485void AbstractDataPluginModel::favoriteItemChanged(const QString &id, bool isFavorite)
486{
487 QStringList favorites = d->m_favoriteItems;
488
489 if (isFavorite) {
490 if (!favorites.contains(id))
491 favorites.append(id);
492 } else {
493 favorites.removeOne(id);
494 }
495
496 setFavoriteItems(favorites);
497 scheduleItemSort();
498}
499
500void AbstractDataPluginModel::scheduleItemSort()
501{
502 d->m_needsSorting = true;
503}
504
505QString AbstractDataPluginModelPrivate::generateFilename(const QString &id, const QString &type)
506{
508 name += id;
509 name += fileIdSeparator;
510 name += type;
511
512 return name;
513}
514
515QString AbstractDataPluginModelPrivate::generateFilepath(const QString &id, const QString &type) const
516{
517 return MarbleDirs::localPath() + QLatin1StringView("/cache/") + m_name + QLatin1Char('/') + generateFilename(id, type);
518}
519
520AbstractDataPluginItem *AbstractDataPluginModel::findItem(const QString &id) const
521{
522 for (AbstractDataPluginItem *item : std::as_const(d->m_itemSet)) {
523 if (item->id() == id) {
524 return item;
525 }
526 }
527
528 return nullptr;
529}
530
531bool AbstractDataPluginModel::itemExists(const QString &id) const
532{
533 return findItem(id);
534}
535
536void AbstractDataPluginModel::setItemSettings(const QHash<QString, QVariant> &itemSettings)
537{
538 d->m_itemSettings = itemSettings;
539}
540
541void AbstractDataPluginModel::handleChangedViewport()
542{
543 if (d->m_favoriteItemsOnly) {
544 return;
545 }
546
547 // All this is to prevent to often downloads
548 if (d->m_lastNumber != 0
549 // We don't need to download if nothing changed
550 && (!(d->m_downloadedBox == d->m_lastBox) || d->m_downloadedNumber != d->m_lastNumber)
551 // We try to filter little changes of the bounding box
552 && (fabs(d->m_downloadedBox.east() - d->m_lastBox.east()) * boxComparisonFactor > d->m_lastBox.width()
553 || fabs(d->m_downloadedBox.south() - d->m_lastBox.south()) * boxComparisonFactor > d->m_lastBox.height()
554 || fabs(d->m_downloadedBox.north() - d->m_lastBox.north()) * boxComparisonFactor > d->m_lastBox.height()
555 || fabs(d->m_downloadedBox.west() - d->m_lastBox.west()) * boxComparisonFactor > d->m_lastBox.width())) {
556 // We will wait a little bit longer to start the
557 // next download as we will really download something now.
558 d->m_downloadTimer.setInterval(timeBetweenDownloads);
559
560 // Save the download parameter
561 d->m_downloadedBox = d->m_lastBox;
562 d->m_downloadedNumber = d->m_lastNumber;
563
564 // Get items
565 getAdditionalItems(d->m_lastBox, d->m_lastNumber);
566 } else {
567 // Don't wait to long to start the next download as we decided not to download anything.
568 // This will enhance response.
569 d->m_downloadTimer.setInterval(timeBetweenTriedDownloads);
570 }
571}
572
573void AbstractDataPluginModel::processFinishedJob(const QString &relativeUrlString, const QString &id)
574{
575 Q_UNUSED(relativeUrlString);
576
577 if (id.startsWith(descriptionPrefix)) {
578 parseFile(d->m_storagePolicy.data(id));
579 } else {
580 // The downloaded file contains item data.
581
582 // Splitting the id in itemId and fileType
583 QStringList fileInformation = id.split(fileIdSeparator);
584
585 if (fileInformation.size() < 2) {
586 mDebug() << "Strange file information " << id;
587 return;
588 }
589 QString itemId = fileInformation.at(0);
590 fileInformation.removeAt(0);
591 QString fileType = fileInformation.join(QString(fileIdSeparator));
592
593 // Searching for the right item in m_downloadingItems
594 QHash<QString, AbstractDataPluginItem *>::iterator i = d->m_downloadingItems.find(id);
595 if (i != d->m_downloadingItems.end()) {
596 if (itemId != (*i)->id()) {
597 return;
598 }
599
600 (*i)->addDownloadedFile(d->generateFilepath(itemId, fileType), fileType);
601
602 d->m_downloadingItems.erase(i);
603 }
604 }
605}
606
607void AbstractDataPluginModel::removeItem(QObject *item)
608{
609 auto pluginItem = qobject_cast<AbstractDataPluginItem *>(item);
610 d->m_itemSet.removeAll(pluginItem);
612 for (i = d->m_downloadingItems.begin(); i != d->m_downloadingItems.end(); ++i) {
613 if (*i == pluginItem) {
614 i = d->m_downloadingItems.erase(i);
615 }
616 }
617}
618
619void AbstractDataPluginModel::clear()
620{
621 d->m_displayedItems.clear();
622 QList<AbstractDataPluginItem *>::iterator iter = d->m_itemSet.begin();
623 QList<AbstractDataPluginItem *>::iterator const end = d->m_itemSet.end();
624 for (; iter != end; ++iter) {
625 (*iter)->deleteLater();
626 }
627 d->m_itemSet.clear();
628 d->m_lastBox = GeoDataLatLonAltBox();
629 d->m_downloadedBox = GeoDataLatLonAltBox();
630 d->m_downloadedNumber = 0;
631 Q_EMIT itemsUpdated();
632}
633
634void AbstractDataPluginModel::registerItemProperties(const QMetaObject &item)
635{
636 d->m_metaObject = item;
637 d->m_hasMetaObject = true;
638}
639
640} // namespace Marble
641
642#include "moc_AbstractDataPluginModel.cpp"
This file contains the headers for MarbleModel.
This file contains the headers for ViewportParams.
A class that defines a 3D bounding box for geographic data.
A public class that controls what is visible in the viewport of a Marble map.
QString name(GameStandardAction id)
VehicleSection::Type type(QStringView coachNumber, QStringView coachClassification)
KGuiItem clear()
Binds a QML item to a specific geodetic location in screen coordinates.
@ DownloadBrowse
Browsing mode, normal operation of Marble, like a web browser.
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const override
const char * constData() const const
void append(QList< T > &&value)
const_reference at(qsizetype i) const const
iterator begin()
const_iterator constBegin() const const
const_iterator constEnd() const const
iterator end()
qsizetype length() const const
void removeAt(qsizetype i)
bool removeOne(const AT &t)
qsizetype size() const const
bool isValid() const const
int row() const const
void destroyed(QObject *obj)
QObject * parent() const const
QString fromUtf8(QByteArrayView str)
QString number(double n, char format, int precision)
QByteArray toLatin1() const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
QString join(QChar separator) const const
QueuedConnection
DisplayRole
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
bool isEmpty() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 4 2024 16:37:02 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.