QGIS API Documentation  3.18.1-Zürich (202f1bf7e5)
qgstiledownloadmanager.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgstiledownloadmanager.cpp
3  --------------------------
4  begin : January 2021
5  copyright : (C) 2021 by Martin Dobias
6  email : wonder dot sk at gmail dot com
7  ***************************************************************************/
8 
9 /***************************************************************************
10  * *
11  * This program is free software; you can redistribute it and/or modify *
12  * it under the terms of the GNU General Public License as published by *
13  * the Free Software Foundation; either version 2 of the License, or *
14  * (at your option) any later version. *
15  * *
16  ***************************************************************************/
17 
18 #include "qgstiledownloadmanager.h"
19 
20 #include "qgslogger.h"
22 
23 #include <QElapsedTimer>
24 #include <QNetworkReply>
25 
26 
28 
29 QgsTileDownloadManagerWorker::QgsTileDownloadManagerWorker( QgsTileDownloadManager *manager, QObject *parent )
30  : QObject( parent )
31  , mManager( manager )
32  , mIdleTimer( this )
33 {
34  connect( &mIdleTimer, &QTimer::timeout, this, &QgsTileDownloadManagerWorker::idleTimerTimeout );
35 }
36 
37 void QgsTileDownloadManagerWorker::startIdleTimer()
38 {
39  if ( !mIdleTimer.isActive() )
40  {
41  mIdleTimer.start( mManager->mIdleThreadTimeoutMs );
42  }
43 }
44 
45 void QgsTileDownloadManagerWorker::queueUpdated()
46 {
47  QMutexLocker locker( &mManager->mMutex );
48 
49  if ( mManager->mShuttingDown )
50  {
51  for ( auto it = mManager->mQueue.begin(); it != mManager->mQueue.end(); ++it )
52  {
53  it->networkReply->abort();
54  }
55 
56  quitThread();
57  return;
58  }
59 
60  if ( mIdleTimer.isActive() && !mManager->mQueue.isEmpty() )
61  {
62  // if timer to kill thread is running: stop the timer, we have work to do
63  mIdleTimer.stop();
64  }
65 
66  for ( auto it = mManager->mQueue.begin(); it != mManager->mQueue.end(); ++it )
67  {
68  if ( !it->networkReply )
69  {
70  QgsDebugMsgLevel( QStringLiteral( "Tile download manager: starting request: " ) + it->request.url().toString(), 2 );
71  // start entries which are not in progress
72 
73  it->networkReply = QgsNetworkAccessManager::instance()->get( it->request );
74  connect( it->networkReply, &QNetworkReply::finished, it->objWorker, &QgsTileDownloadManagerReplyWorkerObject::replyFinished );
75 
76  ++mManager->mStats.networkRequestsStarted;
77  }
78  }
79 }
80 
81 void QgsTileDownloadManagerWorker::quitThread()
82 {
83  QgsDebugMsgLevel( QStringLiteral( "Tile download manager: stopping worker thread" ), 2 );
84 
85  mManager->mWorker->deleteLater();
86  mManager->mWorker = nullptr;
87  // we signal to our worker thread it's time to go. Its finished() signal is connected
88  // to deleteLater() call, so it will get deleted automatically
89  mManager->mWorkerThread->quit();
90  mManager->mWorkerThread = nullptr;
91  mManager->mShuttingDown = false;
92 }
93 
94 void QgsTileDownloadManagerWorker::idleTimerTimeout()
95 {
96  QMutexLocker locker( &mManager->mMutex );
97  Q_ASSERT( mManager->mQueue.isEmpty() );
98  quitThread();
99 }
100 
101 
103 
104 
105 void QgsTileDownloadManagerReplyWorkerObject::replyFinished()
106 {
107  QMutexLocker locker( &mManager->mMutex );
108 
109  QgsDebugMsgLevel( QStringLiteral( "Tile download manager: internal reply finished: " ) + mRequest.url().toString(), 2 );
110 
111  QNetworkReply *reply = qobject_cast<QNetworkReply *>( sender() );
112  QByteArray data;
113 
114  if ( reply->error() == QNetworkReply::NoError )
115  {
116  ++mManager->mStats.networkRequestsOk;
117 
118  data = reply->readAll();
119  }
120  else
121  {
122  ++mManager->mStats.networkRequestsFailed;
123  }
124 
125  emit finished( data, reply->error(), reply->errorString() );
126 
127  reply->deleteLater();
128 
129  // kill the worker obj
130  deleteLater();
131 
132  mManager->removeEntry( mRequest );
133 
134  if ( mManager->mQueue.isEmpty() )
135  {
136  // if this was the last thing in the queue, start a timer to kill thread after X seconds
137  mManager->mWorker->startIdleTimer();
138  }
139 }
140 
142 
144 
145 
147  : mMutex( QMutex::Recursive )
148 {
149 }
150 
152 {
153  // make sure the worker thread is gone and any pending requests are canceled
154  shutdown();
155 }
156 
158 {
159  QMutexLocker locker( &mMutex );
160 
161  if ( !mWorker )
162  {
163  QgsDebugMsgLevel( QStringLiteral( "Tile download manager: starting worker thread" ), 2 );
164  mWorkerThread = new QThread;
165  mWorker = new QgsTileDownloadManagerWorker( this );
166  mWorker->moveToThread( mWorkerThread );
167  QObject::connect( mWorkerThread, &QThread::finished, mWorker, &QObject::deleteLater );
168  mWorkerThread->start();
169  }
170 
171  QgsTileDownloadManagerReply *reply = new QgsTileDownloadManagerReply( this, request ); // lives in the same thread as the caller
172 
173  ++mStats.requestsTotal;
174 
175  QgsTileDownloadManager::QueueEntry entry = findEntryForRequest( request );
176  if ( !entry.isValid() )
177  {
178  QgsDebugMsgLevel( QStringLiteral( "Tile download manager: get (new entry): " ) + request.url().toString(), 2 );
179  // create a new entry and add it to queue
180  entry.request = request;
181  entry.objWorker = new QgsTileDownloadManagerReplyWorkerObject( this, request );
182  entry.objWorker->moveToThread( mWorkerThread );
183 
184  QObject::connect( entry.objWorker, &QgsTileDownloadManagerReplyWorkerObject::finished, reply, &QgsTileDownloadManagerReply::requestFinished ); // should be queued connection
185 
186  addEntry( entry );
187  }
188  else
189  {
190  QgsDebugMsgLevel( QStringLiteral( "Tile download manager: get (existing entry): " ) + request.url().toString(), 2 );
191 
192  QObject::connect( entry.objWorker, &QgsTileDownloadManagerReplyWorkerObject::finished, reply, &QgsTileDownloadManagerReply::requestFinished ); // should be queued connection
193 
194  ++mStats.requestsMerged;
195  }
196 
197  signalQueueModified();
198 
199  return reply;
200 }
201 
203 {
204  QMutexLocker locker( &mMutex );
205 
206  return !mQueue.isEmpty();
207 }
208 
210 {
211  QElapsedTimer t;
212  t.start();
213 
214  while ( msec == -1 || t.elapsed() < msec )
215  {
216  {
217  QMutexLocker locker( &mMutex );
218  if ( mQueue.isEmpty() )
219  return true;
220  }
221  QThread::currentThread()->usleep( 1000 );
222  }
223 
224  return false;
225 }
226 
228 {
229  {
230  QMutexLocker locker( &mMutex );
231  if ( !mWorkerThread )
232  return; // nothing to stop
233 
234  // let's signal to the thread
235  mShuttingDown = true;
236  signalQueueModified();
237  }
238 
239  // wait until the thread is gone
240  while ( 1 )
241  {
242  {
243  QMutexLocker locker( &mMutex );
244  if ( !mWorkerThread )
245  return; // the thread has stopped
246  }
247 
248  QThread::currentThread()->usleep( 1000 );
249  }
250 }
251 
253 {
254  return mWorkerThread && mWorkerThread->isRunning();
255 }
256 
258 {
259  QMutexLocker locker( &mMutex );
261 }
262 
263 QgsTileDownloadManager::QueueEntry QgsTileDownloadManager::findEntryForRequest( const QNetworkRequest &request )
264 {
265  for ( auto it = mQueue.constBegin(); it != mQueue.constEnd(); ++it )
266  {
267  if ( it->request.url() == request.url() )
268  return *it;
269  }
270  return QgsTileDownloadManager::QueueEntry();
271 }
272 
273 void QgsTileDownloadManager::addEntry( const QgsTileDownloadManager::QueueEntry &entry )
274 {
275  for ( auto it = mQueue.constBegin(); it != mQueue.constEnd(); ++it )
276  {
277  Q_ASSERT( entry.request.url() != it->request.url() );
278  }
279 
280  mQueue.append( entry );
281 }
282 
283 void QgsTileDownloadManager::updateEntry( const QgsTileDownloadManager::QueueEntry &entry )
284 {
285  for ( auto it = mQueue.begin(); it != mQueue.end(); ++it )
286  {
287  if ( entry.request.url() == it->request.url() )
288  {
289  *it = entry;
290  return;
291  }
292  }
293  Q_ASSERT( false );
294 }
295 
296 void QgsTileDownloadManager::removeEntry( const QNetworkRequest &request )
297 {
298  int i = 0;
299  for ( auto it = mQueue.constBegin(); it != mQueue.constEnd(); ++it, ++i )
300  {
301  if ( it->request.url() == request.url() )
302  {
303  mQueue.removeAt( i );
304  return;
305  }
306  }
307  Q_ASSERT( false );
308 }
309 
310 void QgsTileDownloadManager::signalQueueModified()
311 {
312 #if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
313  QMetaObject::invokeMethod( mWorker, "queueUpdated", Qt::QueuedConnection );
314 #else
315  QMetaObject::invokeMethod( mWorker, &QgsTileDownloadManagerWorker::queueUpdated, Qt::QueuedConnection );
316 #endif
317 }
318 
319 
321 
322 
323 QgsTileDownloadManagerReply::QgsTileDownloadManagerReply( QgsTileDownloadManager *manager, const QNetworkRequest &request )
324  : mManager( manager )
325  , mRequest( request )
326 {
327 }
328 
330 {
331  QMutexLocker locker( &mManager->mMutex );
332 
333  if ( !mHasFinished )
334  {
335  QgsDebugMsgLevel( QStringLiteral( "Tile download manager: reply deleted before finished: " ) + mRequest.url().toString(), 2 );
336 
337  ++mManager->mStats.requestsEarlyDeleted;
338  }
339 }
340 
341 void QgsTileDownloadManagerReply::requestFinished( QByteArray data, QNetworkReply::NetworkError error, const QString &errorString )
342 {
343  QgsDebugMsgLevel( QStringLiteral( "Tile download manager: reply finished: " ) + mRequest.url().toString(), 2 );
344 
345  mHasFinished = true;
346  mData = data;
347  mError = error;
348  mErrorString = errorString;
349  emit finished();
350 }
static QgsNetworkAccessManager * instance(Qt::ConnectionType connectionType=Qt::BlockingQueuedConnection)
Returns a pointer to the active QgsNetworkAccessManager for the current thread.
Reply object for tile download manager requests returned from calls to QgsTileDownloadManager::get().
QString errorString() const
Returns error string (only valid when already finished)
QByteArray data() const
Returns binary data returned in the reply (only valid when already finished)
QNetworkReply::NetworkError error() const
Returns error code (only valid when already finished)
void finished()
Emitted when the reply has finished (either with a success or with a failure)
Encapsulates any statistics we would like to keep about requests.
int requestsMerged
How many requests were same as some other pending request and got "merged".
int requestsEarlyDeleted
How many requests were deleted early by the client (i.e. lost interest)
int requestsTotal
How many requests were done through the download manager.
Tile download manager handles downloads of map tiles for the purpose of map rendering.
bool hasWorkerThreadRunning() const
Returns whether the worker thread is running currently (it may be stopped if there were no requests r...
friend class QgsTileDownloadManagerReplyWorkerObject
bool waitForPendingRequests(int msec=-1)
Blocks the current thread until the queue is empty.
QgsTileDownloadManagerReply * get(const QNetworkRequest &request)
Starts a request.
bool hasPendingRequests() const
Returns whether there are any pending requests in the queue.
void resetStatistics()
Resets statistics of numbers of queries handled by this class.
void shutdown()
Asks the worker thread to stop and blocks until it is not stopped.
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39