29#include <QElapsedTimer>
30#include <QNetworkReply>
31#include <QRegularExpression>
32#include <QStandardPaths>
34#include "moc_qgstiledownloadmanager.cpp"
38QgsTileDownloadManagerWorker::QgsTileDownloadManagerWorker(
QgsTileDownloadManager *manager, QObject *parent )
43 connect( &mIdleTimer, &QTimer::timeout,
this, &QgsTileDownloadManagerWorker::idleTimerTimeout );
46void QgsTileDownloadManagerWorker::startIdleTimer()
48 if ( !mIdleTimer.isActive() )
50 mIdleTimer.start( mManager->mIdleThreadTimeoutMs );
54void QgsTileDownloadManagerWorker::queueUpdated()
56 const QMutexLocker locker( &mManager->mMutex );
58 if ( mManager->mShuttingDown )
66 std::vector< QNetworkReply * > replies;
67 replies.reserve( mManager->mQueue.size() );
68 for (
auto it = mManager->mQueue.begin(); it != mManager->mQueue.end(); ++it )
70 replies.emplace_back( it->networkReply );
73 for ( QNetworkReply *reply : replies )
82 if ( mIdleTimer.isActive() && !mManager->mQueue.empty() )
93 mManager->mStageQueueRemovals =
true;
94 for (
auto it = mManager->mQueue.begin(); it != mManager->mQueue.end(); ++it )
96 if ( !it->networkReply )
98 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: starting request: " ) + it->request.url().toString(), 2 );
101 QNetworkRequest request( it->request );
102 request.setAttribute( QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy );
104 connect( it->networkReply, &QNetworkReply::finished, it->objWorker, &QgsTileDownloadManagerReplyWorkerObject::replyFinished );
106 ++mManager->mStats.networkRequestsStarted;
109 mManager->mStageQueueRemovals =
false;
110 mManager->processStagedEntryRemovals();
113void QgsTileDownloadManagerWorker::quitThread()
115 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: stopping worker thread" ), 2 );
117 mManager->mWorker->deleteLater();
118 mManager->mWorker =
nullptr;
121 mManager->mWorkerThread->quit();
122 mManager->mWorkerThread =
nullptr;
123 mManager->mShuttingDown =
false;
126void QgsTileDownloadManagerWorker::idleTimerTimeout()
128 const QMutexLocker locker( &mManager->mMutex );
129 Q_ASSERT( mManager->mQueue.empty() );
137void QgsTileDownloadManagerReplyWorkerObject::replyFinished()
139 const QMutexLocker locker( &mManager->mMutex );
141 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: internal reply finished: " ) + mRequest.url().toString(), 2 );
143 QNetworkReply *reply = qobject_cast<QNetworkReply *>( sender() );
146 if ( reply->error() == QNetworkReply::NoError )
148 ++mManager->mStats.networkRequestsOk;
149 data = reply->readAll();
153 ++mManager->mStats.networkRequestsFailed;
154 const QString contentType = reply->header( QNetworkRequest::ContentTypeHeader ).toString();
155 if ( contentType.startsWith( QLatin1String(
"text/plain" ) ) )
156 data = reply->readAll();
159 QMap<QNetworkRequest::Attribute, QVariant> attributes;
160 attributes.insert( QNetworkRequest::SourceIsFromCacheAttribute, reply->attribute( QNetworkRequest::SourceIsFromCacheAttribute ) );
161 attributes.insert( QNetworkRequest::RedirectionTargetAttribute, reply->attribute( QNetworkRequest::RedirectionTargetAttribute ) );
162 attributes.insert( QNetworkRequest::HttpStatusCodeAttribute, reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ) );
163 attributes.insert( QNetworkRequest::HttpReasonPhraseAttribute, reply->attribute( QNetworkRequest::HttpReasonPhraseAttribute ) );
165 QMap<QNetworkRequest::KnownHeaders, QVariant> headers;
166 headers.insert( QNetworkRequest::ContentTypeHeader, reply->header( QNetworkRequest::ContentTypeHeader ) );
169 int httpStatusCode = reply->attribute( QNetworkRequest::Attribute::HttpStatusCodeAttribute ).toInt();
170 if ( httpStatusCode == 206 && mManager->isRangeRequest( mRequest ) )
172 mManager->mRangesCache->registerEntry( mRequest, data );
175 emit finished( data, reply->url(), attributes, headers, reply->rawHeaderPairs(), reply->error(), reply->errorString() );
177 reply->deleteLater();
182 mManager->removeEntry( mRequest );
184 if ( mManager->mQueue.empty() )
187 mManager->mWorker->startIdleTimer();
198 mRangesCache = std::make_unique<QgsRangeRequestCache>( );
202 if ( cacheDirectory.isEmpty() )
203 cacheDirectory = QStandardPaths::writableLocation( QStandardPaths::CacheLocation );
204 if ( !cacheDirectory.endsWith( QDir::separator() ) )
206 cacheDirectory.push_back( QDir::separator() );
208 cacheDirectory += QLatin1String(
"http-ranges" );
209 mRangesCache->setCacheDirectory( cacheDirectory );
211 mRangesCache->setCacheSize( cacheSize );
222 const QMutexLocker locker( &mMutex );
224 if ( isCachedRangeRequest( request ) )
227 QTimer::singleShot( 0, reply, &QgsTileDownloadManagerReply::cachedRangeRequestFinished );
233 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: starting worker thread" ), 2 );
234 mWorkerThread =
new QThread;
236 mWorker->moveToThread( mWorkerThread );
237 QObject::connect( mWorkerThread, &QThread::finished, mWorker, &QObject::deleteLater );
238 mWorkerThread->start();
243 ++mStats.requestsTotal;
245 QgsTileDownloadManager::QueueEntry entry = findEntryForRequest( request );
246 if ( !entry.isValid() )
248 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: get (new entry): " ) + request.url().toString(), 2 );
250 entry.request = request;
252 entry.objWorker->moveToThread( mWorkerThread );
254 QObject::connect( entry.objWorker, &QgsTileDownloadManagerReplyWorkerObject::finished, reply, &QgsTileDownloadManagerReply::requestFinished );
260 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: get (existing entry): " ) + request.url().toString(), 2 );
262 QObject::connect( entry.objWorker, &QgsTileDownloadManagerReplyWorkerObject::finished, reply, &QgsTileDownloadManagerReply::requestFinished );
264 ++mStats.requestsMerged;
267 signalQueueModified();
274 const QMutexLocker locker( &mMutex );
276 return !mQueue.empty();
284 while ( msec == -1 || t.elapsed() < msec )
287 const QMutexLocker locker( &mMutex );
288 if ( mQueue.empty() )
291 QThread::usleep( 1000 );
300 const QMutexLocker locker( &mMutex );
301 if ( !mWorkerThread )
305 mShuttingDown =
true;
306 signalQueueModified();
313 const QMutexLocker locker( &mMutex );
314 if ( !mWorkerThread )
318 QThread::usleep( 1000 );
324 return mWorkerThread && mWorkerThread->isRunning();
329 const QMutexLocker locker( &mMutex );
333QgsTileDownloadManager::QueueEntry QgsTileDownloadManager::findEntryForRequest(
const QNetworkRequest &request )
335 for (
auto it = mQueue.begin(); it != mQueue.end(); ++it )
337 if ( it->request.url() == request.url() && it->request.rawHeader(
"Range" ) == request.rawHeader(
"Range" ) )
340 return QgsTileDownloadManager::QueueEntry();
343void QgsTileDownloadManager::addEntry(
const QgsTileDownloadManager::QueueEntry &entry )
345 for (
auto it = mQueue.begin(); it != mQueue.end(); ++it )
347 Q_ASSERT( entry.request.url() != it->request.url() || entry.request.rawHeader(
"Range" ) != it->request.rawHeader(
"Range" ) );
350 mQueue.emplace_back( entry );
353void QgsTileDownloadManager::updateEntry(
const QgsTileDownloadManager::QueueEntry &entry )
355 for (
auto it = mQueue.begin(); it != mQueue.end(); ++it )
357 if ( entry.request.url() == it->request.url() && entry.request.rawHeader(
"Range" ) == it->request.rawHeader(
"Range" ) )
366void QgsTileDownloadManager::removeEntry(
const QNetworkRequest &request )
368 if ( mStageQueueRemovals )
370 mStagedQueueRemovals.emplace_back( request );
374 for (
auto it = mQueue.begin(); it != mQueue.end(); ++it )
376 if ( it->request.url() == request.url() && it->request.rawHeader(
"Range" ) == request.rawHeader(
"Range" ) )
386void QgsTileDownloadManager::processStagedEntryRemovals()
388 Q_ASSERT( !mStageQueueRemovals );
389 for (
const QNetworkRequest &request : mStagedQueueRemovals )
391 removeEntry( request );
393 mStagedQueueRemovals.clear();
396void QgsTileDownloadManager::signalQueueModified()
398 QMetaObject::invokeMethod( mWorker, &QgsTileDownloadManagerWorker::queueUpdated, Qt::QueuedConnection );
401bool QgsTileDownloadManager::isRangeRequest(
const QNetworkRequest &request )
403 if ( request.rawHeader(
"Range" ).isEmpty() )
405 const thread_local QRegularExpression regex(
"^bytes=\\d+-\\d+$" );
406 QRegularExpressionMatch match = regex.match( QString::fromUtf8( request.rawHeader(
"Range" ) ) );
407 return match.hasMatch();
410bool QgsTileDownloadManager::isCachedRangeRequest(
const QNetworkRequest &request )
412 QNetworkRequest::CacheLoadControl loadControl = ( QNetworkRequest::CacheLoadControl ) request.attribute( QNetworkRequest::CacheLoadControlAttribute ).toInt();
413 bool saveControl = request.attribute( QNetworkRequest::CacheSaveControlAttribute ).toBool();
414 return isRangeRequest( request ) && saveControl && loadControl != QNetworkRequest::AlwaysNetwork && mRangesCache->hasEntry( request );
420QgsTileDownloadManagerReply::QgsTileDownloadManagerReply(
QgsTileDownloadManager *manager,
const QNetworkRequest &request )
421 : mManager( manager )
422 , mRequest( request )
428 const QMutexLocker locker( &mManager->mMutex );
432 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: reply deleted before finished: " ) + mRequest.url().toString(), 2 );
434 ++mManager->mStats.requestsEarlyDeleted;
438void 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 )
440 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: reply finished: " ) + mRequest.url().toString(), 2 );
445 mAttributes = attributes;
453void QgsTileDownloadManagerReply::cachedRangeRequestFinished()
455 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: internal range request reply loaded from cache: " ) + mRequest.url().toString(), 2 );
457 mData = mManager->mRangesCache->entry( mRequest );
458 mUrl = mRequest.url();
464 return mAttributes.contains( code ) ? mAttributes.value( code ) : QVariant();
469 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)