QGIS API Documentation  3.21.0-Master (5b68dc587e)
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  const 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  const QMutexLocker locker( &mManager->mMutex );
97  Q_ASSERT( mManager->mQueue.isEmpty() );
98  quitThread();
99 }
100 
101 
103 
104 
105 void QgsTileDownloadManagerReplyWorkerObject::replyFinished()
106 {
107  const 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 #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
148  : mMutex( QMutex::Recursive )
149 #endif
150 {
151 }
152 
154 {
155  // make sure the worker thread is gone and any pending requests are canceled
156  shutdown();
157 }
158 
160 {
161  const QMutexLocker locker( &mMutex );
162 
163  if ( !mWorker )
164  {
165  QgsDebugMsgLevel( QStringLiteral( "Tile download manager: starting worker thread" ), 2 );
166  mWorkerThread = new QThread;
167  mWorker = new QgsTileDownloadManagerWorker( this );
168  mWorker->moveToThread( mWorkerThread );
169  QObject::connect( mWorkerThread, &QThread::finished, mWorker, &QObject::deleteLater );
170  mWorkerThread->start();
171  }
172 
173  QgsTileDownloadManagerReply *reply = new QgsTileDownloadManagerReply( this, request ); // lives in the same thread as the caller
174 
175  ++mStats.requestsTotal;
176 
177  QgsTileDownloadManager::QueueEntry entry = findEntryForRequest( request );
178  if ( !entry.isValid() )
179  {
180  QgsDebugMsgLevel( QStringLiteral( "Tile download manager: get (new entry): " ) + request.url().toString(), 2 );
181  // create a new entry and add it to queue
182  entry.request = request;
183  entry.objWorker = new QgsTileDownloadManagerReplyWorkerObject( this, request );
184  entry.objWorker->moveToThread( mWorkerThread );
185 
186  QObject::connect( entry.objWorker, &QgsTileDownloadManagerReplyWorkerObject::finished, reply, &QgsTileDownloadManagerReply::requestFinished ); // should be queued connection
187 
188  addEntry( entry );
189  }
190  else
191  {
192  QgsDebugMsgLevel( QStringLiteral( "Tile download manager: get (existing entry): " ) + request.url().toString(), 2 );
193 
194  QObject::connect( entry.objWorker, &QgsTileDownloadManagerReplyWorkerObject::finished, reply, &QgsTileDownloadManagerReply::requestFinished ); // should be queued connection
195 
196  ++mStats.requestsMerged;
197  }
198 
199  signalQueueModified();
200 
201  return reply;
202 }
203 
205 {
206  const QMutexLocker locker( &mMutex );
207 
208  return !mQueue.isEmpty();
209 }
210 
212 {
213  QElapsedTimer t;
214  t.start();
215 
216  while ( msec == -1 || t.elapsed() < msec )
217  {
218  {
219  const QMutexLocker locker( &mMutex );
220  if ( mQueue.isEmpty() )
221  return true;
222  }
223  QThread::currentThread()->usleep( 1000 );
224  }
225 
226  return false;
227 }
228 
230 {
231  {
232  const QMutexLocker locker( &mMutex );
233  if ( !mWorkerThread )
234  return; // nothing to stop
235 
236  // let's signal to the thread
237  mShuttingDown = true;
238  signalQueueModified();
239  }
240 
241  // wait until the thread is gone
242  while ( 1 )
243  {
244  {
245  const QMutexLocker locker( &mMutex );
246  if ( !mWorkerThread )
247  return; // the thread has stopped
248  }
249 
250  QThread::currentThread()->usleep( 1000 );
251  }
252 }
253 
255 {
256  return mWorkerThread && mWorkerThread->isRunning();
257 }
258 
260 {
261  const QMutexLocker locker( &mMutex );
263 }
264 
265 QgsTileDownloadManager::QueueEntry QgsTileDownloadManager::findEntryForRequest( const QNetworkRequest &request )
266 {
267  for ( auto it = mQueue.constBegin(); it != mQueue.constEnd(); ++it )
268  {
269  if ( it->request.url() == request.url() )
270  return *it;
271  }
272  return QgsTileDownloadManager::QueueEntry();
273 }
274 
275 void QgsTileDownloadManager::addEntry( const QgsTileDownloadManager::QueueEntry &entry )
276 {
277  for ( auto it = mQueue.constBegin(); it != mQueue.constEnd(); ++it )
278  {
279  Q_ASSERT( entry.request.url() != it->request.url() );
280  }
281 
282  mQueue.append( entry );
283 }
284 
285 void QgsTileDownloadManager::updateEntry( const QgsTileDownloadManager::QueueEntry &entry )
286 {
287  for ( auto it = mQueue.begin(); it != mQueue.end(); ++it )
288  {
289  if ( entry.request.url() == it->request.url() )
290  {
291  *it = entry;
292  return;
293  }
294  }
295  Q_ASSERT( false );
296 }
297 
298 void QgsTileDownloadManager::removeEntry( const QNetworkRequest &request )
299 {
300  int i = 0;
301  for ( auto it = mQueue.constBegin(); it != mQueue.constEnd(); ++it, ++i )
302  {
303  if ( it->request.url() == request.url() )
304  {
305  mQueue.removeAt( i );
306  return;
307  }
308  }
309  Q_ASSERT( false );
310 }
311 
312 void QgsTileDownloadManager::signalQueueModified()
313 {
314  QMetaObject::invokeMethod( mWorker, &QgsTileDownloadManagerWorker::queueUpdated, Qt::QueuedConnection );
315 }
316 
317 
319 
320 
321 QgsTileDownloadManagerReply::QgsTileDownloadManagerReply( QgsTileDownloadManager *manager, const QNetworkRequest &request )
322  : mManager( manager )
323  , mRequest( request )
324 {
325 }
326 
328 {
329  const QMutexLocker locker( &mManager->mMutex );
330 
331  if ( !mHasFinished )
332  {
333  QgsDebugMsgLevel( QStringLiteral( "Tile download manager: reply deleted before finished: " ) + mRequest.url().toString(), 2 );
334 
335  ++mManager->mStats.requestsEarlyDeleted;
336  }
337 }
338 
339 void QgsTileDownloadManagerReply::requestFinished( QByteArray data, QNetworkReply::NetworkError error, const QString &errorString )
340 {
341  QgsDebugMsgLevel( QStringLiteral( "Tile download manager: reply finished: " ) + mRequest.url().toString(), 2 );
342 
343  mHasFinished = true;
344  mData = data;
345  mError = error;
346  mErrorString = errorString;
347  emit finished();
348 }
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