Marble

AbstractDataPluginModel.cpp
1 // SPDX-License-Identifier: LGPL-2.1-or-later
2 //
3 // SPDX-FileCopyrightText: 2009 Bastian Holst <[email protected]>
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 
32 namespace Marble
33 {
34 
35 const QString descriptionPrefix( "description_" );
36 
37 // Time between two tried description file downloads (we decided not to download anything) in ms
38 const int timeBetweenTriedDownloads = 500;
39 // Time between two real description file downloads in ms
40 const 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.
44 const qreal boxComparisonFactor = 16.0;
45 
46 // Separator to separate the id of the item from the file type
47 const QChar fileIdSeparator = QLatin1Char('_');
48 
49 class FavoritesModel;
50 
51 class AbstractDataPluginModelPrivate
52 {
53 public:
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;
74  QHash<QString, AbstractDataPluginItem*> m_downloadingItems;
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 
90 class FavoritesModel : public QAbstractListModel
91 {
92 public:
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 
105 private:
106  QHash<int, QByteArray> m_roleNames;
107 };
108 
109 AbstractDataPluginModelPrivate::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 
132 AbstractDataPluginModelPrivate::~AbstractDataPluginModelPrivate() {
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 
148 void 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 
159 void 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 
167 static 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 
187 FavoritesModel::FavoritesModel( AbstractDataPluginModelPrivate *_d, QObject* parent ) :
188  QAbstractListModel( parent ), d(_d)
189 {
190  QHash<int,QByteArray> roles;
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 
201 int 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 
217 QVariant 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 
236 void FavoritesModel::reset()
237 {
238  beginResetModel();
239  endResetModel();
240 }
241 
242 QHash<int, QByteArray> FavoritesModel::roleNames() const
243 {
244  return m_roleNames;
245 }
246 
247 AbstractDataPluginModel::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 
267 AbstractDataPluginModel::~AbstractDataPluginModel()
268 {
269  delete d;
270 }
271 
272 const MarbleModel *AbstractDataPluginModel::marbleModel() const
273 {
274  return d->m_marbleModel;
275 }
276 
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 [email protected]" );
284  Q_ASSERT( !d->m_itemSet.contains( 0 ) && "Null item in m_itemSet. Please report a bug to [email protected]" );
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 
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 
368 {
369  Q_UNUSED( file );
370 }
371 
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 
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 
397 void AbstractDataPluginModel::addItemToList( AbstractDataPluginItem *item )
398 {
400 }
401 
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 
456 {
457  qWarning() << "Retrieving items by identifier is not implemented by this plugin";
458 }
459 
460 void 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 
472 QStringList AbstractDataPluginModel::favoriteItems() const
473 {
474  return d->m_favoriteItems;
475 }
476 
477 void AbstractDataPluginModel::setFavoriteItemsOnly( bool favoriteOnly )
478 {
479  if ( isFavoriteItemsOnly() != favoriteOnly ) {
480  d->m_favoriteItemsOnly = favoriteOnly;
481  d->updateFavoriteItems();
482  emit favoriteItemsOnlyChanged();
483  }
484 }
485 
486 bool AbstractDataPluginModel::isFavoriteItemsOnly() const
487 {
488  return d->m_favoriteItemsOnly;
489 }
490 
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 
501 void 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 
516 void AbstractDataPluginModel::scheduleItemSort()
517 {
518  d->m_needsSorting = true;
519 }
520 
521 QString AbstractDataPluginModelPrivate::generateFilename(const QString &id, const QString &type)
522 {
523  QString name;
524  name += id;
525  name += fileIdSeparator;
526  name += type;
527 
528  return name;
529 }
530 
531 QString AbstractDataPluginModelPrivate::generateFilepath( const QString& id, const QString& type ) const
532 {
533  return MarbleDirs::localPath() + QLatin1String("/cache/") + m_name + QLatin1Char('/') + generateFilename(id, type);
534 }
535 
536 AbstractDataPluginItem *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 
548 {
549  return findItem( id );
550 }
551 
553 {
554  d->m_itemSettings = itemSettings;
555 }
556 
557 void 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 
596 void 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 
633 void 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 
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 
660 void 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"
void append(const T &value)
A class that defines a 3D bounding box for geographic data.
DisplayRole
@ DownloadBrowse
Browsing mode, normal operation of Marble, like a web browser.
Definition: MarbleGlobal.h:155
QString number(int n, int base)
Type type(const QSqlDatabase &db)
bool contains(const QString &str, Qt::CaseSensitivity cs) const const
QHash::iterator begin()
int length() const const
void addItemToList(AbstractDataPluginItem *item)
Convenience method to add one item to the list.
QByteArray toLatin1() const const
QList::const_iterator constBegin() const const
const MarbleModel * marbleModel() const
Access to the MarbleModel.
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
void removeAt(int i)
void destroyed(QObject *obj)
int size() const const
virtual void getAdditionalItems(const GeoDataLatLonAltBox &box, qint32 number=10)=0
Managing to get number additional items in box.
AbstractDataPluginItem * findItem(const QString &id) const
Finds the item with id in the list.
A public class that controls what is visible in the viewport of a Marble map.
bool isEmpty() const const
bool removeOne(const T &value)
const T & at(int i) const const
QueuedConnection
Binds a QML item to a specific geodetic location in screen coordinates.
QString join(const QString &separator) const const
virtual void parseFile(const QByteArray &file)
Parse the file and generate items.
bool isValid() const const
int row() const const
QList< AbstractDataPluginItem * > whichItemAt(const QPoint &curpos)
Get all items that contain the given point Returns a list of all items that contain the point curpos.
void downloadDescriptionFile(const QUrl &url)
Download the description file from the url.
QList::const_iterator constEnd() const const
const char * constData() const const
QString name(StandardShortcut id)
void setItemSettings(const QHash< QString, QVariant > &itemSettings)
Sets the settings for all items.
KGuiItem reset()
QList::iterator begin()
void downloadItem(const QUrl &url, const QString &type, AbstractDataPluginItem *item)
Downloads the file from url.
bool itemExists(const QString &id) const
Testing the existence of the item id in the list.
QList::iterator end()
void addItemsToList(const QList< AbstractDataPluginItem * > &items)
Adds the items to the list of initialized items.
QList< AbstractDataPluginItem * > items(const ViewportParams *viewport, qint32 number=10)
Get the items on the viewport Returns the currently downloaded images in the viewport.
QDebug mDebug()
a function to replace qDebug() in Marble library code
Definition: MarbleDebug.cpp:31
QHash::iterator end()
virtual void getItem(const QString &id)
Retrieve data for a specific item.
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Mon Oct 2 2023 03:52:07 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.