19#include "moc_qgstiledownloadmanager.cpp"
28#include <QElapsedTimer>
29#include <QNetworkReply>
30#include <QStandardPaths>
31#include <QRegularExpression>
35QgsTileDownloadManagerWorker::QgsTileDownloadManagerWorker(
QgsTileDownloadManager *manager, QObject *parent )
40 connect( &mIdleTimer, &QTimer::timeout,
this, &QgsTileDownloadManagerWorker::idleTimerTimeout );
43void QgsTileDownloadManagerWorker::startIdleTimer()
45 if ( !mIdleTimer.isActive() )
47 mIdleTimer.start( mManager->mIdleThreadTimeoutMs );
51void QgsTileDownloadManagerWorker::queueUpdated()
53 const QMutexLocker locker( &mManager->mMutex );
55 if ( mManager->mShuttingDown )
63 std::vector< QNetworkReply * > replies;
64 replies.reserve( mManager->mQueue.size() );
65 for (
auto it = mManager->mQueue.begin(); it != mManager->mQueue.end(); ++it )
67 replies.emplace_back( it->networkReply );
70 for ( QNetworkReply *reply : replies )
79 if ( mIdleTimer.isActive() && !mManager->mQueue.empty() )
90 mManager->mStageQueueRemovals =
true;
91 for (
auto it = mManager->mQueue.begin(); it != mManager->mQueue.end(); ++it )
93 if ( !it->networkReply )
95 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: starting request: " ) + it->request.url().toString(), 2 );
98 QNetworkRequest request( it->request );
99 request.setAttribute( QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy );
101 connect( it->networkReply, &QNetworkReply::finished, it->objWorker, &QgsTileDownloadManagerReplyWorkerObject::replyFinished );
103 ++mManager->mStats.networkRequestsStarted;
106 mManager->mStageQueueRemovals =
false;
107 mManager->processStagedEntryRemovals();
110void QgsTileDownloadManagerWorker::quitThread()
112 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: stopping worker thread" ), 2 );
114 mManager->mWorker->deleteLater();
115 mManager->mWorker =
nullptr;
118 mManager->mWorkerThread->quit();
119 mManager->mWorkerThread =
nullptr;
120 mManager->mShuttingDown =
false;
123void QgsTileDownloadManagerWorker::idleTimerTimeout()
125 const QMutexLocker locker( &mManager->mMutex );
126 Q_ASSERT( mManager->mQueue.empty() );
134void QgsTileDownloadManagerReplyWorkerObject::replyFinished()
136 const QMutexLocker locker( &mManager->mMutex );
138 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: internal reply finished: " ) + mRequest.url().toString(), 2 );
140 QNetworkReply *reply = qobject_cast<QNetworkReply *>( sender() );
143 if ( reply->error() == QNetworkReply::NoError )
145 ++mManager->mStats.networkRequestsOk;
146 data = reply->readAll();
150 ++mManager->mStats.networkRequestsFailed;
151 const QString contentType = reply->header( QNetworkRequest::ContentTypeHeader ).toString();
152 if ( contentType.startsWith( QLatin1String(
"text/plain" ) ) )
153 data = reply->readAll();
156 QMap<QNetworkRequest::Attribute, QVariant> attributes;
157 attributes.insert( QNetworkRequest::SourceIsFromCacheAttribute, reply->attribute( QNetworkRequest::SourceIsFromCacheAttribute ) );
158 attributes.insert( QNetworkRequest::RedirectionTargetAttribute, reply->attribute( QNetworkRequest::RedirectionTargetAttribute ) );
159 attributes.insert( QNetworkRequest::HttpStatusCodeAttribute, reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ) );
160 attributes.insert( QNetworkRequest::HttpReasonPhraseAttribute, reply->attribute( QNetworkRequest::HttpReasonPhraseAttribute ) );
162 QMap<QNetworkRequest::KnownHeaders, QVariant> headers;
163 headers.insert( QNetworkRequest::ContentTypeHeader, reply->header( QNetworkRequest::ContentTypeHeader ) );
166 int httpStatusCode = reply->attribute( QNetworkRequest::Attribute::HttpStatusCodeAttribute ).toInt();
167 if ( httpStatusCode == 206 && mManager->isRangeRequest( mRequest ) )
169 mManager->mRangesCache->registerEntry( mRequest, data );
172 emit finished( data, reply->url(), attributes, headers, reply->rawHeaderPairs(), reply->error(), reply->errorString() );
174 reply->deleteLater();
179 mManager->removeEntry( mRequest );
181 if ( mManager->mQueue.empty() )
184 mManager->mWorker->startIdleTimer();
199 if ( cacheDirectory.isEmpty() )
200 cacheDirectory = QStandardPaths::writableLocation( QStandardPaths::CacheLocation );
201 if ( !cacheDirectory.endsWith( QDir::separator() ) )
203 cacheDirectory.push_back( QDir::separator() );
205 cacheDirectory += QLatin1String(
"http-ranges" );
206 mRangesCache->setCacheDirectory( cacheDirectory );
208 mRangesCache->setCacheSize( cacheSize );
219 const QMutexLocker locker( &mMutex );
221 if ( isCachedRangeRequest( request ) )
224 QTimer::singleShot( 0, reply, &QgsTileDownloadManagerReply::cachedRangeRequestFinished );
230 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: starting worker thread" ), 2 );
231 mWorkerThread =
new QThread;
233 mWorker->moveToThread( mWorkerThread );
234 QObject::connect( mWorkerThread, &QThread::finished, mWorker, &QObject::deleteLater );
235 mWorkerThread->start();
242 QgsTileDownloadManager::QueueEntry entry = findEntryForRequest( request );
243 if ( !entry.isValid() )
245 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: get (new entry): " ) + request.url().toString(), 2 );
247 entry.request = request;
249 entry.objWorker->moveToThread( mWorkerThread );
251 QObject::connect( entry.objWorker, &QgsTileDownloadManagerReplyWorkerObject::finished, reply, &QgsTileDownloadManagerReply::requestFinished );
257 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: get (existing entry): " ) + request.url().toString(), 2 );
259 QObject::connect( entry.objWorker, &QgsTileDownloadManagerReplyWorkerObject::finished, reply, &QgsTileDownloadManagerReply::requestFinished );
264 signalQueueModified();
271 const QMutexLocker locker( &mMutex );
273 return !mQueue.empty();
281 while ( msec == -1 || t.elapsed() < msec )
284 const QMutexLocker locker( &mMutex );
285 if ( mQueue.empty() )
288 QThread::usleep( 1000 );
297 const QMutexLocker locker( &mMutex );
298 if ( !mWorkerThread )
302 mShuttingDown =
true;
303 signalQueueModified();
310 const QMutexLocker locker( &mMutex );
311 if ( !mWorkerThread )
315 QThread::usleep( 1000 );
321 return mWorkerThread && mWorkerThread->isRunning();
326 const QMutexLocker locker( &mMutex );
330QgsTileDownloadManager::QueueEntry QgsTileDownloadManager::findEntryForRequest(
const QNetworkRequest &request )
332 for (
auto it = mQueue.begin(); it != mQueue.end(); ++it )
334 if ( it->request.url() == request.url() && it->request.rawHeader(
"Range" ) == request.rawHeader(
"Range" ) )
337 return QgsTileDownloadManager::QueueEntry();
340void QgsTileDownloadManager::addEntry(
const QgsTileDownloadManager::QueueEntry &entry )
342 for (
auto it = mQueue.begin(); it != mQueue.end(); ++it )
344 Q_ASSERT( entry.request.url() != it->request.url() || entry.request.rawHeader(
"Range" ) != it->request.rawHeader(
"Range" ) );
347 mQueue.emplace_back( entry );
350void QgsTileDownloadManager::updateEntry(
const QgsTileDownloadManager::QueueEntry &entry )
352 for (
auto it = mQueue.begin(); it != mQueue.end(); ++it )
354 if ( entry.request.url() == it->request.url() && entry.request.rawHeader(
"Range" ) == it->request.rawHeader(
"Range" ) )
363void QgsTileDownloadManager::removeEntry(
const QNetworkRequest &request )
365 if ( mStageQueueRemovals )
367 mStagedQueueRemovals.emplace_back( request );
371 for (
auto it = mQueue.begin(); it != mQueue.end(); ++it )
373 if ( it->request.url() == request.url() && it->request.rawHeader(
"Range" ) == request.rawHeader(
"Range" ) )
383void QgsTileDownloadManager::processStagedEntryRemovals()
385 Q_ASSERT( !mStageQueueRemovals );
386 for (
const QNetworkRequest &request : mStagedQueueRemovals )
388 removeEntry( request );
390 mStagedQueueRemovals.clear();
393void QgsTileDownloadManager::signalQueueModified()
395 QMetaObject::invokeMethod( mWorker, &QgsTileDownloadManagerWorker::queueUpdated, Qt::QueuedConnection );
398bool QgsTileDownloadManager::isRangeRequest(
const QNetworkRequest &request )
400 if ( request.rawHeader(
"Range" ).isEmpty() )
402 const thread_local QRegularExpression regex(
"^bytes=\\d+-\\d+$" );
403 QRegularExpressionMatch match = regex.match( QString::fromUtf8( request.rawHeader(
"Range" ) ) );
404 return match.hasMatch();
407bool QgsTileDownloadManager::isCachedRangeRequest(
const QNetworkRequest &request )
409 QNetworkRequest::CacheLoadControl loadControl = ( QNetworkRequest::CacheLoadControl ) request.attribute( QNetworkRequest::CacheLoadControlAttribute ).toInt();
410 bool saveControl = request.attribute( QNetworkRequest::CacheSaveControlAttribute ).toBool();
411 return isRangeRequest( request ) && saveControl && loadControl != QNetworkRequest::AlwaysNetwork && mRangesCache->hasEntry( request );
417QgsTileDownloadManagerReply::QgsTileDownloadManagerReply(
QgsTileDownloadManager *manager,
const QNetworkRequest &request )
418 : mManager( manager )
419 , mRequest( request )
425 const QMutexLocker locker( &mManager->mMutex );
429 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: reply deleted before finished: " ) + mRequest.url().toString(), 2 );
435void 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 )
437 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: reply finished: " ) + mRequest.url().toString(), 2 );
442 mAttributes = attributes;
450void QgsTileDownloadManagerReply::cachedRangeRequestFinished()
452 QgsDebugMsgLevel( QStringLiteral(
"Tile download manager: internal range request reply loaded from cache: " ) + mRequest.url().toString(), 2 );
454 mData = mManager->mRangesCache->entry( mRequest );
455 mUrl = mRequest.url();
461 return mAttributes.contains( code ) ? mAttributes.value( code ) : QVariant();
466 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.
T value(const QString &dynamicKeyPart=QString()) const
Returns settings value.
static const QgsSettingsEntryInteger64 * settingsNetworkCacheSize
Settings entry network cache directory.
static const QgsSettingsEntryString * settingsNetworkCacheDirectory
Settings entry network cache directory.
This class is a composition of two QSettings instances:
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)