25#include <QElapsedTimer>
26#include <QNetworkReply>
27#include <QStandardPaths>
28#include <QRegularExpression>
32QgsTileDownloadManagerWorker::QgsTileDownloadManagerWorker(
QgsTileDownloadManager *manager, QObject *parent )
37 connect( &mIdleTimer, &QTimer::timeout,
this, &QgsTileDownloadManagerWorker::idleTimerTimeout );
40void QgsTileDownloadManagerWorker::startIdleTimer()
42 if ( !mIdleTimer.isActive() )
44 mIdleTimer.start( mManager->mIdleThreadTimeoutMs );
48void QgsTileDownloadManagerWorker::queueUpdated()
50 const QMutexLocker locker( &mManager->mMutex );
52 if ( mManager->mShuttingDown )
60 std::vector< QNetworkReply * > replies;
61 replies.reserve( mManager->mQueue.size() );
62 for (
auto it = mManager->mQueue.begin(); it != mManager->mQueue.end(); ++it )
64 replies.emplace_back( it->networkReply );
67 for ( QNetworkReply *reply : replies )
76 if ( mIdleTimer.isActive() && !mManager->mQueue.empty() )
87 mManager->mStageQueueRemovals =
true;
88 for (
auto it = mManager->mQueue.begin(); it != mManager->mQueue.end(); ++it )
90 if ( !it->networkReply )
92 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: starting request: " ) + it->request.url().toString(), 2 );
96 connect( it->networkReply, &QNetworkReply::finished, it->objWorker, &QgsTileDownloadManagerReplyWorkerObject::replyFinished );
98 ++mManager->mStats.networkRequestsStarted;
101 mManager->mStageQueueRemovals =
false;
102 mManager->processStagedEntryRemovals();
105void QgsTileDownloadManagerWorker::quitThread()
107 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: stopping worker thread" ), 2 );
109 mManager->mWorker->deleteLater();
110 mManager->mWorker =
nullptr;
113 mManager->mWorkerThread->quit();
114 mManager->mWorkerThread =
nullptr;
115 mManager->mShuttingDown =
false;
118void QgsTileDownloadManagerWorker::idleTimerTimeout()
120 const QMutexLocker locker( &mManager->mMutex );
121 Q_ASSERT( mManager->mQueue.empty() );
129void QgsTileDownloadManagerReplyWorkerObject::replyFinished()
131 const QMutexLocker locker( &mManager->mMutex );
133 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: internal reply finished: " ) + mRequest.url().toString(), 2 );
135 QNetworkReply *reply = qobject_cast<QNetworkReply *>( sender() );
138 if ( reply->error() == QNetworkReply::NoError )
140 ++mManager->mStats.networkRequestsOk;
141 data = reply->readAll();
145 ++mManager->mStats.networkRequestsFailed;
146 const QString contentType = reply->header( QNetworkRequest::ContentTypeHeader ).toString();
147 if ( contentType.startsWith( QLatin1String(
"text/plain" ) ) )
148 data = reply->readAll();
151 QMap<QNetworkRequest::Attribute, QVariant> attributes;
152 attributes.insert( QNetworkRequest::SourceIsFromCacheAttribute, reply->attribute( QNetworkRequest::SourceIsFromCacheAttribute ) );
153 attributes.insert( QNetworkRequest::RedirectionTargetAttribute, reply->attribute( QNetworkRequest::RedirectionTargetAttribute ) );
154 attributes.insert( QNetworkRequest::HttpStatusCodeAttribute, reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ) );
155 attributes.insert( QNetworkRequest::HttpReasonPhraseAttribute, reply->attribute( QNetworkRequest::HttpReasonPhraseAttribute ) );
157 QMap<QNetworkRequest::KnownHeaders, QVariant> headers;
158 headers.insert( QNetworkRequest::ContentTypeHeader, reply->header( QNetworkRequest::ContentTypeHeader ) );
161 int httpStatusCode = reply->attribute( QNetworkRequest::Attribute::HttpStatusCodeAttribute ).toInt();
162 if ( httpStatusCode == 206 && mManager->isRangeRequest( mRequest ) )
164 mManager->mRangesCache->registerEntry( mRequest, data );
167 emit finished( data, reply->url(), attributes, headers, reply->rawHeaderPairs(), reply->error(), reply->errorString() );
169 reply->deleteLater();
174 mManager->removeEntry( mRequest );
176 if ( mManager->mQueue.empty() )
179 mManager->mWorker->startIdleTimer();
193 QString cacheDirectory = settings.
value( QStringLiteral(
"cache/directory" ) ).toString();
194 if ( cacheDirectory.isEmpty() )
195 cacheDirectory = QStandardPaths::writableLocation( QStandardPaths::CacheLocation );
196 if ( !cacheDirectory.endsWith( QDir::separator() ) )
198 cacheDirectory.push_back( QDir::separator() );
200 cacheDirectory += QLatin1String(
"http-ranges" );
201 const qint64 cacheSize = settings.
value( QStringLiteral(
"cache/size" ), 256 * 1024 * 1024 ).toLongLong();
203 mRangesCache->setCacheDirectory( cacheDirectory );
204 mRangesCache->setCacheSize( cacheSize );
215 const QMutexLocker locker( &mMutex );
217 if ( isCachedRangeRequest( request ) )
220 QTimer::singleShot( 0, reply, &QgsTileDownloadManagerReply::cachedRangeRequestFinished );
226 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: starting worker thread" ), 2 );
227 mWorkerThread =
new QThread;
229 mWorker->moveToThread( mWorkerThread );
230 QObject::connect( mWorkerThread, &QThread::finished, mWorker, &QObject::deleteLater );
231 mWorkerThread->start();
238 QgsTileDownloadManager::QueueEntry entry = findEntryForRequest( request );
239 if ( !entry.isValid() )
241 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: get (new entry): " ) + request.url().toString(), 2 );
243 entry.request = request;
245 entry.objWorker->moveToThread( mWorkerThread );
247 QObject::connect( entry.objWorker, &QgsTileDownloadManagerReplyWorkerObject::finished, reply, &QgsTileDownloadManagerReply::requestFinished );
253 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: get (existing entry): " ) + request.url().toString(), 2 );
255 QObject::connect( entry.objWorker, &QgsTileDownloadManagerReplyWorkerObject::finished, reply, &QgsTileDownloadManagerReply::requestFinished );
260 signalQueueModified();
267 const QMutexLocker locker( &mMutex );
269 return !mQueue.empty();
277 while ( msec == -1 || t.elapsed() < msec )
280 const QMutexLocker locker( &mMutex );
281 if ( mQueue.empty() )
284 QThread::usleep( 1000 );
293 const QMutexLocker locker( &mMutex );
294 if ( !mWorkerThread )
298 mShuttingDown =
true;
299 signalQueueModified();
306 const QMutexLocker locker( &mMutex );
307 if ( !mWorkerThread )
311 QThread::usleep( 1000 );
317 return mWorkerThread && mWorkerThread->isRunning();
322 const QMutexLocker locker( &mMutex );
326QgsTileDownloadManager::QueueEntry QgsTileDownloadManager::findEntryForRequest(
const QNetworkRequest &request )
328 for (
auto it = mQueue.begin(); it != mQueue.end(); ++it )
330 if ( it->request.url() == request.url() && it->request.rawHeader(
"Range" ) == request.rawHeader(
"Range" ) )
333 return QgsTileDownloadManager::QueueEntry();
336void QgsTileDownloadManager::addEntry(
const QgsTileDownloadManager::QueueEntry &entry )
338 for (
auto it = mQueue.begin(); it != mQueue.end(); ++it )
340 Q_ASSERT( entry.request.url() != it->request.url() || entry.request.rawHeader(
"Range" ) != it->request.rawHeader(
"Range" ) );
343 mQueue.emplace_back( entry );
346void QgsTileDownloadManager::updateEntry(
const QgsTileDownloadManager::QueueEntry &entry )
348 for (
auto it = mQueue.begin(); it != mQueue.end(); ++it )
350 if ( entry.request.url() == it->request.url() && entry.request.rawHeader(
"Range" ) == it->request.rawHeader(
"Range" ) )
359void QgsTileDownloadManager::removeEntry(
const QNetworkRequest &request )
361 if ( mStageQueueRemovals )
363 mStagedQueueRemovals.emplace_back( request );
367 for (
auto it = mQueue.begin(); it != mQueue.end(); ++it )
369 if ( it->request.url() == request.url() && it->request.rawHeader(
"Range" ) == request.rawHeader(
"Range" ) )
379void QgsTileDownloadManager::processStagedEntryRemovals()
381 Q_ASSERT( !mStageQueueRemovals );
382 for (
const QNetworkRequest &request : mStagedQueueRemovals )
384 removeEntry( request );
386 mStagedQueueRemovals.clear();
389void QgsTileDownloadManager::signalQueueModified()
391 QMetaObject::invokeMethod( mWorker, &QgsTileDownloadManagerWorker::queueUpdated, Qt::QueuedConnection );
394bool QgsTileDownloadManager::isRangeRequest(
const QNetworkRequest &request )
396 if ( request.rawHeader(
"Range" ).isEmpty() )
398 const thread_local QRegularExpression regex(
"^bytes=\\d+-\\d+$" );
399 QRegularExpressionMatch match = regex.match( QString::fromUtf8( request.rawHeader(
"Range" ) ) );
400 return match.hasMatch();
403bool QgsTileDownloadManager::isCachedRangeRequest(
const QNetworkRequest &request )
405 QNetworkRequest::CacheLoadControl loadControl = ( QNetworkRequest::CacheLoadControl ) request.attribute( QNetworkRequest::CacheLoadControlAttribute ).toInt();
406 bool saveControl = request.attribute( QNetworkRequest::CacheSaveControlAttribute ).toBool();
407 return isRangeRequest( request ) && saveControl && loadControl != QNetworkRequest::AlwaysNetwork && mRangesCache->hasEntry( request );
413QgsTileDownloadManagerReply::QgsTileDownloadManagerReply(
QgsTileDownloadManager *manager,
const QNetworkRequest &request )
414 : mManager( manager )
415 , mRequest( request )
421 const QMutexLocker locker( &mManager->mMutex );
425 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: reply deleted before finished: " ) + mRequest.url().toString(), 2 );
431void 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 )
433 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: reply finished: " ) + mRequest.url().toString(), 2 );
438 mAttributes = attributes;
446void QgsTileDownloadManagerReply::cachedRangeRequestFinished()
448 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: internal range request reply loaded from cache: " ) + mRequest.url().toString(), 2 );
450 mData = mManager->mRangesCache->entry( mRequest );
451 mUrl = mRequest.url();
457 return mAttributes.contains( code ) ? mAttributes.value( code ) : QVariant();
462 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.
A custom cache for handling the storage and retrieval of HTTP range requests on disk.
This class is a composition of two QSettings instances:
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
Reply object for tile download manager requests returned from calls to QgsTileDownloadManager::get().
QString errorString() const
Returns error string (only valid when already finished)
~QgsTileDownloadManagerReply()
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.
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) 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)