29#include <QElapsedTimer>
30#include <QNetworkReply>
31#include <QRegularExpression>
32#include <QStandardPaths>
35#include "moc_qgstiledownloadmanager.cpp"
37using namespace Qt::StringLiterals;
41QgsTileDownloadManagerWorker::QgsTileDownloadManagerWorker(
QgsTileDownloadManager *manager, QObject *parent )
46 connect( &mIdleTimer, &QTimer::timeout,
this, &QgsTileDownloadManagerWorker::idleTimerTimeout );
49void QgsTileDownloadManagerWorker::startIdleTimer()
51 if ( !mIdleTimer.isActive() )
53 mIdleTimer.start( mManager->mIdleThreadTimeoutMs );
57void QgsTileDownloadManagerWorker::queueUpdated()
59 const QMutexLocker locker( &mManager->mMutex );
61 if ( mManager->mShuttingDown )
69 std::vector< QNetworkReply * > replies;
70 replies.reserve( mManager->mQueue.size() );
71 for (
auto it = mManager->mQueue.begin(); it != mManager->mQueue.end(); ++it )
73 replies.emplace_back( it->networkReply );
76 for ( QNetworkReply *reply : replies )
85 if ( mIdleTimer.isActive() && !mManager->mQueue.empty() )
96 mManager->mStageQueueRemovals =
true;
97 for (
auto it = mManager->mQueue.begin(); it != mManager->mQueue.end(); ++it )
99 if ( !it->networkReply )
101 QgsDebugMsgLevel( u
"Tile download manager: starting request: "_s + it->request.url().toString(), 2 );
104 QNetworkRequest request( it->request );
105 request.setAttribute( QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy );
107 connect( it->networkReply, &QNetworkReply::finished, it->objWorker, &QgsTileDownloadManagerReplyWorkerObject::replyFinished );
109 ++mManager->mStats.networkRequestsStarted;
112 mManager->mStageQueueRemovals =
false;
113 mManager->processStagedEntryRemovals();
116void QgsTileDownloadManagerWorker::quitThread()
120 mManager->mWorker->deleteLater();
121 mManager->mWorker =
nullptr;
124 mManager->mWorkerThread->quit();
125 mManager->mWorkerThread =
nullptr;
126 mManager->mShuttingDown =
false;
129void QgsTileDownloadManagerWorker::idleTimerTimeout()
131 const QMutexLocker locker( &mManager->mMutex );
132 Q_ASSERT( mManager->mQueue.empty() );
140void QgsTileDownloadManagerReplyWorkerObject::replyFinished()
142 const QMutexLocker locker( &mManager->mMutex );
144 QgsDebugMsgLevel( u
"Tile download manager: internal reply finished: "_s + mRequest.url().toString(), 2 );
146 QNetworkReply *reply = qobject_cast<QNetworkReply *>( sender() );
149 if ( reply->error() == QNetworkReply::NoError )
151 ++mManager->mStats.networkRequestsOk;
152 data = reply->readAll();
156 ++mManager->mStats.networkRequestsFailed;
157 const QString contentType = reply->header( QNetworkRequest::ContentTypeHeader ).toString();
158 if ( contentType.startsWith(
"text/plain"_L1 ) )
159 data = reply->readAll();
162 QMap<QNetworkRequest::Attribute, QVariant> attributes;
163 attributes.insert( QNetworkRequest::SourceIsFromCacheAttribute, reply->attribute( QNetworkRequest::SourceIsFromCacheAttribute ) );
164 attributes.insert( QNetworkRequest::RedirectionTargetAttribute, reply->attribute( QNetworkRequest::RedirectionTargetAttribute ) );
165 attributes.insert( QNetworkRequest::HttpStatusCodeAttribute, reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ) );
166 attributes.insert( QNetworkRequest::HttpReasonPhraseAttribute, reply->attribute( QNetworkRequest::HttpReasonPhraseAttribute ) );
168 QMap<QNetworkRequest::KnownHeaders, QVariant> headers;
169 headers.insert( QNetworkRequest::ContentTypeHeader, reply->header( QNetworkRequest::ContentTypeHeader ) );
172 int httpStatusCode = reply->attribute( QNetworkRequest::Attribute::HttpStatusCodeAttribute ).toInt();
173 if ( httpStatusCode == 206 && mManager->isRangeRequest( mRequest ) )
175 mManager->mRangesCache->registerEntry( mRequest, data );
178 emit finished( data, reply->url(), attributes, headers, reply->rawHeaderPairs(), reply->error(), reply->errorString() );
180 reply->deleteLater();
185 mManager->removeEntry( mRequest );
187 if ( mManager->mQueue.empty() )
190 mManager->mWorker->startIdleTimer();
201 mRangesCache = std::make_unique<QgsRangeRequestCache>( );
205 if ( cacheDirectory.isEmpty() )
206 cacheDirectory = QStandardPaths::writableLocation( QStandardPaths::CacheLocation );
207 if ( !cacheDirectory.endsWith( QDir::separator() ) )
209 cacheDirectory.push_back( QDir::separator() );
211 cacheDirectory +=
"http-ranges"_L1;
212 mRangesCache->setCacheDirectory( cacheDirectory );
214 mRangesCache->setCacheSize( cacheSize );
225 const QMutexLocker locker( &mMutex );
227 if ( isCachedRangeRequest( request ) )
230 QTimer::singleShot( 0, reply, &QgsTileDownloadManagerReply::cachedRangeRequestFinished );
237 mWorkerThread =
new QThread;
239 mWorker->moveToThread( mWorkerThread );
240 QObject::connect( mWorkerThread, &QThread::finished, mWorker, &QObject::deleteLater );
241 mWorkerThread->start();
246 ++mStats.requestsTotal;
248 QgsTileDownloadManager::QueueEntry entry = findEntryForRequest( request );
249 if ( !entry.isValid() )
251 QgsDebugMsgLevel( u
"Tile download manager: get (new entry): "_s + request.url().toString(), 2 );
253 entry.request = request;
255 entry.objWorker->moveToThread( mWorkerThread );
257 QObject::connect( entry.objWorker, &QgsTileDownloadManagerReplyWorkerObject::finished, reply, &QgsTileDownloadManagerReply::requestFinished );
263 QgsDebugMsgLevel( u
"Tile download manager: get (existing entry): "_s + request.url().toString(), 2 );
265 QObject::connect( entry.objWorker, &QgsTileDownloadManagerReplyWorkerObject::finished, reply, &QgsTileDownloadManagerReply::requestFinished );
267 ++mStats.requestsMerged;
270 signalQueueModified();
277 const QMutexLocker locker( &mMutex );
279 return !mQueue.empty();
287 while ( msec == -1 || t.elapsed() < msec )
290 const QMutexLocker locker( &mMutex );
291 if ( mQueue.empty() )
294 QThread::usleep( 1000 );
303 const QMutexLocker locker( &mMutex );
304 if ( !mWorkerThread )
308 mShuttingDown =
true;
309 signalQueueModified();
316 const QMutexLocker locker( &mMutex );
317 if ( !mWorkerThread )
321 QThread::usleep( 1000 );
327 return mWorkerThread && mWorkerThread->isRunning();
332 const QMutexLocker locker( &mMutex );
336QgsTileDownloadManager::QueueEntry QgsTileDownloadManager::findEntryForRequest(
const QNetworkRequest &request )
338 for (
auto it = mQueue.begin(); it != mQueue.end(); ++it )
340 if ( it->request.url() == request.url() && it->request.rawHeader(
"Range" ) == request.rawHeader(
"Range" ) )
343 return QgsTileDownloadManager::QueueEntry();
346void QgsTileDownloadManager::addEntry(
const QgsTileDownloadManager::QueueEntry &entry )
348 for (
auto it = mQueue.begin(); it != mQueue.end(); ++it )
350 Q_ASSERT( entry.request.url() != it->request.url() || entry.request.rawHeader(
"Range" ) != it->request.rawHeader(
"Range" ) );
353 mQueue.emplace_back( entry );
356void QgsTileDownloadManager::updateEntry(
const QgsTileDownloadManager::QueueEntry &entry )
358 for (
auto it = mQueue.begin(); it != mQueue.end(); ++it )
360 if ( entry.request.url() == it->request.url() && entry.request.rawHeader(
"Range" ) == it->request.rawHeader(
"Range" ) )
369void QgsTileDownloadManager::removeEntry(
const QNetworkRequest &request )
371 if ( mStageQueueRemovals )
373 mStagedQueueRemovals.emplace_back( request );
377 for (
auto it = mQueue.begin(); it != mQueue.end(); ++it )
379 if ( it->request.url() == request.url() && it->request.rawHeader(
"Range" ) == request.rawHeader(
"Range" ) )
389void QgsTileDownloadManager::processStagedEntryRemovals()
391 Q_ASSERT( !mStageQueueRemovals );
392 for (
const QNetworkRequest &request : mStagedQueueRemovals )
394 removeEntry( request );
396 mStagedQueueRemovals.clear();
399void QgsTileDownloadManager::signalQueueModified()
401 QMetaObject::invokeMethod( mWorker, &QgsTileDownloadManagerWorker::queueUpdated, Qt::QueuedConnection );
404bool QgsTileDownloadManager::isRangeRequest(
const QNetworkRequest &request )
406 if ( request.rawHeader(
"Range" ).isEmpty() )
408 const thread_local QRegularExpression regex(
"^bytes=\\d+-\\d+$" );
409 QRegularExpressionMatch match = regex.match( QString::fromUtf8( request.rawHeader(
"Range" ) ) );
410 return match.hasMatch();
413bool QgsTileDownloadManager::isCachedRangeRequest(
const QNetworkRequest &request )
415 QNetworkRequest::CacheLoadControl loadControl = ( QNetworkRequest::CacheLoadControl ) request.attribute( QNetworkRequest::CacheLoadControlAttribute ).toInt();
416 bool saveControl = request.attribute( QNetworkRequest::CacheSaveControlAttribute ).toBool();
417 return isRangeRequest( request ) && saveControl && loadControl != QNetworkRequest::AlwaysNetwork && mRangesCache->hasEntry( request );
423QgsTileDownloadManagerReply::QgsTileDownloadManagerReply(
QgsTileDownloadManager *manager,
const QNetworkRequest &request )
424 : mManager( manager )
425 , mRequest( request )
431 const QMutexLocker locker( &mManager->mMutex );
435 QgsDebugMsgLevel( u
"Tile download manager: reply deleted before finished: "_s + mRequest.url().toString(), 2 );
437 ++mManager->mStats.requestsEarlyDeleted;
441void QgsTileDownloadManagerReply::requestFinished( QByteArray data, QUrl url,
const QMap<QNetworkRequest::Attribute, QVariant> &attributes,
const QMap<QNetworkRequest::KnownHeaders, QVariant> &headers,
const QList<QNetworkReply::RawHeaderPair> rawHeaderPairs, QNetworkReply::NetworkError error,
const QString &errorString )
443 QgsDebugMsgLevel( u
"Tile download manager: reply finished: "_s + mRequest.url().toString(), 2 );
448 mAttributes = attributes;
456void QgsTileDownloadManagerReply::cachedRangeRequestFinished()
458 QgsDebugMsgLevel( u
"Tile download manager: internal range request reply loaded from cache: "_s + mRequest.url().toString(), 2 );
460 mData = mManager->mRangesCache->entry( mRequest );
461 mUrl = mRequest.url();
467 return mAttributes.contains( code ) ? mAttributes.value( code ) : QVariant();
472 return mHeaders.contains(
header ) ? mHeaders.value(
header ) : QVariant();
static QgsNetworkAccessManager * instance(Qt::ConnectionType connectionType=Qt::BlockingQueuedConnection)
Returns a pointer to the active QgsNetworkAccessManager for the current thread.
static const QgsSettingsEntryInteger64 * settingsNetworkCacheSize
Settings entry network cache directory.
static const QgsSettingsEntryString * settingsNetworkCacheDirectory
Settings entry network cache directory.
Stores settings for use within QGIS.
QString errorString() const
Returns error string (only valid when already finished).
~QgsTileDownloadManagerReply() override
const QList< QNetworkReply::RawHeaderPair > rawHeaderPairs() const
Returns a list of raw header pairs.
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).
QUrl url() const
Returns the reply URL.
QVariant header(QNetworkRequest::KnownHeaders header)
Returns the value of the known header header.
QVariant attribute(QNetworkRequest::Attribute code)
Returns the attribute associated with the code.
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.
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) const
Blocks the current thread until the queue is empty.
QgsTileDownloadManagerReply * get(const QNetworkRequest &request)
Starts a request.
friend class QgsTileDownloadManagerReply
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.
friend class QgsTileDownloadManagerWorker
~QgsTileDownloadManager()
void shutdown()
Asks the worker thread to stop and blocks until it is not stopped.
#define QgsDebugMsgLevel(str, level)