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

KDE's Doxygen guidelines are available online.