24#include <QElapsedTimer>
25#include <QNetworkReply>
26#include <QStandardPaths>
27#include <QRegularExpression>
31QgsTileDownloadManagerWorker::QgsTileDownloadManagerWorker(
QgsTileDownloadManager *manager, QObject *parent )
36 connect( &mIdleTimer, &QTimer::timeout,
this, &QgsTileDownloadManagerWorker::idleTimerTimeout );
39void QgsTileDownloadManagerWorker::startIdleTimer()
41 if ( !mIdleTimer.isActive() )
43 mIdleTimer.start( mManager->mIdleThreadTimeoutMs );
47void QgsTileDownloadManagerWorker::queueUpdated()
49 const QMutexLocker locker( &mManager->mMutex );
51 if ( mManager->mShuttingDown )
59 std::vector< QNetworkReply * > replies;
60 replies.reserve( mManager->mQueue.size() );
61 for (
auto it = mManager->mQueue.begin(); it != mManager->mQueue.end(); ++it )
63 replies.emplace_back( it->networkReply );
66 for ( QNetworkReply *reply : replies )
75 if ( mIdleTimer.isActive() && !mManager->mQueue.empty() )
86 mManager->mStageQueueRemovals =
true;
87 for (
auto it = mManager->mQueue.begin(); it != mManager->mQueue.end(); ++it )
89 if ( !it->networkReply )
91 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: starting request: " ) + it->request.url().toString(), 2 );
95 connect( it->networkReply, &QNetworkReply::finished, it->objWorker, &QgsTileDownloadManagerReplyWorkerObject::replyFinished );
97 ++mManager->mStats.networkRequestsStarted;
100 mManager->mStageQueueRemovals =
false;
101 mManager->processStagedEntryRemovals();
104void QgsTileDownloadManagerWorker::quitThread()
106 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: stopping worker thread" ), 2 );
108 mManager->mWorker->deleteLater();
109 mManager->mWorker =
nullptr;
112 mManager->mWorkerThread->quit();
113 mManager->mWorkerThread =
nullptr;
114 mManager->mShuttingDown =
false;
117void QgsTileDownloadManagerWorker::idleTimerTimeout()
119 const QMutexLocker locker( &mManager->mMutex );
120 Q_ASSERT( mManager->mQueue.empty() );
128void QgsTileDownloadManagerReplyWorkerObject::replyFinished()
130 const QMutexLocker locker( &mManager->mMutex );
132 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: internal reply finished: " ) + mRequest.url().toString(), 2 );
134 QNetworkReply *reply = qobject_cast<QNetworkReply *>( sender() );
137 if ( reply->error() == QNetworkReply::NoError )
139 ++mManager->mStats.networkRequestsOk;
140 data = reply->readAll();
144 ++mManager->mStats.networkRequestsFailed;
145 const QString contentType = reply->header( QNetworkRequest::ContentTypeHeader ).toString();
146 if ( contentType.startsWith( QLatin1String(
"text/plain" ) ) )
147 data = reply->readAll();
150 QMap<QNetworkRequest::Attribute, QVariant> attributes;
151 attributes.insert( QNetworkRequest::SourceIsFromCacheAttribute, reply->attribute( QNetworkRequest::SourceIsFromCacheAttribute ) );
152 attributes.insert( QNetworkRequest::RedirectionTargetAttribute, reply->attribute( QNetworkRequest::RedirectionTargetAttribute ) );
153 attributes.insert( QNetworkRequest::HttpStatusCodeAttribute, reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ) );
154 attributes.insert( QNetworkRequest::HttpReasonPhraseAttribute, reply->attribute( QNetworkRequest::HttpReasonPhraseAttribute ) );
156 QMap<QNetworkRequest::KnownHeaders, QVariant> headers;
157 headers.insert( QNetworkRequest::ContentTypeHeader, reply->header( QNetworkRequest::ContentTypeHeader ) );
160 int httpStatusCode = reply->attribute( QNetworkRequest::Attribute::HttpStatusCodeAttribute ).toInt();
161 if ( httpStatusCode == 206 && mManager->isRangeRequest( mRequest ) )
163 mManager->mRangesCache->registerEntry( mRequest, data );
166 emit finished( data, reply->url(), attributes, headers, reply->rawHeaderPairs(), reply->error(), reply->errorString() );
168 reply->deleteLater();
173 mManager->removeEntry( mRequest );
175 if ( mManager->mQueue.empty() )
178 mManager->mWorker->startIdleTimer();
192 QString cacheDirectory = settings.
value( QStringLiteral(
"cache/directory" ) ).toString();
193 if ( cacheDirectory.isEmpty() )
194 cacheDirectory = QStandardPaths::writableLocation( QStandardPaths::CacheLocation );
195 if ( !cacheDirectory.endsWith( QDir::separator() ) )
197 cacheDirectory.push_back( QDir::separator() );
199 cacheDirectory += QLatin1String(
"http-ranges" );
200 const qint64 cacheSize = settings.
value( QStringLiteral(
"cache/size" ), 256 * 1024 * 1024 ).toLongLong();
202 mRangesCache->setCacheDirectory( cacheDirectory );
203 mRangesCache->setCacheSize( cacheSize );
214 const QMutexLocker locker( &mMutex );
216 if ( isCachedRangeRequest( request ) )
219 QTimer::singleShot( 0, reply, &QgsTileDownloadManagerReply::cachedRangeRequestFinished );
225 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: starting worker thread" ), 2 );
226 mWorkerThread =
new QThread;
228 mWorker->moveToThread( mWorkerThread );
229 QObject::connect( mWorkerThread, &QThread::finished, mWorker, &QObject::deleteLater );
230 mWorkerThread->start();
237 QgsTileDownloadManager::QueueEntry entry = findEntryForRequest( request );
238 if ( !entry.isValid() )
240 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: get (new entry): " ) + request.url().toString(), 2 );
242 entry.request = request;
244 entry.objWorker->moveToThread( mWorkerThread );
246 QObject::connect( entry.objWorker, &QgsTileDownloadManagerReplyWorkerObject::finished, reply, &QgsTileDownloadManagerReply::requestFinished );
252 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: get (existing entry): " ) + request.url().toString(), 2 );
254 QObject::connect( entry.objWorker, &QgsTileDownloadManagerReplyWorkerObject::finished, reply, &QgsTileDownloadManagerReply::requestFinished );
259 signalQueueModified();
266 const QMutexLocker locker( &mMutex );
268 return !mQueue.empty();
276 while ( msec == -1 || t.elapsed() < msec )
279 const QMutexLocker locker( &mMutex );
280 if ( mQueue.empty() )
283 QThread::usleep( 1000 );
292 const QMutexLocker locker( &mMutex );
293 if ( !mWorkerThread )
297 mShuttingDown =
true;
298 signalQueueModified();
305 const QMutexLocker locker( &mMutex );
306 if ( !mWorkerThread )
310 QThread::usleep( 1000 );
316 return mWorkerThread && mWorkerThread->isRunning();
321 const QMutexLocker locker( &mMutex );
325QgsTileDownloadManager::QueueEntry QgsTileDownloadManager::findEntryForRequest(
const QNetworkRequest &request )
327 for (
auto it = mQueue.begin(); it != mQueue.end(); ++it )
329 if ( it->request.url() == request.url() && it->request.rawHeader(
"Range" ) == request.rawHeader(
"Range" ) )
332 return QgsTileDownloadManager::QueueEntry();
335void QgsTileDownloadManager::addEntry(
const QgsTileDownloadManager::QueueEntry &entry )
337 for (
auto it = mQueue.begin(); it != mQueue.end(); ++it )
339 Q_ASSERT( entry.request.url() != it->request.url() || entry.request.rawHeader(
"Range" ) != it->request.rawHeader(
"Range" ) );
342 mQueue.emplace_back( entry );
345void QgsTileDownloadManager::updateEntry(
const QgsTileDownloadManager::QueueEntry &entry )
347 for (
auto it = mQueue.begin(); it != mQueue.end(); ++it )
349 if ( entry.request.url() == it->request.url() && entry.request.rawHeader(
"Range" ) == it->request.rawHeader(
"Range" ) )
358void QgsTileDownloadManager::removeEntry(
const QNetworkRequest &request )
360 if ( mStageQueueRemovals )
362 mStagedQueueRemovals.emplace_back( request );
366 for (
auto it = mQueue.begin(); it != mQueue.end(); ++it )
368 if ( it->request.url() == request.url() && it->request.rawHeader(
"Range" ) == request.rawHeader(
"Range" ) )
378void QgsTileDownloadManager::processStagedEntryRemovals()
380 Q_ASSERT( !mStageQueueRemovals );
381 for (
const QNetworkRequest &request : mStagedQueueRemovals )
383 removeEntry( request );
385 mStagedQueueRemovals.clear();
388void QgsTileDownloadManager::signalQueueModified()
390 QMetaObject::invokeMethod( mWorker, &QgsTileDownloadManagerWorker::queueUpdated, Qt::QueuedConnection );
393bool QgsTileDownloadManager::isRangeRequest(
const QNetworkRequest &request )
395 if ( request.rawHeader(
"Range" ).isEmpty() )
397 QRegularExpression regex(
"^bytes=\\d+-\\d+$" );
398 QRegularExpressionMatch match = regex.match( QString::fromUtf8( request.rawHeader(
"Range" ) ) );
399 return match.hasMatch();
402bool QgsTileDownloadManager::isCachedRangeRequest(
const QNetworkRequest &request )
404 QNetworkRequest::CacheLoadControl loadControl = ( QNetworkRequest::CacheLoadControl ) request.attribute( QNetworkRequest::CacheLoadControlAttribute ).toInt();
405 bool saveControl = request.attribute( QNetworkRequest::CacheSaveControlAttribute ).toBool();
406 return isRangeRequest( request ) && saveControl && loadControl != QNetworkRequest::AlwaysNetwork && mRangesCache->hasEntry( request );
412QgsTileDownloadManagerReply::QgsTileDownloadManagerReply(
QgsTileDownloadManager *manager,
const QNetworkRequest &request )
413 : mManager( manager )
414 , mRequest( request )
420 const QMutexLocker locker( &mManager->mMutex );
424 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: reply deleted before finished: " ) + mRequest.url().toString(), 2 );
430void 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 )
432 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: reply finished: " ) + mRequest.url().toString(), 2 );
437 mAttributes = attributes;
445void QgsTileDownloadManagerReply::cachedRangeRequestFinished()
447 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: internal range request reply loaded from cache: " ) + mRequest.url().toString(), 2 );
449 mData = mManager->mRangesCache->entry( mRequest );
450 mUrl = mRequest.url();
456 return mAttributes.contains( code ) ? mAttributes.value( code ) : QVariant();
461 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)