QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
QGCTileCacheWorker.cpp
Go to the documentation of this file.
3
4#include <QtCore/QCoreApplication>
5#include <QtCore/QSettings>
6
7#include "QGCCachedTileSet.h"
9#include "QGCMapTasks.h"
10#include "QGCMapUrlEngine.h"
11
12QGC_LOGGING_CATEGORY(QGCTileCacheWorkerLog, "QtLocationPlugin.QGCTileCacheWorker")
13
15 : QThread(parent)
16{
17 qCDebug(QGCTileCacheWorkerLog) << this;
18}
19
20// L3: Defensive destructor ensures thread is stopped even if caller forgets
22{
23 stop();
24 wait();
25 qCDebug(QGCTileCacheWorkerLog) << this;
26}
27
28// C1: Added _taskQueue.clear() — qDeleteAll does not clear the container,
29// leaving dangling pointers that the worker thread would dequeue.
31{
32 _stopRequested = true;
33 QMutexLocker lock(&_taskQueueMutex);
34 qDeleteAll(_taskQueue);
35 _taskQueue.clear();
36 lock.unlock();
37
38 if (isRunning()) {
39 _waitc.wakeAll();
40 }
41}
42
44{
45 if (!_dbValid && !isRunning() && (task->type() != QGCMapTask::TaskType::taskInit)) {
46 task->setError(tr("Database Not Initialized"));
47 task->deleteLater();
48 return false;
49 }
50
51 QMutexLocker lock(&_taskQueueMutex);
52 _taskQueue.enqueue(task);
53 lock.unlock();
54
55 if (isRunning()) {
56 _waitc.wakeAll();
57 } else {
58 start(QThread::NormalPriority);
59 }
60
61 return true;
62}
63
65{
66 _stopRequested = false;
67 _database = std::make_unique<QGCTileCacheDatabase>(_databasePath);
68
69 if (!_database->init()) {
70 qCWarning(QGCTileCacheWorkerLog) << "Failed To Init Database";
71 _database.reset();
72
73 QMutexLocker lock(&_taskQueueMutex);
74 for (QGCMapTask *orphan : _taskQueue) {
75 orphan->setError(tr("Database Init Failed"));
76 orphan->deleteLater();
77 }
78 _taskQueue.clear();
79 return;
80 }
81
82 if (_database->isValid()) {
83 if (_database->connectDB()) {
84 _database->deleteBingNoTileTiles();
85 }
86 }
87
88 _dbValid = _database->isValid();
89
90 // M1: Start timer before the loop — hasExpired() on an unstarted timer is UB
91 _updateTimer.start();
92
93 QMutexLocker lock(&_taskQueueMutex);
94 while (!_stopRequested) {
95 if (!_taskQueue.isEmpty()) {
96 QGCMapTask* const task = _taskQueue.dequeue();
97 lock.unlock();
98 _runTask(task);
99 lock.relock();
100 task->deleteLater();
101
102 const qsizetype count = _taskQueue.count();
103 if (count > 100) {
104 _updateTimeout = kLongTimeoutMs;
105 } else if (count < 25) {
106 _updateTimeout = kShortTimeoutMs;
107 }
108
109 if ((count == 0) || _updateTimer.hasExpired(_updateTimeout)) {
110 if (_database && _database->isValid()) {
111 lock.unlock();
112 _emitTotals();
113 lock.relock();
114 }
115 }
116 } else {
117 (void) _waitc.wait(lock.mutex(), 5000);
118 }
119 }
120
121 // H1: Drain any tasks enqueued between the break decision and shutdown.
122 // Tasks are main-thread QObjects so deleteLater() posts to the main event loop.
123 for (QGCMapTask *orphan : _taskQueue) {
124 orphan->setError(tr("Worker shutting down"));
125 orphan->deleteLater();
126 }
127 _taskQueue.clear();
128 lock.unlock();
129
130 _dbValid = false;
131 if (_database) {
132 _database->disconnectDB();
133 _database.reset();
134 }
135}
136
137void QGCCacheWorker::_runTask(QGCMapTask *task)
138{
139 switch (task->type()) {
141 // L2: No-op — used only to bootstrap the worker thread
142 break;
144 _saveTile(task);
145 break;
147 _getTile(task);
148 break;
150 _getTileSets(task);
151 break;
153 _createTileSet(task);
154 break;
156 _getTileDownloadList(task);
157 break;
159 _updateTileDownloadState(task);
160 break;
162 _deleteTileSet(task);
163 break;
165 _renameTileSet(task);
166 break;
168 _pruneCache(task);
169 break;
171 _resetCacheDatabase(task);
172 break;
174 _exportSets(task);
175 break;
177 _importSets(task);
178 break;
179 default:
180 qCWarning(QGCTileCacheWorkerLog) << "given unhandled task type" << task->type();
181 break;
182 }
183}
184
185bool QGCCacheWorker::_testTask(QGCMapTask *mtask)
186{
187 if (!_database || !_database->isValid()) {
188 mtask->setError("No Cache Database");
189 return false;
190 }
191
192 return true;
193}
194
195void QGCCacheWorker::_emitTotals()
196{
197 TotalsResult t = _database->computeTotals();
199 _updateTimer.restart();
200}
201
202// M2: Check saveTile return value
203void QGCCacheWorker::_saveTile(QGCMapTask *mtask)
204{
205 if (!_testTask(mtask)) {
206 return;
207 }
208
209 QGCSaveTileTask *task = static_cast<QGCSaveTileTask*>(mtask);
210 if (!_database->saveTile(task->tile()->hash, task->tile()->format,
211 task->tile()->img, task->tile()->type, task->tile()->tileSet)) {
212 mtask->setError("Error saving tile to cache");
213 }
214}
215
216void QGCCacheWorker::_getTile(QGCMapTask *mtask)
217{
218 if (!_testTask(mtask)) {
219 return;
220 }
221
222 QGCFetchTileTask *task = static_cast<QGCFetchTileTask*>(mtask);
223 auto tile = _database->getTile(task->hash());
224 if (tile) {
225 task->setTileFetched(tile.release());
226 } else {
227 task->setError("Tile not in cache database");
228 }
229}
230
231// M4: Empty result is not an error (fresh DB or all sets deleted)
232// M5: Block signals on worker-thread-created QObjects until moveToThread
233void QGCCacheWorker::_getTileSets(QGCMapTask *mtask)
234{
235 if (!_testTask(mtask)) {
236 return;
237 }
238
239 QGCFetchTileSetTask *task = static_cast<QGCFetchTileSetTask*>(mtask);
240 const QList<TileSetRecord> records = _database->getTileSets();
241
242 for (const auto &rec : records) {
243 QGCCachedTileSet *set = new QGCCachedTileSet(rec.name);
244 set->blockSignals(true);
245
246 set->setId(rec.setID);
247 set->setMapTypeStr(rec.mapTypeStr);
248 set->setTopleftLat(rec.topleftLat);
249 set->setTopleftLon(rec.topleftLon);
250 set->setBottomRightLat(rec.bottomRightLat);
251 set->setBottomRightLon(rec.bottomRightLon);
252 set->setMinZoom(rec.minZoom);
253 set->setMaxZoom(rec.maxZoom);
254 set->setType(UrlFactory::getProviderTypeFromQtMapId(rec.type));
255 set->setTotalTileCount(rec.numTiles);
256 set->setDefaultSet(rec.defaultSet);
257 set->setCreationDate(QDateTime::fromSecsSinceEpoch(rec.date));
258
259 const SetTotalsResult totals = _database->computeSetTotals(rec.setID, rec.defaultSet, rec.numTiles, set->type());
260 set->setSavedTileCount(totals.savedTileCount);
261 set->setSavedTileSize(totals.savedTileSize);
262 set->setTotalTileSize(totals.totalTileSize);
263 set->setUniqueTileCount(totals.uniqueTileCount);
264 set->setUniqueTileSize(totals.uniqueTileSize);
265
266 set->blockSignals(false);
267 (void) set->moveToThread(QCoreApplication::instance()->thread());
268 task->setTileSetFetched(set);
269 }
270}
271
272// H2: Block signals while modifying the tile set from the worker thread.
273// The object is exclusively owned by this task (no concurrent access), but
274// emitting signals from the wrong thread is technically incorrect.
275void QGCCacheWorker::_createTileSet(QGCMapTask *mtask)
276{
277 if (!_testTask(mtask)) {
278 return;
279 }
280
281 QGCCreateTileSetTask *task = static_cast<QGCCreateTileSetTask*>(mtask);
282 const auto setID = _database->createTileSet(
283 task->tileSet()->name(), task->tileSet()->mapTypeStr(),
284 task->tileSet()->topleftLat(), task->tileSet()->topleftLon(),
285 task->tileSet()->bottomRightLat(), task->tileSet()->bottomRightLon(),
286 task->tileSet()->minZoom(), task->tileSet()->maxZoom(),
287 task->tileSet()->type(), task->tileSet()->totalTileCount());
288
289 if (!setID.has_value()) {
290 mtask->setError("Error saving tile set");
291 return;
292 }
293
294 task->tileSet()->blockSignals(true);
295 task->tileSet()->setId(setID.value());
296
297 const SetTotalsResult totals = _database->computeSetTotals(
298 setID.value(), task->tileSet()->defaultSet(),
299 task->tileSet()->totalTileCount(), task->tileSet()->type());
300 task->tileSet()->setSavedTileCount(totals.savedTileCount);
301 task->tileSet()->setSavedTileSize(totals.savedTileSize);
302 task->tileSet()->setTotalTileSize(totals.totalTileSize);
303 task->tileSet()->setUniqueTileCount(totals.uniqueTileCount);
304 task->tileSet()->setUniqueTileSize(totals.uniqueTileSize);
305 task->tileSet()->blockSignals(false);
306
307 task->setTileSetSaved();
308}
309
310void QGCCacheWorker::_getTileDownloadList(QGCMapTask *mtask)
311{
312 if (!_testTask(mtask)) {
313 return;
314 }
315
316 QGCGetTileDownloadListTask *task = static_cast<QGCGetTileDownloadListTask*>(mtask);
317 const QList<QGCTile> tileValues = _database->getTileDownloadList(task->setID(), task->count());
318 QQueue<QGCTile*> tiles;
319 for (const auto &t : tileValues) {
320 tiles.enqueue(new QGCTile(t));
321 }
322 task->setTileListFetched(tiles);
323}
324
325void QGCCacheWorker::_updateTileDownloadState(QGCMapTask *mtask)
326{
327 if (!_testTask(mtask)) {
328 return;
329 }
330
332 bool ok;
333 if (task->hash() == QStringLiteral("*")) {
334 ok = _database->updateAllTileDownloadStates(task->setID(), static_cast<int>(task->state()));
335 } else {
336 ok = _database->updateTileDownloadState(task->setID(), static_cast<int>(task->state()), task->hash());
337 }
338 if (!ok) {
339 mtask->setError("Error updating tile download state");
340 }
341}
342
343// M2: Check return value, don't signal success on failure
344void QGCCacheWorker::_pruneCache(QGCMapTask *mtask)
345{
346 if (!_testTask(mtask)) {
347 return;
348 }
349
350 QGCPruneCacheTask *task = static_cast<QGCPruneCacheTask*>(mtask);
351 if (!_database->pruneCache(task->amount())) {
352 mtask->setError("Error pruning cache");
353 return;
354 }
355 task->setPruned();
356}
357
358// M2: Check return value, don't signal success on failure
359void QGCCacheWorker::_deleteTileSet(QGCMapTask *mtask)
360{
361 if (!_testTask(mtask)) {
362 return;
363 }
364
365 QGCDeleteTileSetTask *task = static_cast<QGCDeleteTileSetTask*>(mtask);
366 if (!_database->deleteTileSet(task->setID())) {
367 mtask->setError("Error deleting tile set");
368 return;
369 }
370 _emitTotals();
371 task->setTileSetDeleted();
372}
373
374void QGCCacheWorker::_renameTileSet(QGCMapTask *mtask)
375{
376 if (!_testTask(mtask)) {
377 return;
378 }
379
380 QGCRenameTileSetTask *task = static_cast<QGCRenameTileSetTask*>(mtask);
381 if (!_database->renameTileSet(task->setID(), task->newName())) {
382 task->setError("Error renaming tile set");
383 }
384}
385
386// M2: Check return value, don't signal success on failure
387void QGCCacheWorker::_resetCacheDatabase(QGCMapTask *mtask)
388{
389 if (!_testTask(mtask)) {
390 return;
391 }
392
393 QGCResetTask *task = static_cast<QGCResetTask*>(mtask);
394 if (!_database->resetDatabase()) {
395 mtask->setError("Error resetting cache database");
396 return;
397 }
398 _dbValid = _database->isValid();
399 task->setResetCompleted();
400}
401
402// H4: Don't emit completion on failure
403void QGCCacheWorker::_importSets(QGCMapTask *mtask)
404{
405 if (!_testTask(mtask)) {
406 return;
407 }
408
409 QGCImportTileTask *task = static_cast<QGCImportTileTask*>(mtask);
410 auto progress = [task](int pct) { task->setProgress(pct); };
411
412 DatabaseResult result;
413 if (task->replace()) {
414 result = _database->importSetsReplace(task->path(), progress);
415 } else {
416 result = _database->importSetsMerge(task->path(), progress);
417 }
418
419 _dbValid = _database->isValid();
420
421 if (!result.success) {
422 task->setError(result.errorString);
423 return;
424 }
425
426 if (task->replace() && _database->isValid()) {
427 QSettings settings;
428 settings.remove(QLatin1String(QGCTileCacheDatabase::kBingNoTileDoneKey));
429 _database->deleteBingNoTileTiles();
430 }
431
432 task->setImportCompleted();
433}
434
435// H3: Records are now snapshotted on the main thread via QGCExportTileTask,
436// eliminating cross-thread reads of live QGCCachedTileSet objects.
437// H4: Don't emit completion on failure
438void QGCCacheWorker::_exportSets(QGCMapTask *mtask)
439{
440 if (!_testTask(mtask)) {
441 return;
442 }
443
444 QGCExportTileTask *task = static_cast<QGCExportTileTask*>(mtask);
445
446 auto progress = [task](int pct) { task->setProgress(pct); };
447 DatabaseResult result = _database->exportSets(task->sets(), task->path(), progress);
448
449 if (!result.success) {
450 task->setError(result.errorString);
451 return;
452 }
453
454 task->setExportCompleted();
455}
#define QGC_LOGGING_CATEGORY(name, categoryStr)
bool enqueueTask(QGCMapTask *task)
void updateTotals(quint32 totaltiles, quint64 totalsize, quint32 defaulttiles, quint64 defaultsize)
QGCCachedTileSet * tileSet()
Definition QGCMapTasks.h:94
quint64 setID() const
void setProgress(int percentage)
QString path() const
void setExportCompleted()
const QList< TileSetRecord > & sets() const
void setTileSetFetched(QGCCachedTileSet *tileSet)
Definition QGCMapTasks.h:66
QString hash() const
void setTileFetched(QGCCacheTile *tile)
void setTileListFetched(const QQueue< QGCTile * > &tiles)
void setImportCompleted()
bool replace() const
void setProgress(int percentage)
QString path() const
void setError(const QString &errorString=QString())
Definition QGCMapTasks.h:42
TaskType type() const
Definition QGCMapTasks.h:40
quint64 amount() const
QString newName() const
quint64 setID() const
void setResetCompleted()
const QGCCacheTile * tile() const
static constexpr const char * kBingNoTileDoneKey
QGCTile::TileState state() const
static QString getProviderTypeFromQtMapId(int qtMapId)
quint64 tileSet
QByteArray img
QString format