QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
QGCTileCacheDatabase.cpp
Go to the documentation of this file.
2
3#include <QtCore/QCoreApplication>
4#include <QtCore/QDateTime>
5#include <QtCore/QFile>
6#include <QtCore/QFileInfo>
7#include <QtCore/QSettings>
8#include <QtCore/QUuid>
9#include <QtSql/QSqlDatabase>
10#include <QtSql/QSqlError>
11#include <QtSql/QSqlQuery>
12
13#include <atomic>
14
15#include "QGCCacheTile.h"
16#include "QGCLoggingCategory.h"
17#include "QGCMapUrlEngine.h"
18#include "QGCSqlHelper.h"
19#include "QGCTile.h"
20#include "QGCTileSet.h"
21
22QGC_LOGGING_CATEGORY(QGCTileCacheDatabaseLog, "QtLocationPlugin.QGCTileCacheDatabase")
23
24static std::atomic<quint64> s_connectionCounter{0};
25
27 : _databasePath(databasePath)
28 , _connectionName(QStringLiteral("QGCTileCache_%1").arg(s_connectionCounter.fetch_add(1)))
29{
30}
31
36
37QSqlDatabase QGCTileCacheDatabase::_database() const
38{
39 return QSqlDatabase::database(_connectionName);
40}
41
43{
44 return _database();
45}
46
47bool QGCTileCacheDatabase::_ensureConnected() const
48{
49 if (!_connected || !_valid) {
50 qCWarning(QGCTileCacheDatabaseLog) << "Database not connected";
51 return false;
52 }
53 return true;
54}
55
56bool QGCTileCacheDatabase::_checkSchemaVersion()
57{
58 QSqlDatabase db = _database();
59 const auto current = QGCSqlHelper::userVersion(db);
60 if (!current) {
61 qCWarning(QGCTileCacheDatabaseLog) << "Failed to read schema version";
62 return false;
63 }
64
65 const int version = *current;
66 if (version == kSchemaVersion) {
67 return true;
68 }
69
70 QSqlQuery query(db);
71
72 if (version == 0) {
73 // Either a fresh database or a legacy database created before versioning.
74 // Check for existing data — if Tiles table exists with rows, it's legacy.
75 // Legacy DBs stored map type as text; migration is not supported so the cache is rebuilt.
76 if (query.exec("SELECT COUNT(*) FROM Tiles") && query.next() && query.value(0).toInt() > 0) {
77 qCWarning(QGCTileCacheDatabaseLog) << "Legacy database detected (no schema version). Discarding cached tiles and rebuilding.";
78 _defaultSet = kInvalidTileSet;
79 query.exec("DROP TABLE IF EXISTS TilesDownload");
80 query.exec("DROP TABLE IF EXISTS SetTiles");
81 query.exec("DROP TABLE IF EXISTS Tiles");
82 query.exec("DROP TABLE IF EXISTS TileSets");
83 }
84 return true;
85 }
86
87 // Future: handle incremental migrations here (version < kSchemaVersion).
88 qCWarning(QGCTileCacheDatabaseLog) << "Unknown schema version" << version << "(expected" << kSchemaVersion << "). Resetting cache.";
89 _defaultSet = kInvalidTileSet;
90 query.exec("DROP TABLE IF EXISTS TilesDownload");
91 query.exec("DROP TABLE IF EXISTS SetTiles");
92 query.exec("DROP TABLE IF EXISTS Tiles");
93 query.exec("DROP TABLE IF EXISTS TileSets");
94 return true;
95}
96
98{
99 _failed = false;
100 if (!_databasePath.isEmpty()) {
101 qCDebug(QGCTileCacheDatabaseLog) << "Mapping cache directory:" << _databasePath;
102 if (connectDB()) {
103 if (!_checkSchemaVersion()) {
104 _failed = true;
105 disconnectDB();
106 return false;
107 }
108 _valid = _createDB(_database());
109 if (!_valid) {
110 _failed = true;
111 (void) QFile::remove(_databasePath);
112 }
113 } else {
114 _failed = true;
115 }
116 disconnectDB();
117 } else {
118 qCCritical(QGCTileCacheDatabaseLog) << "Could not find suitable cache directory.";
119 _failed = true;
120 }
121
122 return !_failed;
123}
124
126{
127 if (_connected) {
128 disconnectDB();
129 }
130
131 QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", _connectionName);
132 db.setDatabaseName(_databasePath);
133 _valid = db.open();
134 if (_valid) {
136 _connected = true;
137 } else {
138 qCCritical(QGCTileCacheDatabaseLog) << "Map Cache SQL error (open db):" << db.lastError();
139 QSqlDatabase::removeDatabase(_connectionName);
140 }
141 return _valid;
142}
143
145{
146 if (!_connected) {
147 return;
148 }
149 _connected = false;
150
151 if (!QCoreApplication::instance()) {
152 return;
153 }
154
155 {
156 QSqlDatabase db = QSqlDatabase::database(_connectionName, false);
157 if (db.isOpen()) {
158 db.close();
159 }
160 }
161 QSqlDatabase::removeDatabase(_connectionName);
162}
163
164bool QGCTileCacheDatabase::saveTile(const QString &hash, const QString &format, const QByteArray &img, const QString &type, quint64 tileSet)
165{
166 if (!_ensureConnected()) {
167 return false;
168 }
169
170 QGCSqlHelper::Transaction txn(_database());
171 if (!txn.ok()) {
172 qCWarning(QGCTileCacheDatabaseLog) << "Failed to start transaction for saveTile";
173 return false;
174 }
175
176 QSqlQuery query(_database());
177 if (!query.prepare("INSERT OR IGNORE INTO Tiles(hash, format, tile, size, type, date) VALUES(?, ?, ?, ?, ?, ?)")) {
178 qCWarning(QGCTileCacheDatabaseLog) << "Map Cache SQL error (prepare saveTile):" << query.lastError().text();
179 return false;
180 }
181 query.addBindValue(hash);
182 query.addBindValue(format);
183 query.addBindValue(img);
184 query.addBindValue(img.size());
185 query.addBindValue(UrlFactory::getQtMapIdFromProviderType(type));
186 query.addBindValue(QDateTime::currentSecsSinceEpoch());
187 if (!query.exec()) {
188 qCWarning(QGCTileCacheDatabaseLog) << "Map Cache SQL error (saveTile INSERT):" << query.lastError().text();
189 return false;
190 }
191
192 if (!query.prepare("SELECT tileID FROM Tiles WHERE hash = ?")) {
193 qCWarning(QGCTileCacheDatabaseLog) << "Map Cache SQL error (prepare tile lookup):" << query.lastError().text();
194 return false;
195 }
196 query.addBindValue(hash);
197 if (!query.exec() || !query.next()) {
198 qCWarning(QGCTileCacheDatabaseLog) << "Map Cache SQL error (tile lookup):" << query.lastError().text();
199 return false;
200 }
201 const quint64 tileID = query.value(0).toULongLong();
202
203 const quint64 setID = (tileSet == kInvalidTileSet) ? _getDefaultTileSet() : tileSet;
204 if (setID == kInvalidTileSet) {
205 qCWarning(QGCTileCacheDatabaseLog) << "Cannot save tile: no valid tile set";
206 return false;
207 }
208 if (!query.prepare("INSERT OR IGNORE INTO SetTiles(tileID, setID) VALUES(?, ?)")) {
209 qCWarning(QGCTileCacheDatabaseLog) << "Map Cache SQL error (prepare SetTiles):" << query.lastError().text();
210 return false;
211 }
212 query.addBindValue(tileID);
213 query.addBindValue(setID);
214 if (!query.exec()) {
215 qCWarning(QGCTileCacheDatabaseLog) << "Map Cache SQL error (add tile into SetTiles):" << query.lastError().text();
216 return false;
217 }
218
219 if (!txn.commit()) {
220 qCWarning(QGCTileCacheDatabaseLog) << "Failed to commit saveTile transaction";
221 return false;
222 }
223
224 qCDebug(QGCTileCacheDatabaseLog) << "HASH:" << hash;
225 return true;
226}
227
228std::unique_ptr<QGCCacheTile> QGCTileCacheDatabase::getTile(const QString &hash)
229{
230 if (!_ensureConnected()) {
231 return nullptr;
232 }
233
234 QSqlQuery query(_database());
235 if (!query.prepare("SELECT tile, format, type FROM Tiles WHERE hash = ?")) {
236 return nullptr;
237 }
238 query.addBindValue(hash);
239 if (query.exec() && query.next()) {
240 const QByteArray tileData = query.value(0).toByteArray();
241 const QString format = query.value(1).toString();
242 const QString type = UrlFactory::getProviderTypeFromQtMapId(query.value(2).toInt());
243 qCDebug(QGCTileCacheDatabaseLog) << "(Found in DB) HASH:" << hash;
244 return std::make_unique<QGCCacheTile>(hash, tileData, format, type);
245 }
246
247 qCDebug(QGCTileCacheDatabaseLog) << "(NOT in DB) HASH:" << hash;
248 return nullptr;
249}
250
251std::optional<quint64> QGCTileCacheDatabase::findTile(const QString &hash)
252{
253 if (!_ensureConnected()) {
254 return std::nullopt;
255 }
256
257 QSqlQuery query(_database());
258 if (!query.prepare("SELECT tileID FROM Tiles WHERE hash = ?")) {
259 return std::nullopt;
260 }
261 query.addBindValue(hash);
262 if (query.exec() && query.next()) {
263 return query.value(0).toULongLong();
264 }
265
266 return std::nullopt;
267}
268
270{
271 QList<TileSetRecord> records;
272 if (!_ensureConnected()) {
273 return records;
274 }
275
276 QSqlQuery query(_database());
277 query.setForwardOnly(true);
278 if (!query.exec("SELECT setID, name, typeStr, topleftLat, topleftLon, bottomRightLat, bottomRightLon, "
279 "minZoom, maxZoom, type, numTiles, defaultSet, date "
280 "FROM TileSets ORDER BY defaultSet DESC, name ASC")) {
281 return records;
282 }
283
284 while (query.next()) {
285 TileSetRecord rec;
286 rec.setID = query.value(0).toULongLong();
287 rec.name = query.value(1).toString();
288 rec.mapTypeStr = query.value(2).toString();
289 rec.topleftLat = query.value(3).toDouble();
290 rec.topleftLon = query.value(4).toDouble();
291 rec.bottomRightLat = query.value(5).toDouble();
292 rec.bottomRightLon = query.value(6).toDouble();
293 rec.minZoom = query.value(7).toInt();
294 rec.maxZoom = query.value(8).toInt();
295 rec.type = query.value(9).toInt();
296 rec.numTiles = query.value(10).toUInt();
297 rec.defaultSet = (query.value(11).toInt() != 0);
298 rec.date = query.value(12).toULongLong();
299 records.append(rec);
300 }
301
302 return records;
303}
304
305std::optional<quint64> QGCTileCacheDatabase::createTileSet(const QString &name, const QString &mapTypeStr,
306 double topleftLat, double topleftLon,
307 double bottomRightLat, double bottomRightLon,
308 int minZoom, int maxZoom, const QString &type, quint32 numTiles)
309{
310 if (!_ensureConnected()) {
311 return std::nullopt;
312 }
313
314 QGCSqlHelper::Transaction txn(_database());
315 if (!txn.ok()) {
316 qCWarning(QGCTileCacheDatabaseLog) << "Failed to start transaction for createTileSet";
317 return std::nullopt;
318 }
319
320 QSqlQuery query(_database());
321 if (!query.prepare("INSERT INTO TileSets("
322 "name, typeStr, topleftLat, topleftLon, bottomRightLat, bottomRightLon, minZoom, maxZoom, type, numTiles, date"
323 ") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")) {
324 qCWarning(QGCTileCacheDatabaseLog) << "Map Cache SQL error (prepare createTileSet):" << query.lastError().text();
325 return std::nullopt;
326 }
327 query.addBindValue(name);
328 query.addBindValue(mapTypeStr);
329 query.addBindValue(topleftLat);
330 query.addBindValue(topleftLon);
331 query.addBindValue(bottomRightLat);
332 query.addBindValue(bottomRightLon);
333 query.addBindValue(minZoom);
334 query.addBindValue(maxZoom);
335 query.addBindValue(UrlFactory::getQtMapIdFromProviderType(type));
336 query.addBindValue(numTiles);
337 query.addBindValue(QDateTime::currentSecsSinceEpoch());
338 if (!query.exec()) {
339 qCWarning(QGCTileCacheDatabaseLog) << "Map Cache SQL error (add tileSet into TileSets):" << query.lastError().text();
340 return std::nullopt;
341 }
342
343 const quint64 setID = query.lastInsertId().toULongLong();
344
345 // Process tiles in streaming batches to avoid holding all coordinates in memory
346 constexpr int kHashBatchSize = 500;
347 const int mapTypeId = UrlFactory::getQtMapIdFromProviderType(type);
348
349 struct TileCoord { int x, y; QString hash; };
350
351 auto processBatch = [&](const QList<TileCoord> &tiles, int z) -> bool {
352 QHash<QString, quint64> existingTiles;
353 QSqlQuery lookup(_database());
354 lookup.setForwardOnly(true);
355 if (lookup.prepare(QStringLiteral("SELECT hash, tileID FROM Tiles WHERE hash IN (%1)").arg(QGCSqlHelper::placeholders(tiles.size())))) {
356 for (const auto &tc : tiles) {
357 lookup.addBindValue(tc.hash);
358 }
359 if (lookup.exec()) {
360 while (lookup.next()) {
361 existingTiles.insert(lookup.value(0).toString(), lookup.value(1).toULongLong());
362 }
363 }
364 }
365
366 for (const auto &tc : tiles) {
367 auto it = existingTiles.find(tc.hash);
368 if (it != existingTiles.end()) {
369 if (!query.prepare("INSERT OR IGNORE INTO SetTiles(tileID, setID) VALUES(?, ?)")) {
370 return false;
371 }
372 query.addBindValue(it.value());
373 query.addBindValue(setID);
374 if (!query.exec()) {
375 qCWarning(QGCTileCacheDatabaseLog) << "Map Cache SQL error (add tile into SetTiles):" << query.lastError().text();
376 return false;
377 }
378 } else {
379 if (!query.prepare("INSERT OR IGNORE INTO TilesDownload(setID, hash, type, x, y, z, state) VALUES(?, ?, ?, ?, ?, ?, ?)")) {
380 return false;
381 }
382 query.addBindValue(setID);
383 query.addBindValue(tc.hash);
384 query.addBindValue(mapTypeId);
385 query.addBindValue(tc.x);
386 query.addBindValue(tc.y);
387 query.addBindValue(z);
388 query.addBindValue(static_cast<int>(QGCTile::StatePending));
389 if (!query.exec()) {
390 qCWarning(QGCTileCacheDatabaseLog) << "Map Cache SQL error (add tile into TilesDownload):" << query.lastError().text();
391 return false;
392 }
393 }
394 }
395 return true;
396 };
397
398 for (int z = minZoom; z <= maxZoom; z++) {
399 const QGCTileSet set = UrlFactory::getTileCount(z, topleftLon, topleftLat, bottomRightLon, bottomRightLat, type);
400
401 QList<TileCoord> batch;
402 batch.reserve(kHashBatchSize);
403
404 for (int x = set.tileX0; x <= set.tileX1; x++) {
405 for (int y = set.tileY0; y <= set.tileY1; y++) {
406 batch.append({x, y, UrlFactory::getTileHash(type, x, y, z)});
407
408 if (batch.size() >= kHashBatchSize) {
409 if (!processBatch(batch, z)) return std::nullopt;
410 batch.clear();
411 }
412 }
413 }
414
415 if (!batch.isEmpty()) {
416 if (!processBatch(batch, z)) return std::nullopt;
417 }
418 }
419
420 if (!txn.commit()) {
421 qCWarning(QGCTileCacheDatabaseLog) << "Failed to commit createTileSet transaction";
422 return std::nullopt;
423 }
424
425 return setID;
426}
427
429{
430 if (!_ensureConnected()) {
431 return false;
432 }
433
434 QGCSqlHelper::Transaction txn(_database());
435 if (!txn.ok()) {
436 qCWarning(QGCTileCacheDatabaseLog) << "Failed to start transaction for deleteTileSet";
437 return false;
438 }
439
440 QSqlQuery query(_database());
441
442 // Delete download queue entries first
443 if (!query.prepare("DELETE FROM TilesDownload WHERE setID = ?")) {
444 qCWarning(QGCTileCacheDatabaseLog) << "Failed to prepare download delete:" << query.lastError().text();
445 return false;
446 }
447 query.addBindValue(id);
448 if (!query.exec()) {
449 return false;
450 }
451
452 // Find tiles unique to this set (not shared with other sets)
453 // Must collect IDs before deleting SetTiles links
454 QList<quint64> uniqueTileIDs;
455 if (query.prepare(QStringLiteral("SELECT tileID FROM SetTiles WHERE tileID IN (%1)").arg(kUniqueTilesSubquery))) {
456 query.addBindValue(id);
457 if (query.exec()) {
458 while (query.next()) {
459 uniqueTileIDs.append(query.value(0).toULongLong());
460 }
461 }
462 }
463
464 // Remove set-tile links
465 if (!query.prepare("DELETE FROM SetTiles WHERE setID = ?")) {
466 qCWarning(QGCTileCacheDatabaseLog) << "Failed to prepare SetTiles delete:" << query.lastError().text();
467 return false;
468 }
469 query.addBindValue(id);
470 if (!query.exec()) {
471 return false;
472 }
473
474 // Delete unique tiles (no longer referenced by any set)
475 if (!uniqueTileIDs.isEmpty()) {
476 if (query.prepare(QStringLiteral("DELETE FROM Tiles WHERE tileID IN (%1)").arg(QGCSqlHelper::placeholders(uniqueTileIDs.size())))) {
477 for (const quint64 tileID : uniqueTileIDs) {
478 query.addBindValue(tileID);
479 }
480 if (!query.exec()) {
481 qCWarning(QGCTileCacheDatabaseLog) << "Failed to delete unique tiles:" << query.lastError().text();
482 return false;
483 }
484 }
485 }
486
487 // Delete the tile set itself
488 if (!query.prepare("DELETE FROM TileSets WHERE setID = ?")) {
489 qCWarning(QGCTileCacheDatabaseLog) << "Failed to prepare TileSets delete:" << query.lastError().text();
490 return false;
491 }
492 query.addBindValue(id);
493 if (!query.exec()) {
494 return false;
495 }
496
497 if (id == _defaultSet) {
498 _defaultSet = kInvalidTileSet;
499 }
500
501 return txn.commit();
502}
503
504bool QGCTileCacheDatabase::renameTileSet(quint64 setID, const QString &newName)
505{
506 if (!_ensureConnected()) {
507 return false;
508 }
509
510 QSqlQuery query(_database());
511 if (!query.prepare("UPDATE TileSets SET name = ? WHERE setID = ?")) {
512 return false;
513 }
514 query.addBindValue(newName);
515 query.addBindValue(setID);
516 return query.exec();
517}
518
519std::optional<quint64> QGCTileCacheDatabase::findTileSetID(const QString &name)
520{
521 if (!_ensureConnected()) {
522 return std::nullopt;
523 }
524
525 QSqlQuery query(_database());
526 if (!query.prepare("SELECT setID FROM TileSets WHERE name = ?")) {
527 return std::nullopt;
528 }
529 query.addBindValue(name);
530 if (query.exec() && query.next()) {
531 return query.value(0).toULongLong();
532 }
533
534 return std::nullopt;
535}
536
538{
539 if (!_ensureConnected()) {
540 return false;
541 }
542
543 _defaultSet = kInvalidTileSet;
544
545 QGCSqlHelper::Transaction txn(_database());
546 if (!txn.ok()) {
547 qCWarning(QGCTileCacheDatabaseLog) << "Failed to start transaction for resetDatabase";
548 return false;
549 }
550 QSqlQuery query(_database());
551 if (!query.exec("DROP TABLE IF EXISTS TilesDownload") ||
552 !query.exec("DROP TABLE IF EXISTS SetTiles") ||
553 !query.exec("DROP TABLE IF EXISTS Tiles") ||
554 !query.exec("DROP TABLE IF EXISTS TileSets")) {
555 qCWarning(QGCTileCacheDatabaseLog) << "Failed to drop tables:" << query.lastError().text();
556 return false;
557 }
558 if (!txn.commit()) {
559 qCWarning(QGCTileCacheDatabaseLog) << "Failed to commit table drops in resetDatabase";
560 return false;
561 }
562 _valid = _createDB(_database());
563 return _valid;
564}
565
566QList<QGCTile> QGCTileCacheDatabase::getTileDownloadList(quint64 setID, int count)
567{
568 QList<QGCTile> tiles;
569 if (!_ensureConnected()) {
570 return tiles;
571 }
572
573 QGCSqlHelper::Transaction txn(_database());
574 if (!txn.ok()) {
575 qCWarning(QGCTileCacheDatabaseLog) << "Failed to start transaction for getTileDownloadList";
576 return tiles;
577 }
578
579 QSqlQuery query(_database());
580 if (!query.prepare("SELECT hash, type, x, y, z FROM TilesDownload WHERE setID = ? AND state = ? LIMIT ?")) {
581 qCWarning(QGCTileCacheDatabaseLog) << "Failed to prepare tile download list query:" << query.lastError().text();
582 return tiles;
583 }
584 query.addBindValue(setID);
585 query.addBindValue(static_cast<int>(QGCTile::StatePending));
586 query.addBindValue(count);
587 if (!query.exec()) {
588 return tiles;
589 }
590
591 while (query.next()) {
592 QGCTile tile;
593 tile.hash = query.value(0).toString();
594 tile.type = query.value(1).toInt();
595 tile.x = query.value(2).toInt();
596 tile.y = query.value(3).toInt();
597 tile.z = query.value(4).toInt();
598 tiles.append(std::move(tile));
599 }
600
601 if (!tiles.isEmpty()) {
602 if (query.prepare(QStringLiteral("UPDATE TilesDownload SET state = ? WHERE setID = ? AND hash IN (%1)").arg(QGCSqlHelper::placeholders(tiles.size())))) {
603 query.addBindValue(static_cast<int>(QGCTile::StateDownloading));
604 query.addBindValue(setID);
605 for (qsizetype i = 0; i < tiles.size(); i++) {
606 query.addBindValue(tiles[i].hash);
607 }
608 if (!query.exec()) {
609 qCWarning(QGCTileCacheDatabaseLog) << "Map Cache SQL error (batch set TilesDownload state):" << query.lastError().text();
610 tiles.clear();
611 return tiles;
612 }
613 }
614 }
615
616 if (!txn.commit()) {
617 qCWarning(QGCTileCacheDatabaseLog) << "Failed to commit getTileDownloadList transaction";
618 tiles.clear();
619 }
620
621 return tiles;
622}
623
624bool QGCTileCacheDatabase::updateTileDownloadState(quint64 setID, int state, const QString &hash)
625{
626 if (!_ensureConnected()) {
627 return false;
628 }
629
630 QSqlQuery query(_database());
631 if (state == QGCTile::StateComplete) {
632 if (!query.prepare("DELETE FROM TilesDownload WHERE setID = ? AND hash = ?")) {
633 return false;
634 }
635 query.addBindValue(setID);
636 query.addBindValue(hash);
637 } else {
638 if (!query.prepare("UPDATE TilesDownload SET state = ? WHERE setID = ? AND hash = ?")) {
639 return false;
640 }
641 query.addBindValue(state);
642 query.addBindValue(setID);
643 query.addBindValue(hash);
644 }
645
646 if (!query.exec()) {
647 qCWarning(QGCTileCacheDatabaseLog) << "Error:" << query.lastError().text();
648 return false;
649 }
650
651 return true;
652}
653
655{
656 if (!_ensureConnected()) {
657 return false;
658 }
659
660 QSqlQuery query(_database());
661 if (!query.prepare("UPDATE TilesDownload SET state = ? WHERE setID = ?")) {
662 return false;
663 }
664 query.addBindValue(state);
665 query.addBindValue(setID);
666
667 if (!query.exec()) {
668 qCWarning(QGCTileCacheDatabaseLog) << "Error:" << query.lastError().text();
669 return false;
670 }
671
672 return true;
673}
674
676{
677 if (!_ensureConnected()) {
678 return false;
679 }
680
681 quint64 remaining = amount;
682 while (remaining > 0) {
683 QSqlQuery query(_database());
684 query.setForwardOnly(true);
685 if (!query.prepare(QStringLiteral("SELECT tileID, size, hash FROM Tiles WHERE tileID IN (%1) ORDER BY date ASC LIMIT ?").arg(kUniqueTilesSubquery))) {
686 qCWarning(QGCTileCacheDatabaseLog) << "Failed to prepare prune query:" << query.lastError().text();
687 return false;
688 }
689 query.addBindValue(_getDefaultTileSet());
690 query.addBindValue(kPruneBatchSize);
691 if (!query.exec()) {
692 return false;
693 }
694
695 QList<quint64> tileIDs;
696 while (query.next() && (remaining > 0)) {
697 tileIDs << query.value(0).toULongLong();
698 const quint64 sz = query.value(1).toULongLong();
699 remaining = (sz >= remaining) ? 0 : remaining - sz;
700 qCDebug(QGCTileCacheDatabaseLog) << "HASH:" << query.value(2).toString();
701 }
702
703 if (tileIDs.isEmpty()) {
704 break;
705 }
706
707 QGCSqlHelper::Transaction txn(_database());
708 if (!txn.ok()) {
709 return false;
710 }
711
712 if (!_deleteTilesByIDs(tileIDs)) {
713 return false;
714 }
715
716 if (!txn.commit()) {
717 return false;
718 }
719 }
720
721 return true;
722}
723
725{
726 if (!_ensureConnected()) {
727 return;
728 }
729
730 QSettings settings;
731 if (settings.value(QLatin1String(kBingNoTileDoneKey), false).toBool()) {
732 return;
733 }
734
735 QFile file(QStringLiteral(":/res/BingNoTileBytes.dat"));
736 if (!file.open(QFile::ReadOnly)) {
737 qCWarning(QGCTileCacheDatabaseLog) << "Failed to Open File" << file.fileName() << ":" << file.errorString();
738 return;
739 }
740
741 const QByteArray noTileBytes = file.readAll();
742 file.close();
743
744 QSqlQuery query(_database());
745 query.setForwardOnly(true);
746 if (!query.prepare("SELECT tileID, hash FROM Tiles WHERE LENGTH(tile) = ? AND tile = ?")) {
747 qCWarning(QGCTileCacheDatabaseLog) << "Failed to prepare Bing no-tile query";
748 return;
749 }
750 query.addBindValue(noTileBytes.length());
751 query.addBindValue(noTileBytes);
752 if (!query.exec()) {
753 qCWarning(QGCTileCacheDatabaseLog) << "query failed";
754 return;
755 }
756
757 QList<quint64> idsToDelete;
758 while (query.next()) {
759 idsToDelete.append(query.value(0).toULongLong());
760 qCDebug(QGCTileCacheDatabaseLog) << "HASH:" << query.value(1).toString();
761 }
762
763 if (idsToDelete.isEmpty()) {
764 settings.setValue(QLatin1String(kBingNoTileDoneKey), true);
765 return;
766 }
767
768 QGCSqlHelper::Transaction txn(_database());
769 if (!txn.ok()) {
770 return;
771 }
772
773 bool allSucceeded = true;
774 for (qsizetype offset = 0; offset < idsToDelete.size(); offset += kPruneBatchSize) {
775 const qsizetype batchEnd = qMin(offset + static_cast<qsizetype>(kPruneBatchSize), idsToDelete.size());
776 const QList<quint64> batch = idsToDelete.mid(offset, batchEnd - offset);
777 if (!_deleteTilesByIDs(batch)) {
778 allSucceeded = false;
779 break;
780 }
781 }
782
783 if (allSucceeded && txn.commit()) {
784 settings.setValue(QLatin1String(kBingNoTileDoneKey), true);
785 }
786}
787
789{
790 TotalsResult result;
791 if (!_ensureConnected()) {
792 return result;
793 }
794
795 QSqlQuery query(_database());
796
797 if (query.exec("SELECT COUNT(size), SUM(size) FROM Tiles") && query.next()) {
798 result.totalCount = query.value(0).toUInt();
799 result.totalSize = query.value(1).toULongLong();
800 }
801
802 if (!query.prepare(QStringLiteral("SELECT COUNT(size), SUM(size) FROM Tiles WHERE tileID IN (%1)").arg(kUniqueTilesSubquery))) {
803 return result;
804 }
805 query.addBindValue(_getDefaultTileSet());
806 if (query.exec() && query.next()) {
807 result.defaultCount = query.value(0).toUInt();
808 result.defaultSize = query.value(1).toULongLong();
809 }
810
811 return result;
812}
813
814SetTotalsResult QGCTileCacheDatabase::computeSetTotals(quint64 setID, bool isDefault, quint32 totalTileCount, const QString &type)
815{
816 SetTotalsResult result;
817
818 if (isDefault) {
819 TotalsResult totals = computeTotals();
820 result.savedTileCount = totals.totalCount;
821 result.savedTileSize = totals.totalSize;
822 result.totalTileSize = totals.totalSize;
823 result.uniqueTileCount = totals.defaultCount;
824 result.uniqueTileSize = totals.defaultSize;
825 return result;
826 }
827
828 if (!_ensureConnected()) {
829 return result;
830 }
831
832 QSqlQuery subquery(_database());
833 if (!subquery.prepare("SELECT COUNT(size), SUM(size) FROM Tiles A INNER JOIN SetTiles B ON A.tileID = B.tileID WHERE B.setID = ?")) {
834 return result;
835 }
836 subquery.addBindValue(setID);
837 if (!subquery.exec() || !subquery.next()) {
838 return result;
839 }
840
841 result.savedTileCount = subquery.value(0).toUInt();
842 result.savedTileSize = subquery.value(1).toULongLong();
843
844 quint64 avg = UrlFactory::averageSizeForType(type);
845 if (avg == 0) {
846 avg = 4096;
847 }
848 if (totalTileCount <= result.savedTileCount) {
849 result.totalTileSize = result.savedTileSize;
850 } else {
851 if ((result.savedTileCount > 10) && result.savedTileSize) {
852 avg = result.savedTileSize / result.savedTileCount;
853 }
854 result.totalTileSize = avg * totalTileCount;
855 }
856
857 quint32 dbUniqueCount = 0;
858 quint64 dbUniqueSize = 0;
859 if (subquery.prepare(QStringLiteral("SELECT COUNT(size), SUM(size) FROM Tiles WHERE tileID IN (%1)").arg(kUniqueTilesSubquery))) {
860 subquery.addBindValue(setID);
861 if (subquery.exec() && subquery.next()) {
862 dbUniqueCount = subquery.value(0).toUInt();
863 dbUniqueSize = subquery.value(1).toULongLong();
864 }
865 } else {
866 qCWarning(QGCTileCacheDatabaseLog) << "Failed to prepare unique tiles query:" << subquery.lastError().text();
867 }
868
869 if (dbUniqueCount > 0) {
870 result.uniqueTileCount = dbUniqueCount;
871 result.uniqueTileSize = dbUniqueSize;
872 } else {
873 const quint32 estimatedCount = (totalTileCount > result.savedTileCount) ? (totalTileCount - result.savedTileCount) : 0;
874 result.uniqueTileCount = estimatedCount;
875 result.uniqueTileSize = estimatedCount * avg;
876 }
877
878 return result;
879}
880
882{
883 DatabaseResult result;
884 if (QFileInfo(path).canonicalFilePath() == QFileInfo(_databasePath).canonicalFilePath()) {
885 result.errorString = "Import path must differ from the active database";
886 return result;
887 }
888 _defaultSet = kInvalidTileSet;
889 disconnectDB();
890 const QString backupPath = _databasePath + QStringLiteral(".bak");
891 (void) QFile::remove(backupPath);
892 const bool hasBackup = QFile::rename(_databasePath, backupPath);
893 if (!hasBackup) {
894 (void) QFile::remove(_databasePath);
895 }
896 if (!QFile::copy(path, _databasePath)) {
897 if (hasBackup) {
898 (void) QFile::rename(backupPath, _databasePath);
899 }
900 result.errorString = "Failed to copy import database";
901 _valid = false;
902 _failed = true;
903 return result;
904 }
905 (void) QFile::remove(backupPath);
906 if (progressCb) progressCb(25);
907 init();
908 if (!_valid) {
909 result.errorString = QStringLiteral("Failed to initialize tile cache database after import");
910 } else {
911 if (progressCb) progressCb(50);
912 connectDB();
913 if (!_valid) {
914 result.errorString = QStringLiteral("Failed to connect to tile cache database after import");
915 }
916 }
917 if (progressCb) progressCb(100);
918 result.success = _valid;
919 return result;
920}
921
923{
924 DatabaseResult result;
925 if (QFileInfo(path).canonicalFilePath() == QFileInfo(_databasePath).canonicalFilePath()) {
926 result.errorString = "Import path must differ from the active database";
927 return result;
928 }
929 if (!_ensureConnected()) {
930 result.errorString = "Database not connected";
931 return result;
932 }
933
934 QGCSqlHelper::ScopedConnection importDB(path, /*readOnly=*/true,
935 QStringLiteral("QGeoTileImportSession"));
936 if (!importDB.isValid()) {
937 result.errorString = "Error opening import database";
938 return result;
939 }
940
941 QSqlQuery query(importDB.database());
942 quint64 tileCount = 0;
943 int lastProgress = -1;
944 if (query.exec("SELECT COUNT(tileID) FROM Tiles") && query.next()) {
945 tileCount = query.value(0).toULongLong();
946 }
947
948 bool tilesImported = false;
949
950 if (tileCount > 0) {
951 if (query.exec("SELECT * FROM TileSets ORDER BY defaultSet DESC, name ASC")) {
952 quint64 currentCount = 0;
953 while (query.next()) {
954 QString name = query.value("name").toString();
955 const quint64 setID = query.value("setID").toULongLong();
956 const QString mapType = query.value("typeStr").toString();
957 const double topleftLat = query.value("topleftLat").toDouble();
958 const double topleftLon = query.value("topleftLon").toDouble();
959 const double bottomRightLat = query.value("bottomRightLat").toDouble();
960 const double bottomRightLon = query.value("bottomRightLon").toDouble();
961 const int minZoom = query.value("minZoom").toInt();
962 const int maxZoom = query.value("maxZoom").toInt();
963 const int type = query.value("type").toInt();
964 const quint32 numTiles = query.value("numTiles").toUInt();
965 const int defaultSet = query.value("defaultSet").toInt();
966 quint64 insertSetID = _getDefaultTileSet();
967
968 // Wrap each set creation + tile copy in a single transaction
969 QGCSqlHelper::Transaction txn(_database());
970 if (!txn.ok()) {
971 result.errorString = "Failed to start transaction for import set";
972 break;
973 }
974
975 if (defaultSet == 0) {
976 name = _deduplicateSetName(name);
977 QSqlQuery cQuery(_database());
978 if (!cQuery.prepare("INSERT INTO TileSets("
979 "name, typeStr, topleftLat, topleftLon, bottomRightLat, bottomRightLon, minZoom, maxZoom, type, numTiles, defaultSet, date"
980 ") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")) {
981 result.errorString = "Error preparing tile set insert";
982 break;
983 }
984 cQuery.addBindValue(name);
985 cQuery.addBindValue(mapType);
986 cQuery.addBindValue(topleftLat);
987 cQuery.addBindValue(topleftLon);
988 cQuery.addBindValue(bottomRightLat);
989 cQuery.addBindValue(bottomRightLon);
990 cQuery.addBindValue(minZoom);
991 cQuery.addBindValue(maxZoom);
992 cQuery.addBindValue(type);
993 cQuery.addBindValue(numTiles);
994 cQuery.addBindValue(defaultSet);
995 cQuery.addBindValue(QDateTime::currentSecsSinceEpoch());
996 if (!cQuery.exec()) {
997 result.errorString = "Error adding imported tile set to database";
998 break;
999 }
1000 insertSetID = cQuery.lastInsertId().toULongLong();
1001 }
1002
1003 quint64 tilesIterated = 0;
1004 const quint64 tilesSaved = _copyTilesForSet(importDB.database(), setID, insertSetID,
1005 currentCount, tileCount,
1006 lastProgress, progressCb,
1007 &tilesIterated, false);
1008 if (tilesSaved > 0) {
1009 tilesImported = true;
1010 QSqlQuery cQuery(_database());
1011 if (cQuery.prepare("SELECT COUNT(size) FROM Tiles A INNER JOIN SetTiles B ON A.tileID = B.tileID WHERE B.setID = ?")) {
1012 cQuery.addBindValue(insertSetID);
1013 if (cQuery.exec() && cQuery.next()) {
1014 const quint64 count = cQuery.value(0).toULongLong();
1015 if (cQuery.prepare("UPDATE TileSets SET numTiles = ? WHERE setID = ?")) {
1016 cQuery.addBindValue(count);
1017 cQuery.addBindValue(insertSetID);
1018 (void) cQuery.exec();
1019 }
1020 }
1021 }
1022 }
1023
1024 if (!txn.commit()) {
1025 qCWarning(QGCTileCacheDatabaseLog) << "Failed to commit import transaction for set:" << name;
1026 continue;
1027 }
1028
1029 if (tilesIterated > tilesSaved) {
1030 const quint64 alreadyExisting = tilesIterated - tilesSaved;
1031 tileCount = (alreadyExisting < tileCount) ? tileCount - alreadyExisting : 0;
1032 }
1033
1034 if ((tilesSaved == 0) && (defaultSet == 0)) {
1035 qCDebug(QGCTileCacheDatabaseLog) << "No unique tiles in" << name << "Removing it.";
1036 deleteTileSet(insertSetID);
1037 }
1038 }
1039 } else {
1040 result.errorString = "No tile set in database";
1041 }
1042 }
1043
1044 if (!tilesImported && result.errorString.isEmpty()) {
1045 result.errorString = "No unique tiles in imported database";
1046 }
1047 result.success = result.errorString.isEmpty();
1048 return result;
1049}
1050
1051DatabaseResult QGCTileCacheDatabase::exportSets(const QList<TileSetRecord> &sets, const QString &path, ProgressCallback progressCb)
1052{
1053 DatabaseResult result;
1054 if (!_ensureConnected()) {
1055 result.errorString = "Database not connected";
1056 return result;
1057 }
1058 if (QFileInfo(path).canonicalFilePath() == QFileInfo(_databasePath).canonicalFilePath()) {
1059 result.errorString = "Export path must differ from the active database";
1060 return result;
1061 }
1062
1063 (void) QFile::remove(path);
1064 QGCSqlHelper::ScopedConnection exportDB(path, /*readOnly=*/false,
1065 QStringLiteral("QGeoTileExportSession"));
1066 if (!exportDB.isValid()) {
1067 qCCritical(QGCTileCacheDatabaseLog) << "Map Cache SQL error (create export database):" << exportDB.database().lastError();
1068 result.errorString = "Error opening export database";
1069 return result;
1070 }
1071
1072 if (!_createDB(exportDB.database(), false)) {
1073 result.errorString = "Error creating export database";
1074 return result;
1075 }
1076
1077 quint64 tileCount = 0;
1078 quint64 currentCount = 0;
1079 int lastProgress = -1;
1080 for (const auto &set : sets) {
1081 QSqlQuery countQuery(_database());
1082 quint64 actualCount = 0;
1083 if (countQuery.prepare("SELECT COUNT(*) FROM Tiles T INNER JOIN SetTiles S ON T.tileID = S.tileID WHERE S.setID = ?")) {
1084 countQuery.addBindValue(set.setID);
1085 if (countQuery.exec() && countQuery.next()) {
1086 actualCount = countQuery.value(0).toULongLong();
1087 }
1088 }
1089 tileCount += (actualCount > 0) ? actualCount : set.numTiles;
1090 }
1091
1092 if (tileCount == 0) {
1093 tileCount = 1;
1094 }
1095
1096 for (const auto &set : sets) {
1097 QSqlQuery query(_database());
1098 query.setForwardOnly(true);
1099 if (!query.prepare("SELECT T.hash, T.format, T.tile, T.type, T.date FROM Tiles T "
1100 "INNER JOIN SetTiles S ON T.tileID = S.tileID WHERE S.setID = ?")) {
1101 qCWarning(QGCTileCacheDatabaseLog) << "Failed to prepare tile query for export set" << set.name;
1102 continue;
1103 }
1104 query.addBindValue(set.setID);
1105 if (!query.exec()) {
1106 qCWarning(QGCTileCacheDatabaseLog) << "Failed to query tiles for export set" << set.name;
1107 continue;
1108 }
1109
1110 QGCSqlHelper::Transaction txn(exportDB.database());
1111 if (!txn.ok()) {
1112 qCWarning(QGCTileCacheDatabaseLog) << "Failed to start transaction for export set" << set.name;
1113 result.errorString = "Failed to start export transaction";
1114 break;
1115 }
1116
1117 QSqlQuery exportQuery(exportDB.database());
1118 if (!exportQuery.prepare("INSERT INTO TileSets("
1119 "name, typeStr, topleftLat, topleftLon, bottomRightLat, bottomRightLon, minZoom, maxZoom, type, numTiles, defaultSet, date"
1120 ") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")) {
1121 result.errorString = "Error preparing tile set insert for export";
1122 break;
1123 }
1124 exportQuery.addBindValue(set.name);
1125 exportQuery.addBindValue(set.mapTypeStr);
1126 exportQuery.addBindValue(set.topleftLat);
1127 exportQuery.addBindValue(set.topleftLon);
1128 exportQuery.addBindValue(set.bottomRightLat);
1129 exportQuery.addBindValue(set.bottomRightLon);
1130 exportQuery.addBindValue(set.minZoom);
1131 exportQuery.addBindValue(set.maxZoom);
1132 exportQuery.addBindValue(set.type);
1133 exportQuery.addBindValue(set.numTiles);
1134 exportQuery.addBindValue(set.defaultSet);
1135 exportQuery.addBindValue(set.date);
1136 if (!exportQuery.exec()) {
1137 result.errorString = "Error adding tile set to exported database";
1138 break;
1139 }
1140
1141 const quint64 exportSetID = exportQuery.lastInsertId().toULongLong();
1142
1143 quint64 skippedTiles = 0;
1144 while (query.next()) {
1145 const QString hash = query.value(0).toString();
1146 const QString format = query.value(1).toString();
1147 const QByteArray img = query.value(2).toByteArray();
1148 const int tileType = query.value(3).toInt();
1149 const quint64 tileDate = query.value(4).toULongLong();
1150
1151 quint64 exportTileID = 0;
1152 if (!exportQuery.prepare("INSERT INTO Tiles(hash, format, tile, size, type, date) VALUES(?, ?, ?, ?, ?, ?)")) {
1153 qCWarning(QGCTileCacheDatabaseLog) << "Failed to prepare tile INSERT for export:" << exportQuery.lastError().text();
1154 skippedTiles++;
1155 continue;
1156 }
1157 exportQuery.addBindValue(hash);
1158 exportQuery.addBindValue(format);
1159 exportQuery.addBindValue(img);
1160 exportQuery.addBindValue(img.size());
1161 exportQuery.addBindValue(tileType);
1162 exportQuery.addBindValue(tileDate);
1163 if (exportQuery.exec()) {
1164 exportTileID = exportQuery.lastInsertId().toULongLong();
1165 } else {
1166 QSqlQuery lookup(exportDB.database());
1167 if (lookup.prepare("SELECT tileID FROM Tiles WHERE hash = ?")) {
1168 lookup.addBindValue(hash);
1169 if (lookup.exec() && lookup.next()) {
1170 exportTileID = lookup.value(0).toULongLong();
1171 }
1172 }
1173 }
1174
1175 if (exportTileID > 0) {
1176 if (exportQuery.prepare("INSERT OR IGNORE INTO SetTiles(tileID, setID) VALUES(?, ?)")) {
1177 exportQuery.addBindValue(exportTileID);
1178 exportQuery.addBindValue(exportSetID);
1179 if (!exportQuery.exec()) {
1180 qCWarning(QGCTileCacheDatabaseLog) << "Failed to link tile to set in export:" << exportQuery.lastError().text();
1181 }
1182 }
1183 } else {
1184 skippedTiles++;
1185 }
1186 currentCount++;
1187 if (progressCb) {
1188 const int progress = qMin(100, static_cast<int>((static_cast<double>(currentCount) / static_cast<double>(tileCount)) * 100.0));
1189 if (lastProgress != progress) {
1190 lastProgress = progress;
1191 progressCb(progress);
1192 }
1193 }
1194 }
1195 if (skippedTiles > 0) {
1196 qCWarning(QGCTileCacheDatabaseLog) << "Skipped" << skippedTiles << "tiles during export of" << set.name;
1197 }
1198 if (!txn.commit()) {
1199 qCWarning(QGCTileCacheDatabaseLog) << "Failed to commit export transaction for" << set.name;
1200 }
1201 }
1202
1203 result.success = result.errorString.isEmpty();
1204 return result;
1205}
1206
1207bool QGCTileCacheDatabase::_createDB(QSqlDatabase db, bool createDefault)
1208{
1209 // applySqlitePragmas (in connectDB / ScopedConnection ctor) already
1210 // enabled foreign_keys; nothing to redo here.
1211 QSqlQuery query(db);
1212
1213 if (!query.exec(
1214 "CREATE TABLE IF NOT EXISTS Tiles ("
1215 "tileID INTEGER PRIMARY KEY NOT NULL, "
1216 "hash TEXT NOT NULL UNIQUE, "
1217 "format TEXT NOT NULL, "
1218 "tile BLOB NULL, "
1219 "size INTEGER, "
1220 "type INTEGER, "
1221 "date INTEGER DEFAULT 0)"))
1222 {
1223 qCWarning(QGCTileCacheDatabaseLog) << "Map Cache SQL error (create Tiles db):" << query.lastError().text();
1224 return false;
1225 }
1226
1227 if (!query.exec(
1228 "CREATE TABLE IF NOT EXISTS TileSets ("
1229 "setID INTEGER PRIMARY KEY NOT NULL, "
1230 "name TEXT NOT NULL UNIQUE, "
1231 "typeStr TEXT, "
1232 "topleftLat REAL DEFAULT 0.0, "
1233 "topleftLon REAL DEFAULT 0.0, "
1234 "bottomRightLat REAL DEFAULT 0.0, "
1235 "bottomRightLon REAL DEFAULT 0.0, "
1236 "minZoom INTEGER DEFAULT 3, "
1237 "maxZoom INTEGER DEFAULT 3, "
1238 "type INTEGER DEFAULT -1, "
1239 "numTiles INTEGER DEFAULT 0, "
1240 "defaultSet INTEGER DEFAULT 0, "
1241 "date INTEGER DEFAULT 0)"))
1242 {
1243 qCWarning(QGCTileCacheDatabaseLog) << "Map Cache SQL error (create TileSets db):" << query.lastError().text();
1244 return false;
1245 }
1246
1247 if (!query.exec(
1248 "CREATE TABLE IF NOT EXISTS SetTiles ("
1249 "setID INTEGER NOT NULL REFERENCES TileSets(setID) ON DELETE CASCADE, "
1250 "tileID INTEGER NOT NULL REFERENCES Tiles(tileID) ON DELETE CASCADE)"))
1251 {
1252 qCWarning(QGCTileCacheDatabaseLog) << "Map Cache SQL error (create SetTiles db):" << query.lastError().text();
1253 return false;
1254 }
1255
1256 if (!query.exec(
1257 "CREATE TABLE IF NOT EXISTS TilesDownload ("
1258 "setID INTEGER NOT NULL REFERENCES TileSets(setID) ON DELETE CASCADE, "
1259 "hash TEXT NOT NULL, "
1260 "type INTEGER, "
1261 "x INTEGER, "
1262 "y INTEGER, "
1263 "z INTEGER, "
1264 "state INTEGER DEFAULT 0)"))
1265 {
1266 qCWarning(QGCTileCacheDatabaseLog) << "Map Cache SQL error (create TilesDownload db):" << query.lastError().text();
1267 return false;
1268 }
1269
1270 static const char *indexStatements[] = {
1271 "CREATE UNIQUE INDEX IF NOT EXISTS idx_settiles_unique ON SetTiles(tileID, setID)",
1272 "CREATE INDEX IF NOT EXISTS idx_settiles_setid ON SetTiles(setID)",
1273 "CREATE INDEX IF NOT EXISTS idx_settiles_tileid ON SetTiles(tileID)",
1274 "CREATE UNIQUE INDEX IF NOT EXISTS idx_tilesdownload_setid_hash ON TilesDownload(setID, hash)",
1275 "CREATE INDEX IF NOT EXISTS idx_tilesdownload_setid_state ON TilesDownload(setID, state)",
1276 "CREATE INDEX IF NOT EXISTS idx_tiles_date ON Tiles(date)",
1277 };
1278 for (const char *sql : indexStatements) {
1279 if (!query.exec(QLatin1String(sql))) {
1280 qCWarning(QGCTileCacheDatabaseLog) << "Failed to create index:" << sql << query.lastError().text();
1281 }
1282 }
1283
1285 qCWarning(QGCTileCacheDatabaseLog) << "Failed to set schema version";
1286 }
1287
1288 if (!createDefault) {
1289 return true;
1290 }
1291
1292 if (!query.prepare("SELECT name FROM TileSets WHERE name = ?")) {
1293 qCWarning(QGCTileCacheDatabaseLog) << "Map Cache SQL error (prepare default set check):" << db.lastError();
1294 return false;
1295 }
1296 query.addBindValue(QStringLiteral("Default Tile Set"));
1297 if (!query.exec()) {
1298 qCWarning(QGCTileCacheDatabaseLog) << "Map Cache SQL error (Looking for default tile set):" << db.lastError();
1299 return true;
1300 }
1301 if (query.next()) {
1302 return true;
1303 }
1304
1305 if (!query.prepare("INSERT INTO TileSets(name, defaultSet, date) VALUES(?, ?, ?)")) {
1306 qCWarning(QGCTileCacheDatabaseLog) << "Map Cache SQL error (prepare default tile set):" << db.lastError();
1307 return false;
1308 }
1309 query.addBindValue(QStringLiteral("Default Tile Set"));
1310 query.addBindValue(1);
1311 query.addBindValue(QDateTime::currentSecsSinceEpoch());
1312 if (!query.exec()) {
1313 qCWarning(QGCTileCacheDatabaseLog) << "Map Cache SQL error (Creating default tile set):" << db.lastError();
1314 return false;
1315 }
1316
1317 return true;
1318}
1319
1320quint64 QGCTileCacheDatabase::_getDefaultTileSet()
1321{
1322 if (_defaultSet != kInvalidTileSet) {
1323 return _defaultSet;
1324 }
1325
1326 if (!_ensureConnected()) {
1327 return kInvalidTileSet;
1328 }
1329
1330 QSqlQuery query(_database());
1331 if (query.exec("SELECT setID FROM TileSets WHERE defaultSet = 1") && query.next()) {
1332 _defaultSet = query.value(0).toULongLong();
1333 return _defaultSet;
1334 }
1335
1336 qCWarning(QGCTileCacheDatabaseLog) << "Default tile set not found in database";
1337 return kInvalidTileSet;
1338}
1339
1340bool QGCTileCacheDatabase::_deleteTilesByIDs(const QList<quint64> &ids)
1341{
1342 if (ids.isEmpty()) {
1343 return true;
1344 }
1345
1346 QSqlQuery query(_database());
1347 if (!query.prepare(QStringLiteral("DELETE FROM Tiles WHERE tileID IN (%1)").arg(QGCSqlHelper::placeholders(ids.size())))) {
1348 return false;
1349 }
1350 for (const quint64 id : ids) {
1351 query.addBindValue(id);
1352 }
1353 return query.exec();
1354}
1355
1356QString QGCTileCacheDatabase::_deduplicateSetName(const QString &name)
1357{
1358 if (!findTileSetID(name).has_value()) {
1359 return name;
1360 }
1361
1362 QSet<QString> existing;
1363 existing.insert(name);
1364 QSqlQuery query(_database());
1365 QString escaped = name;
1366 escaped.replace(QLatin1Char('\\'), QStringLiteral("\\\\"));
1367 escaped.replace(QLatin1Char('%'), QStringLiteral("\\%"));
1368 escaped.replace(QLatin1Char('_'), QStringLiteral("\\_"));
1369 if (query.prepare(QStringLiteral("SELECT name FROM TileSets WHERE name LIKE ? || ' %' ESCAPE '\\'"))) {
1370 query.addBindValue(escaped);
1371 if (query.exec()) {
1372 while (query.next()) {
1373 existing.insert(query.value(0).toString());
1374 }
1375 }
1376 }
1377
1378 for (int i = 1; i <= 9999; i++) {
1379 const QString candidate = QStringLiteral("%1 %2").arg(name).arg(i, 4, 10, QChar('0'));
1380 if (!existing.contains(candidate)) {
1381 return candidate;
1382 }
1383 }
1384
1385 return QStringLiteral("%1 %2").arg(name, QUuid::createUuid().toString(QUuid::WithoutBraces).left(8));
1386}
1387
1388quint64 QGCTileCacheDatabase::_copyTilesForSet(QSqlDatabase srcDB, quint64 srcSetID, quint64 dstSetID,
1389 quint64 &currentCount, quint64 tileCount,
1390 int &lastProgress, ProgressCallback progressCb,
1391 quint64 *tilesIteratedOut, bool useTransaction)
1392{
1393 QSqlQuery subQuery(srcDB);
1394 subQuery.setForwardOnly(true);
1395 if (!subQuery.prepare("SELECT T.hash, T.format, T.tile, T.type, T.date FROM Tiles T "
1396 "INNER JOIN SetTiles S ON T.tileID = S.tileID WHERE S.setID = ?")) {
1397 if (tilesIteratedOut) *tilesIteratedOut = 0;
1398 return 0;
1399 }
1400 subQuery.addBindValue(srcSetID);
1401 if (!subQuery.exec()) {
1402 if (tilesIteratedOut) *tilesIteratedOut = 0;
1403 return 0;
1404 }
1405
1406 quint64 tilesFound = 0;
1407 quint64 tilesLinked = 0;
1408
1409 std::unique_ptr<QGCSqlHelper::Transaction> txn;
1410 if (useTransaction) {
1411 txn = std::make_unique<QGCSqlHelper::Transaction>(_database());
1412 if (!txn->ok()) {
1413 qCWarning(QGCTileCacheDatabaseLog) << "Failed to start transaction for merge import";
1414 if (tilesIteratedOut) *tilesIteratedOut = 0;
1415 return 0;
1416 }
1417 }
1418
1419 QSqlQuery cQuery(_database());
1420 while (subQuery.next()) {
1421 tilesFound++;
1422 const QString hash = subQuery.value(0).toString();
1423 const QString format = subQuery.value(1).toString();
1424 const QByteArray img = subQuery.value(2).toByteArray();
1425 const int tileType = subQuery.value(3).toInt();
1426 const quint64 tileDate = subQuery.value(4).toULongLong();
1427
1428 quint64 importTileID = 0;
1429 if (cQuery.prepare("INSERT INTO Tiles(hash, format, tile, size, type, date) VALUES(?, ?, ?, ?, ?, ?)")) {
1430 cQuery.addBindValue(hash);
1431 cQuery.addBindValue(format);
1432 cQuery.addBindValue(img);
1433 cQuery.addBindValue(img.size());
1434 cQuery.addBindValue(tileType);
1435 cQuery.addBindValue(tileDate);
1436 if (cQuery.exec()) {
1437 importTileID = cQuery.lastInsertId().toULongLong();
1438 } else {
1439 if (cQuery.prepare("SELECT tileID FROM Tiles WHERE hash = ?")) {
1440 cQuery.addBindValue(hash);
1441 if (cQuery.exec() && cQuery.next()) {
1442 importTileID = cQuery.value(0).toULongLong();
1443 }
1444 }
1445 }
1446 }
1447
1448 if (importTileID > 0) {
1449 if (cQuery.prepare("INSERT OR IGNORE INTO SetTiles(tileID, setID) VALUES(?, ?)")) {
1450 cQuery.addBindValue(importTileID);
1451 cQuery.addBindValue(dstSetID);
1452 if (cQuery.exec() && cQuery.numRowsAffected() > 0) {
1453 tilesLinked++;
1454 }
1455 }
1456 }
1457
1458 currentCount++;
1459 if (tileCount > 0 && progressCb) {
1460 const int progress = qMin(100, static_cast<int>((static_cast<double>(currentCount) / static_cast<double>(tileCount)) * 100.0));
1461 if (lastProgress != progress) {
1462 lastProgress = progress;
1463 progressCb(progress);
1464 }
1465 }
1466 }
1467
1468 if (txn && !txn->commit()) {
1469 qCWarning(QGCTileCacheDatabaseLog) << "Failed to commit merge import transaction";
1470 if (tilesIteratedOut) *tilesIteratedOut = tilesFound;
1471 return 0;
1472 }
1473
1474 if (tilesIteratedOut) *tilesIteratedOut = tilesFound;
1475 return tilesLinked;
1476}
#define QGC_LOGGING_CATEGORY(name, categoryStr)
static std::atomic< quint64 > s_connectionCounter
std::function< void(int)> ProgressCallback
RAII wrapper around QSqlDatabase::addDatabase / removeDatabase.
QSqlDatabase database() const
RAII wrapper around QSqlDatabase::transaction()/commit()/rollback().
std::optional< quint64 > findTileSetID(const QString &name)
std::optional< quint64 > createTileSet(const QString &name, const QString &mapTypeStr, double topleftLat, double topleftLon, double bottomRightLat, double bottomRightLon, int minZoom, int maxZoom, const QString &type, quint32 numTiles)
bool updateAllTileDownloadStates(quint64 setID, int state)
bool pruneCache(quint64 amount)
std::unique_ptr< QGCCacheTile > getTile(const QString &hash)
SetTotalsResult computeSetTotals(quint64 setID, bool isDefault, quint32 totalTileCount, const QString &type)
DatabaseResult exportSets(const QList< TileSetRecord > &sets, const QString &path, ProgressCallback progressCb)
static constexpr quint64 kInvalidTileSet
bool updateTileDownloadState(quint64 setID, int state, const QString &hash)
static constexpr const char * kBingNoTileDoneKey
DatabaseResult importSetsReplace(const QString &path, ProgressCallback progressCb)
QList< QGCTile > getTileDownloadList(quint64 setID, int count)
bool renameTileSet(quint64 setID, const QString &newName)
static constexpr int kSchemaVersion
QSqlDatabase database() const
QList< TileSetRecord > getTileSets()
bool saveTile(const QString &hash, const QString &format, const QByteArray &img, const QString &type, quint64 tileSet)
DatabaseResult importSetsMerge(const QString &path, ProgressCallback progressCb)
QGCTileCacheDatabase(const QString &databasePath)
std::optional< quint64 > findTile(const QString &hash)
static QString getTileHash(QStringView type, int x, int y, int z)
static QGCTileSet getTileCount(int zoom, double topleftLon, double topleftLat, double bottomRightLon, double bottomRightLat, QStringView mapType)
static QString getProviderTypeFromQtMapId(int qtMapId)
static quint32 averageSizeForType(QStringView type)
static int getQtMapIdFromProviderType(QStringView type)
QByteArray format(const QList< LogEntry > &entries, int fmt)
bool setUserVersion(QSqlDatabase &db, int v)
void applySqlitePragmas(QSqlDatabase &db)
QString placeholders(int n)
std::optional< int > userVersion(QSqlDatabase &db)
int type
Definition QGCTile.h:21
int x
Definition QGCTile.h:16
QString hash
Definition QGCTile.h:20
int z
Definition QGCTile.h:18
int y
Definition QGCTile.h:17
@ StateComplete
Definition QGCTile.h:13
@ StatePending
Definition QGCTile.h:10
@ StateDownloading
Definition QGCTile.h:11