Branch data Line data Source code
1 : : /*************************************************************************** 2 : : qgstiledownloadmanager.h 3 : : ------------------------ 4 : : begin : January 2021 5 : : copyright : (C) 2021 by Martin Dobias 6 : : email : wonder dot sk at gmail dot com 7 : : ***************************************************************************/ 8 : : 9 : : /*************************************************************************** 10 : : * * 11 : : * This program is free software; you can redistribute it and/or modify * 12 : : * it under the terms of the GNU General Public License as published by * 13 : : * the Free Software Foundation; either version 2 of the License, or * 14 : : * (at your option) any later version. * 15 : : * * 16 : : ***************************************************************************/ 17 : : 18 : : #ifndef QGSTILEDOWNLOADMANAGER_H 19 : : #define QGSTILEDOWNLOADMANAGER_H 20 : : 21 : : #define SIP_NO_FILE 22 : : 23 : : #include <QTimer> 24 : : #include <QThread> 25 : : #include <QMutex> 26 : : 27 : : #include <QNetworkAccessManager> 28 : : #include <QNetworkReply> 29 : : 30 : : #include "qgis_core.h" 31 : : 32 : : class QgsTileDownloadManager; 33 : : 34 : : /** 35 : : * \ingroup core 36 : : * \brief Reply object for tile download manager requests returned from calls to QgsTileDownloadManager::get(). 37 : : * 38 : : * When the underlying network request has finished (with success or failure), the finished() signal 39 : : * gets emitted. 40 : : * 41 : : * It is OK to delete this object before the request has finished - the request will not be aborted, 42 : : * the download manager will finish the download (as it may be needed soon afterwards). 43 : : * 44 : : * \since QGIS 3.18 45 : : */ 46 : : class CORE_EXPORT QgsTileDownloadManagerReply : public QObject 47 : : { 48 : : Q_OBJECT 49 : : public: 50 : : ~QgsTileDownloadManagerReply(); 51 : : 52 : : //! Returns whether the reply has already finished (with success/failure) 53 : : bool hasFinished() const { return mHasFinished; } 54 : : //! Returns binary data returned in the reply (only valid when already finished) 55 : 0 : QByteArray data() const { return mData; } 56 : : //! Returns error code (only valid when already finished) 57 : 0 : QNetworkReply::NetworkError error() const { return mError; } 58 : : //! Returns error string (only valid when already finished) 59 : 0 : QString errorString() const { return mErrorString; } 60 : : 61 : : //! Returns the original request for this reply object 62 : 0 : QNetworkRequest request() const { return mRequest; } 63 : : 64 : : signals: 65 : : //! Emitted when the reply has finished (either with a success or with a failure) 66 : : void finished(); 67 : : 68 : : private slots: 69 : : void requestFinished( QByteArray data, QNetworkReply::NetworkError error, const QString &errorString ); 70 : : 71 : : private: 72 : : QgsTileDownloadManagerReply( QgsTileDownloadManager *manager, const QNetworkRequest &request ); 73 : : 74 : : friend class QgsTileDownloadManager; // allows creation of new instances from the manager 75 : : 76 : : private: 77 : : //! "parent" download manager of this reply 78 : : QgsTileDownloadManager *mManager = nullptr; 79 : : QNetworkRequest mRequest; 80 : : bool mHasFinished = false; 81 : : QByteArray mData; 82 : : QNetworkReply::NetworkError mError = QNetworkReply::NoError; 83 : : QString mErrorString; 84 : : }; 85 : : 86 : : 87 : : /// @cond PRIVATE 88 : : 89 : : /** 90 : : * \ingroup core 91 : : * \brief Reply object that is used in the worker thread of the tile download manager. 92 : : * \note This class is not a part of public API 93 : : */ 94 : : class QgsTileDownloadManagerReplyWorkerObject : public QObject 95 : : { 96 : : Q_OBJECT 97 : : public: 98 : 0 : QgsTileDownloadManagerReplyWorkerObject( QgsTileDownloadManager *manager, const QNetworkRequest &request ) 99 : 0 : : mManager( manager ), mRequest( request ) {} 100 : : 101 : : public slots: 102 : : void replyFinished(); 103 : : 104 : : signals: 105 : : void finished( QByteArray data, QNetworkReply::NetworkError error, const QString &errorString ); 106 : : 107 : : private: 108 : : //! "parent" download manager of this worker object 109 : : QgsTileDownloadManager *mManager = nullptr; 110 : : QNetworkRequest mRequest; 111 : : }; 112 : : 113 : : 114 : : /** 115 : : * \ingroup core 116 : : * \brief Worker object that is used in the worker thread of the tile download manager. 117 : : * \note This class is not a part of public API 118 : : */ 119 : : class QgsTileDownloadManagerWorker : public QObject 120 : : { 121 : : Q_OBJECT 122 : : 123 : : public: 124 : : //! Creates the worker 125 : : QgsTileDownloadManagerWorker( QgsTileDownloadManager *manager, QObject *parent = nullptr ); 126 : : 127 : : void startIdleTimer(); 128 : : 129 : : public slots: 130 : : void queueUpdated(); 131 : : void idleTimerTimeout(); 132 : : 133 : : signals: 134 : : void requestFinished( QString url, QByteArray data ); 135 : : 136 : : private: 137 : : void quitThread(); 138 : : 139 : : private: 140 : : //! "parent" download manager of this worker 141 : : QgsTileDownloadManager *mManager = nullptr; 142 : : //! Timer used to delete the worker thread if thread is not doing anything for some time 143 : : QTimer mIdleTimer; 144 : : }; 145 : : 146 : : /// @endcond 147 : : 148 : : 149 : : 150 : : /** 151 : : * \ingroup core 152 : : * 153 : : * \brief Tile download manager handles downloads of map tiles for the purpose of map rendering. 154 : : * The purpose of this class is to handle a couple of situations that may happen: 155 : : * 156 : : * - a map rendering job starts which requests tiles from a remote server, then in a short 157 : : * while user zooms/pans map, which would normally mean that all pending requests get 158 : : * aborted and then restarted soon afterwards. The download manager allows the requests 159 : : * to finish to avoid excessive load on servers and needless aborts and repeated requests. 160 : : * - multiple map rendering jobs start at a similar time, requesting the same map tiles. 161 : : * Normally they could be requested multiple times from the server - the download manager 162 : : * groups these requests and only does a single request at a time. 163 : : * 164 : : * At this point, it is not recommended to use this class for other scenarios than map 165 : : * rendering: using it elsewhere could slow down map rendering or have some unexpected 166 : : * negative effects. 167 : : * 168 : : * How do things work: 169 : : * 170 : : * - Upon a request, a QgsTileDownloadManagerReply object (based on QObject) is returned, 171 : : * encapsulating the pending reply. Client can wait for its finished() signal - when 172 : : * it gets emitted, the request has finished with success or failure. Client can delete 173 : : * the reply object before the request is processed - download manager will finish its 174 : : * download (and it will get cached for a future use). 175 : : * - All requests are done by QgsNetworkAccessManager 176 : : * - A worker thread responsible for all network tile requests is started first time a tile 177 : : * is requested. Having a dedicated thread rather than reusing main thread makes things 178 : : * more predictable as we won't get stuck in case the main thread is doing some work or 179 : : * it is waiting for map rendering to finish. 180 : : * - There is a shared download queue (protected by a mutex) with a list of active requests 181 : : * and requests waiting to be processed. 182 : : * 183 : : * \since QGIS 3.18 184 : : */ 185 : : class CORE_EXPORT QgsTileDownloadManager 186 : : { 187 : : 188 : : //! An entry in the queue of requests to be handled by this class 189 : 0 : class QueueEntry 190 : : { 191 : : public: 192 : 0 : bool isValid() const { return !request.url().isEmpty(); } 193 : : 194 : : //! The actual original Qt network request 195 : : QNetworkRequest request; 196 : : //! Helper QObject that lives in worker thread that emits signals 197 : 0 : QgsTileDownloadManagerReplyWorkerObject *objWorker = nullptr; 198 : : //! Internal network reply - only to be touched by the worker thread 199 : 0 : QNetworkReply *networkReply = nullptr; 200 : : }; 201 : : 202 : : public: 203 : : 204 : : /** 205 : : * \ingroup core 206 : : * \brief Encapsulates any statistics we would like to keep about requests 207 : : * \since QGIS 3.18 208 : : */ 209 : 5 : class Stats 210 : : { 211 : : public: 212 : : //! How many requests were done through the download manager 213 : 5 : int requestsTotal = 0; 214 : : //! How many requests were same as some other pending request and got "merged" 215 : 5 : int requestsMerged = 0; 216 : : //! How many requests were deleted early by the client (i.e. lost interest) 217 : 5 : int requestsEarlyDeleted = 0; 218 : : 219 : : //! How many actual network requests were started 220 : 5 : int networkRequestsStarted = 0; 221 : : //! How many network requests have been successful 222 : 5 : int networkRequestsOk = 0; 223 : : //! How many network requests have failed 224 : 5 : int networkRequestsFailed = 0; 225 : : }; 226 : : 227 : : QgsTileDownloadManager(); 228 : : ~QgsTileDownloadManager(); 229 : : 230 : : /** 231 : : * Starts a request. Returns a new object that should be deleted by the caller 232 : : * when not needed anymore. 233 : : */ 234 : : QgsTileDownloadManagerReply *get( const QNetworkRequest &request ); 235 : : 236 : : //! Returns whether there are any pending requests in the queue 237 : : bool hasPendingRequests() const; 238 : : 239 : : /** 240 : : * Blocks the current thread until the queue is empty. This should not be used 241 : : * in production code, it is however useful for auto tests 242 : : */ 243 : : bool waitForPendingRequests( int msec = -1 ); 244 : : 245 : : //! Asks the worker thread to stop and blocks until it is not stopped. 246 : : void shutdown(); 247 : : 248 : : /** 249 : : * Returns whether the worker thread is running currently (it may be stopped 250 : : * if there were no requests recently 251 : : */ 252 : : bool hasWorkerThreadRunning() const; 253 : : 254 : : /** 255 : : * Sets after how many milliseconds the idle worker therad should terminate. 256 : : * This function is meant mainly for unit testing. 257 : : */ 258 : : void setIdleThreadTimeout( int timeoutMs ) { mIdleThreadTimeoutMs = timeoutMs; } 259 : : 260 : : //! Returns basic statistics of the queries handled by this class 261 : : Stats statistics() { return mStats; } 262 : : 263 : : //! Resets statistics of numbers of queries handled by this class 264 : : void resetStatistics(); 265 : : 266 : : friend class QgsTileDownloadManagerWorker; 267 : : friend class QgsTileDownloadManagerReply; 268 : : friend class QgsTileDownloadManagerReplyWorkerObject; 269 : : 270 : : private: 271 : : 272 : : // these can be only used with mutex locked! 273 : : QueueEntry findEntryForRequest( const QNetworkRequest &request ); 274 : : void addEntry( const QueueEntry &entry ); 275 : : void updateEntry( const QueueEntry &entry ); 276 : : void removeEntry( const QNetworkRequest &request ); 277 : : 278 : : void signalQueueModified(); 279 : : 280 : : private: 281 : : 282 : : QList<QueueEntry> mQueue; 283 : : bool mShuttingDown = false; 284 : : #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) 285 : : mutable QMutex mMutex; 286 : : #else 287 : : mutable QRecursiveMutex mMutex; 288 : : #endif 289 : : QThread *mWorkerThread = nullptr; 290 : : QgsTileDownloadManagerWorker *mWorker = nullptr; 291 : : Stats mStats; 292 : : 293 : : int mIdleThreadTimeoutMs = 10000; 294 : : }; 295 : : 296 : : #endif // QGSTILEDOWNLOADMANAGER_H