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