QGIS API Documentation  3.24.2-Tisler (13c1a02865)
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  data = reply->readAll();
118  }
119  else
120  {
121  ++mManager->mStats.networkRequestsFailed;
122  const QString contentType = reply->header( QNetworkRequest::ContentTypeHeader ).toString();
123  if ( contentType.startsWith( QLatin1String( "text/plain" ) ) )
124  data = reply->readAll();
125  }
126 
127  emit finished( data, reply->error(), reply->errorString() );
128 
129  reply->deleteLater();
130 
131  // kill the worker obj
132  deleteLater();
133 
134  mManager->removeEntry( mRequest );
135 
136  if ( mManager->mQueue.isEmpty() )
137  {
138  // if this was the last thing in the queue, start a timer to kill thread after X seconds
139  mManager->mWorker->startIdleTimer();
140  }
141 }
142 
144 
146 
147 
149 #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
150  : mMutex( QMutex::Recursive )
151 #endif
152 {
153 }
154 
156 {
157  // make sure the worker thread is gone and any pending requests are canceled
158  shutdown();
159 }
160 
162 {
163  const QMutexLocker locker( &mMutex );
164 
165  if ( !mWorker )
166  {
167  QgsDebugMsgLevel( QStringLiteral( "Tile download manager: starting worker thread" ), 2 );
168  mWorkerThread = new QThread;
169  mWorker = new QgsTileDownloadManagerWorker( this );
170  mWorker->moveToThread( mWorkerThread );
171  QObject::connect( mWorkerThread, &QThread::finished, mWorker, &QObject::deleteLater );
172  mWorkerThread->start();
173  }
174 
175  QgsTileDownloadManagerReply *reply = new QgsTileDownloadManagerReply( this, request ); // lives in the same thread as the caller
176 
177  ++mStats.requestsTotal;
178 
179  QgsTileDownloadManager::QueueEntry entry = findEntryForRequest( request );
180  if ( !entry.isValid() )
181  {
182  QgsDebugMsgLevel( QStringLiteral( "Tile download manager: get (new entry): " ) + request.url().toString(), 2 );
183  // create a new entry and add it to queue
184  entry.request = request;
185  entry.objWorker = new QgsTileDownloadManagerReplyWorkerObject( this, request );
186  entry.objWorker->moveToThread( mWorkerThread );
187 
188  QObject::connect( entry.objWorker, &QgsTileDownloadManagerReplyWorkerObject::finished, reply, &QgsTileDownloadManagerReply::requestFinished ); // should be queued connection
189 
190  addEntry( entry );
191  }
192  else
193  {
194  QgsDebugMsgLevel( QStringLiteral( "Tile download manager: get (existing entry): " ) + request.url().toString(), 2 );
195 
196  QObject::connect( entry.objWorker, &QgsTileDownloadManagerReplyWorkerObject::finished, reply, &QgsTileDownloadManagerReply::requestFinished ); // should be queued connection
197 
198  ++mStats.requestsMerged;
199  }
200 
201  signalQueueModified();
202 
203  return reply;
204 }
205 
207 {
208  const QMutexLocker locker( &mMutex );
209 
210  return !mQueue.isEmpty();
211 }
212 
214 {
215  QElapsedTimer t;
216  t.start();
217 
218  while ( msec == -1 || t.elapsed() < msec )
219  {
220  {
221  const QMutexLocker locker( &mMutex );
222  if ( mQueue.isEmpty() )
223  return true;
224  }
225  QThread::usleep( 1000 );
226  }
227 
228  return false;
229 }
230 
232 {
233  {
234  const QMutexLocker locker( &mMutex );
235  if ( !mWorkerThread )
236  return; // nothing to stop
237 
238  // let's signal to the thread
239  mShuttingDown = true;
240  signalQueueModified();
241  }
242 
243  // wait until the thread is gone
244  while ( 1 )
245  {
246  {
247  const QMutexLocker locker( &mMutex );
248  if ( !mWorkerThread )
249  return; // the thread has stopped
250  }
251 
252  QThread::usleep( 1000 );
253  }
254 }
255 
257 {
258  return mWorkerThread && mWorkerThread->isRunning();
259 }
260 
262 {
263  const QMutexLocker locker( &mMutex );
265 }
266 
267 QgsTileDownloadManager::QueueEntry QgsTileDownloadManager::findEntryForRequest( const QNetworkRequest &request )
268 {
269  for ( auto it = mQueue.constBegin(); it != mQueue.constEnd(); ++it )
270  {
271  if ( it->request.url() == request.url() )
272  return *it;
273  }
274  return QgsTileDownloadManager::QueueEntry();
275 }
276 
277 void QgsTileDownloadManager::addEntry( const QgsTileDownloadManager::QueueEntry &entry )
278 {
279  for ( auto it = mQueue.constBegin(); it != mQueue.constEnd(); ++it )
280  {
281  Q_ASSERT( entry.request.url() != it->request.url() );
282  }
283 
284  mQueue.append( entry );
285 }
286 
287 void QgsTileDownloadManager::updateEntry( const QgsTileDownloadManager::QueueEntry &entry )
288 {
289  for ( auto it = mQueue.begin(); it != mQueue.end(); ++it )
290  {
291  if ( entry.request.url() == it->request.url() )
292  {
293  *it = entry;
294  return;
295  }
296  }
297  Q_ASSERT( false );
298 }
299 
300 void QgsTileDownloadManager::removeEntry( const QNetworkRequest &request )
301 {
302  int i = 0;
303  for ( auto it = mQueue.constBegin(); it != mQueue.constEnd(); ++it, ++i )
304  {
305  if ( it->request.url() == request.url() )
306  {
307  mQueue.removeAt( i );
308  return;
309  }
310  }
311  Q_ASSERT( false );
312 }
313 
314 void QgsTileDownloadManager::signalQueueModified()
315 {
316  QMetaObject::invokeMethod( mWorker, &QgsTileDownloadManagerWorker::queueUpdated, Qt::QueuedConnection );
317 }
318 
319 
321 
322 
323 QgsTileDownloadManagerReply::QgsTileDownloadManagerReply( QgsTileDownloadManager *manager, const QNetworkRequest &request )
324  : mManager( manager )
325  , mRequest( request )
326 {
327 }
328 
330 {
331  const 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