28#include <QElapsedTimer>
29#include <QNetworkReply>
30#include <QRegularExpression>
31#include <QStandardPaths>
34#include "moc_qgstiledownloadmanager.cpp"
36using namespace Qt::StringLiterals;
40QgsTileDownloadManagerWorker::QgsTileDownloadManagerWorker(
QgsTileDownloadManager *manager, QObject *parent )
45 connect( &mIdleTimer, &QTimer::timeout,
this, &QgsTileDownloadManagerWorker::idleTimerTimeout );
48void QgsTileDownloadManagerWorker::startIdleTimer()
50 if ( !mIdleTimer.isActive() )
52 mIdleTimer.start( mManager->mIdleThreadTimeoutMs );
56void QgsTileDownloadManagerWorker::queueUpdated()
58 const QMutexLocker locker( &mManager->mMutex );
60 if ( mManager->mShuttingDown )
68 std::vector< QNetworkReply * > replies;
69 replies.reserve( mManager->mQueue.size() );
70 for (
auto it = mManager->mQueue.begin(); it != mManager->mQueue.end(); ++it )
72 replies.emplace_back( it->networkReply );
75 for ( QNetworkReply *reply : replies )
84 if ( mIdleTimer.isActive() && !mManager->mQueue.empty() )
95 mManager->mStageQueueRemovals =
true;
96 for (
auto it = mManager->mQueue.begin(); it != mManager->mQueue.end(); ++it )
98 if ( !it->networkReply )
100 QgsDebugMsgLevel( u
"Tile download manager: starting request: "_s + it->request.url().toString(), 2 );
103 QNetworkRequest request( it->request );
104 request.setAttribute( QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy );
106 connect( it->networkReply, &QNetworkReply::finished, it->objWorker, &QgsTileDownloadManagerReplyWorkerObject::replyFinished );
108 ++mManager->mStats.networkRequestsStarted;
111 mManager->mStageQueueRemovals =
false;
112 mManager->processStagedEntryRemovals();
115void QgsTileDownloadManagerWorker::quitThread()
119 mManager->mWorker->deleteLater();
120 mManager->mWorker =
nullptr;
123 mManager->mWorkerThread->quit();
124 mManager->mWorkerThread =
nullptr;
125 mManager->mShuttingDown =
false;
128void QgsTileDownloadManagerWorker::idleTimerTimeout()
130 const QMutexLocker locker( &mManager->mMutex );
131 Q_ASSERT( mManager->mQueue.empty() );
139void QgsTileDownloadManagerReplyWorkerObject::replyFinished()
141 const QMutexLocker locker( &mManager->mMutex );
143 QgsDebugMsgLevel( u
"Tile download manager: internal reply finished: "_s + mRequest.url().toString(), 2 );
145 QNetworkReply *reply = qobject_cast<QNetworkReply *>( sender() );
148 if ( reply->error() == QNetworkReply::NoError )
150 ++mManager->mStats.networkRequestsOk;
151 data = reply->readAll();
155 ++mManager->mStats.networkRequestsFailed;
156 const QString contentType = reply->header( QNetworkRequest::ContentTypeHeader ).toString();
157 if ( contentType.startsWith(
"text/plain"_L1 ) )
158 data = reply->readAll();
161 QMap<QNetworkRequest::Attribute, QVariant> attributes;
162 attributes.insert( QNetworkRequest::SourceIsFromCacheAttribute, reply->attribute( QNetworkRequest::SourceIsFromCacheAttribute ) );
163 attributes.insert( QNetworkRequest::RedirectionTargetAttribute, reply->attribute( QNetworkRequest::RedirectionTargetAttribute ) );
164 attributes.insert( QNetworkRequest::HttpStatusCodeAttribute, reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ) );
165 attributes.insert( QNetworkRequest::HttpReasonPhraseAttribute, reply->attribute( QNetworkRequest::HttpReasonPhraseAttribute ) );
167 QMap<QNetworkRequest::KnownHeaders, QVariant> headers;
168 headers.insert( QNetworkRequest::ContentTypeHeader, reply->header( QNetworkRequest::ContentTypeHeader ) );
171 int httpStatusCode = reply->attribute( QNetworkRequest::Attribute::HttpStatusCodeAttribute ).toInt();
172 if ( httpStatusCode == 206 && mManager->isRangeRequest( mRequest ) )
174 mManager->mRangesCache->registerEntry( mRequest, data );
177 emit finished( data, reply->url(), attributes, headers, reply->rawHeaderPairs(), reply->error(), reply->errorString() );
179 reply->deleteLater();
184 mManager->removeEntry( mRequest );
186 if ( mManager->mQueue.empty() )
189 mManager->mWorker->startIdleTimer();
200 mRangesCache = std::make_unique<QgsRangeRequestCache>();
203 if ( cacheDirectory.isEmpty() )
204 cacheDirectory = QStandardPaths::writableLocation( QStandardPaths::CacheLocation );
205 if ( !cacheDirectory.endsWith( QDir::separator() ) )
207 cacheDirectory.push_back( QDir::separator() );
209 cacheDirectory +=
"http-ranges"_L1;
210 mRangesCache->setCacheDirectory( cacheDirectory );
212 mRangesCache->setCacheSize( cacheSize );
223 const QMutexLocker locker( &mMutex );
225 if ( isCachedRangeRequest( request ) )
228 QTimer::singleShot( 0, reply, &QgsTileDownloadManagerReply::cachedRangeRequestFinished );
235 mWorkerThread =
new QThread;
237 mWorker->moveToThread( mWorkerThread );
238 QObject::connect( mWorkerThread, &QThread::finished, mWorker, &QObject::deleteLater );
239 mWorkerThread->start();
244 ++mStats.requestsTotal;
246 QgsTileDownloadManager::QueueEntry entry = findEntryForRequest( request );
247 if ( !entry.isValid() )
249 QgsDebugMsgLevel( u
"Tile download manager: get (new entry): "_s + request.url().toString(), 2 );
251 entry.request = request;
253 entry.objWorker->moveToThread( mWorkerThread );
255 QObject::connect( entry.objWorker, &QgsTileDownloadManagerReplyWorkerObject::finished, reply, &QgsTileDownloadManagerReply::requestFinished );
261 QgsDebugMsgLevel( u
"Tile download manager: get (existing entry): "_s + request.url().toString(), 2 );
263 QObject::connect( entry.objWorker, &QgsTileDownloadManagerReplyWorkerObject::finished, reply, &QgsTileDownloadManagerReply::requestFinished );
265 ++mStats.requestsMerged;
268 signalQueueModified();
275 const QMutexLocker locker( &mMutex );
277 return !mQueue.empty();
285 while ( msec == -1 || t.elapsed() < msec )
288 const QMutexLocker locker( &mMutex );
289 if ( mQueue.empty() )
292 QThread::usleep( 1000 );
301 const QMutexLocker locker( &mMutex );
302 if ( !mWorkerThread )
306 mShuttingDown =
true;
307 signalQueueModified();
314 const QMutexLocker locker( &mMutex );
315 if ( !mWorkerThread )
319 QThread::usleep( 1000 );
325 return mWorkerThread && mWorkerThread->isRunning();
330 const QMutexLocker locker( &mMutex );
334QgsTileDownloadManager::QueueEntry QgsTileDownloadManager::findEntryForRequest(
const QNetworkRequest &request )
336 for (
auto it = mQueue.begin(); it != mQueue.end(); ++it )
338 if ( it->request.url() == request.url() && it->request.rawHeader(
"Range" ) == request.rawHeader(
"Range" ) )
341 return QgsTileDownloadManager::QueueEntry();
344void QgsTileDownloadManager::addEntry(
const QgsTileDownloadManager::QueueEntry &entry )
346 for (
auto it = mQueue.begin(); it != mQueue.end(); ++it )
348 Q_ASSERT( entry.request.url() != it->request.url() || entry.request.rawHeader(
"Range" ) != it->request.rawHeader(
"Range" ) );
351 mQueue.emplace_back( entry );
354void QgsTileDownloadManager::updateEntry(
const QgsTileDownloadManager::QueueEntry &entry )
356 for (
auto it = mQueue.begin(); it != mQueue.end(); ++it )
358 if ( entry.request.url() == it->request.url() && entry.request.rawHeader(
"Range" ) == it->request.rawHeader(
"Range" ) )
367void QgsTileDownloadManager::removeEntry(
const QNetworkRequest &request )
369 if ( mStageQueueRemovals )
371 mStagedQueueRemovals.emplace_back( request );
375 for (
auto it = mQueue.begin(); it != mQueue.end(); ++it )
377 if ( it->request.url() == request.url() && it->request.rawHeader(
"Range" ) == request.rawHeader(
"Range" ) )
387void QgsTileDownloadManager::processStagedEntryRemovals()
389 Q_ASSERT( !mStageQueueRemovals );
390 for (
const QNetworkRequest &request : mStagedQueueRemovals )
392 removeEntry( request );
394 mStagedQueueRemovals.clear();
397void QgsTileDownloadManager::signalQueueModified()
399 QMetaObject::invokeMethod( mWorker, &QgsTileDownloadManagerWorker::queueUpdated, Qt::QueuedConnection );
402bool QgsTileDownloadManager::isRangeRequest(
const QNetworkRequest &request )
404 if ( request.rawHeader(
"Range" ).isEmpty() )
406 const thread_local QRegularExpression regex(
"^bytes=\\d+-\\d+$" );
407 QRegularExpressionMatch match = regex.match( QString::fromUtf8( request.rawHeader(
"Range" ) ) );
408 return match.hasMatch();
411bool QgsTileDownloadManager::isCachedRangeRequest(
const QNetworkRequest &request )
413 QNetworkRequest::CacheLoadControl loadControl = ( QNetworkRequest::CacheLoadControl ) request.attribute( QNetworkRequest::CacheLoadControlAttribute ).toInt();
414 bool saveControl = request.attribute( QNetworkRequest::CacheSaveControlAttribute ).toBool();
415 return isRangeRequest( request ) && saveControl && loadControl != QNetworkRequest::AlwaysNetwork && mRangesCache->hasEntry( request );
421QgsTileDownloadManagerReply::QgsTileDownloadManagerReply(
QgsTileDownloadManager *manager,
const QNetworkRequest &request )
422 : mManager( manager )
423 , mRequest( request )
428 const QMutexLocker locker( &mManager->mMutex );
432 QgsDebugMsgLevel( u
"Tile download manager: reply deleted before finished: "_s + mRequest.url().toString(), 2 );
434 ++mManager->mStats.requestsEarlyDeleted;
438void QgsTileDownloadManagerReply::requestFinished(
441 const QMap<QNetworkRequest::Attribute, QVariant> &attributes,
442 const QMap<QNetworkRequest::KnownHeaders, QVariant> &headers,
443 const QList<QNetworkReply::RawHeaderPair> rawHeaderPairs,
444 QNetworkReply::NetworkError error,
445 const QString &errorString
448 QgsDebugMsgLevel( u
"Tile download manager: reply finished: "_s + mRequest.url().toString(), 2 );
453 mAttributes = attributes;
461void QgsTileDownloadManagerReply::cachedRangeRequestFinished()
463 QgsDebugMsgLevel( u
"Tile download manager: internal range request reply loaded from cache: "_s + mRequest.url().toString(), 2 );
465 mData = mManager->mRangesCache->entry( mRequest );
466 mUrl = mRequest.url();
472 return mAttributes.contains( code ) ? mAttributes.value( code ) : QVariant();
477 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.
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)