3#include <QtCore/QCoreApplication>
4#include <QtCore/QDateTime>
6#include <QtCore/QFileInfo>
7#include <QtCore/QSettings>
9#include <QtSql/QSqlDatabase>
10#include <QtSql/QSqlError>
11#include <QtSql/QSqlQuery>
28 bool begin() { _active = _db.transaction();
return _active; }
29 bool commit() {
if (_active) { _committed = _db.commit();
return _committed; }
return false; }
32 bool _committed =
false;
39 std::unique_ptr<QSqlDatabase>
db;
43 db.reset(
new QSqlDatabase(QSqlDatabase::addDatabase(
"QSQLITE",
session)));
44 db->setDatabaseName(path);
55 result.reserve(n * 2);
56 for (
int i = 0; i < n; i++) {
57 if (i > 0) result += QChar(
',');
66 : _databasePath(databasePath)
76QSqlDatabase QGCTileCacheDatabase::_database()
const
78 return QSqlDatabase::database(_connectionName);
86bool QGCTileCacheDatabase::_ensureConnected()
const
88 if (!_connected || !_valid) {
89 qCWarning(QGCTileCacheWorkerLog) <<
"Database not connected";
95bool QGCTileCacheDatabase::_checkSchemaVersion()
97 QSqlQuery query(_database());
98 if (!query.exec(
"PRAGMA user_version") || !query.next()) {
99 qCWarning(QGCTileCacheWorkerLog) <<
"Failed to read schema version";
103 const int version = query.value(0).toInt();
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.";
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");
124 qCWarning(QGCTileCacheWorkerLog) <<
"Unknown schema version" << version <<
"(expected" <<
kSchemaVersion <<
"). Resetting cache.";
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");
136 if (!_databasePath.isEmpty()) {
137 qCDebug(QGCTileCacheWorkerLog) <<
"Mapping cache directory:" << _databasePath;
139 if (!_checkSchemaVersion()) {
144 _valid = _createDB(_database());
147 (void) QFile::remove(_databasePath);
154 qCCritical(QGCTileCacheWorkerLog) <<
"Could not find suitable cache directory.";
167 QSqlDatabase db = QSqlDatabase::addDatabase(
"QSQLITE", _connectionName);
168 db.setDatabaseName(_databasePath);
171 QSqlQuery pragma(db);
172 if (!pragma.exec(
"PRAGMA journal_mode=WAL")) {
173 qCWarning(QGCTileCacheWorkerLog) <<
"Failed to set WAL journal mode:" << pragma.lastError().text();
175 if (!pragma.exec(
"PRAGMA foreign_keys = ON")) {
176 qCWarning(QGCTileCacheWorkerLog) <<
"Failed to enable foreign keys:" << pragma.lastError().text();
180 qCCritical(QGCTileCacheWorkerLog) <<
"Map Cache SQL error (open db):" << db.lastError();
181 QSqlDatabase::removeDatabase(_connectionName);
193 if (!QCoreApplication::instance()) {
198 QSqlDatabase db = QSqlDatabase::database(_connectionName,
false);
203 QSqlDatabase::removeDatabase(_connectionName);
208 if (!_ensureConnected()) {
214 qCWarning(QGCTileCacheWorkerLog) <<
"Failed to start transaction for saveTile";
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();
223 query.addBindValue(hash);
224 query.addBindValue(format);
225 query.addBindValue(img);
226 query.addBindValue(img.size());
228 query.addBindValue(QDateTime::currentSecsSinceEpoch());
230 qCWarning(QGCTileCacheWorkerLog) <<
"Map Cache SQL error (saveTile INSERT):" << query.lastError().text();
234 if (!query.prepare(
"SELECT tileID FROM Tiles WHERE hash = ?")) {
235 qCWarning(QGCTileCacheWorkerLog) <<
"Map Cache SQL error (prepare tile lookup):" << query.lastError().text();
238 query.addBindValue(hash);
239 if (!query.exec() || !query.next()) {
240 qCWarning(QGCTileCacheWorkerLog) <<
"Map Cache SQL error (tile lookup):" << query.lastError().text();
243 const quint64 tileID = query.value(0).toULongLong();
245 const quint64 setID = (tileSet ==
kInvalidTileSet) ? _getDefaultTileSet() : tileSet;
247 qCWarning(QGCTileCacheWorkerLog) <<
"Cannot save tile: no valid tile set";
250 if (!query.prepare(
"INSERT OR IGNORE INTO SetTiles(tileID, setID) VALUES(?, ?)")) {
251 qCWarning(QGCTileCacheWorkerLog) <<
"Map Cache SQL error (prepare SetTiles):" << query.lastError().text();
254 query.addBindValue(tileID);
255 query.addBindValue(setID);
257 qCWarning(QGCTileCacheWorkerLog) <<
"Map Cache SQL error (add tile into SetTiles):" << query.lastError().text();
262 qCWarning(QGCTileCacheWorkerLog) <<
"Failed to commit saveTile transaction";
266 qCDebug(QGCTileCacheWorkerLog) <<
"HASH:" << hash;
272 if (!_ensureConnected()) {
276 QSqlQuery query(_database());
277 if (!query.prepare(
"SELECT tile, format, type FROM Tiles WHERE hash = ?")) {
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();
285 qCDebug(QGCTileCacheWorkerLog) <<
"(Found in DB) HASH:" << hash;
286 return std::make_unique<QGCCacheTile>(hash, tileData, format, type);
289 qCDebug(QGCTileCacheWorkerLog) <<
"(NOT in DB) HASH:" << hash;
295 if (!_ensureConnected()) {
299 QSqlQuery query(_database());
300 if (!query.prepare(
"SELECT tileID FROM Tiles WHERE hash = ?")) {
303 query.addBindValue(hash);
304 if (query.exec() && query.next()) {
305 return query.value(0).toULongLong();
313 QList<TileSetRecord> records;
314 if (!_ensureConnected()) {
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")) {
326 while (query.next()) {
328 rec.
setID = query.value(0).toULongLong();
329 rec.
name = query.value(1).toString();
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();
348 double topleftLat,
double topleftLon,
349 double bottomRightLat,
double bottomRightLon,
350 int minZoom,
int maxZoom,
const QString &type, quint32 numTiles)
352 if (!_ensureConnected()) {
358 qCWarning(QGCTileCacheWorkerLog) <<
"Failed to start transaction for createTileSet";
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();
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);
378 query.addBindValue(numTiles);
379 query.addBindValue(QDateTime::currentSecsSinceEpoch());
381 qCWarning(QGCTileCacheWorkerLog) <<
"Map Cache SQL error (add tileSet into TileSets):" << query.lastError().text();
385 const quint64 setID = query.lastInsertId().toULongLong();
388 constexpr int kHashBatchSize = 500;
391 struct TileCoord {
int x, y; QString hash; };
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);
402 while (lookup.next()) {
403 existingTiles.insert(lookup.value(0).toString(), lookup.value(1).toULongLong());
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(?, ?)")) {
414 query.addBindValue(it.value());
415 query.addBindValue(setID);
417 qCWarning(QGCTileCacheWorkerLog) <<
"Map Cache SQL error (add tile into SetTiles):" << query.lastError().text();
421 if (!query.prepare(
"INSERT OR IGNORE INTO TilesDownload(setID, hash, type, x, y, z, state) VALUES(?, ?, ?, ?, ?, ?, ?)")) {
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);
432 qCWarning(QGCTileCacheWorkerLog) <<
"Map Cache SQL error (add tile into TilesDownload):" << query.lastError().text();
440 for (
int z = minZoom; z <= maxZoom; z++) {
443 QList<TileCoord> batch;
444 batch.reserve(kHashBatchSize);
450 if (batch.size() >= kHashBatchSize) {
451 if (!processBatch(batch, z))
return std::nullopt;
457 if (!batch.isEmpty()) {
458 if (!processBatch(batch, z))
return std::nullopt;
463 qCWarning(QGCTileCacheWorkerLog) <<
"Failed to commit createTileSet transaction";
472 if (!_ensureConnected()) {
478 qCWarning(QGCTileCacheWorkerLog) <<
"Failed to start transaction for deleteTileSet";
482 QSqlQuery query(_database());
485 if (!query.prepare(
"DELETE FROM TilesDownload WHERE setID = ?")) {
486 qCWarning(QGCTileCacheWorkerLog) <<
"Failed to prepare download delete:" << query.lastError().text();
489 query.addBindValue(
id);
496 QList<quint64> uniqueTileIDs;
497 if (query.prepare(QStringLiteral(
"SELECT tileID FROM SetTiles WHERE tileID IN (%1)").arg(kUniqueTilesSubquery))) {
498 query.addBindValue(
id);
500 while (query.next()) {
501 uniqueTileIDs.append(query.value(0).toULongLong());
507 if (!query.prepare(
"DELETE FROM SetTiles WHERE setID = ?")) {
508 qCWarning(QGCTileCacheWorkerLog) <<
"Failed to prepare SetTiles delete:" << query.lastError().text();
511 query.addBindValue(
id);
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);
523 qCWarning(QGCTileCacheWorkerLog) <<
"Failed to delete unique tiles:" << query.lastError().text();
530 if (!query.prepare(
"DELETE FROM TileSets WHERE setID = ?")) {
531 qCWarning(QGCTileCacheWorkerLog) <<
"Failed to prepare TileSets delete:" << query.lastError().text();
534 query.addBindValue(
id);
539 if (
id == _defaultSet) {
548 if (!_ensureConnected()) {
552 QSqlQuery query(_database());
553 if (!query.prepare(
"UPDATE TileSets SET name = ? WHERE setID = ?")) {
556 query.addBindValue(newName);
557 query.addBindValue(setID);
563 if (!_ensureConnected()) {
567 QSqlQuery query(_database());
568 if (!query.prepare(
"SELECT setID FROM TileSets WHERE name = ?")) {
571 query.addBindValue(name);
572 if (query.exec() && query.next()) {
573 return query.value(0).toULongLong();
581 if (!_ensureConnected()) {
589 qCWarning(QGCTileCacheWorkerLog) <<
"Failed to start transaction for resetDatabase";
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();
601 qCWarning(QGCTileCacheWorkerLog) <<
"Failed to commit table drops in resetDatabase";
604 _valid = _createDB(_database());
610 QList<QGCTile> tiles;
611 if (!_ensureConnected()) {
617 qCWarning(QGCTileCacheWorkerLog) <<
"Failed to start transaction for getTileDownloadList";
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();
626 query.addBindValue(setID);
628 query.addBindValue(count);
633 while (query.next()) {
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));
643 if (!tiles.isEmpty()) {
644 if (query.prepare(QStringLiteral(
"UPDATE TilesDownload SET state = ? WHERE setID = ? AND hash IN (%1)").arg(
placeholders(tiles.size())))) {
646 query.addBindValue(setID);
647 for (qsizetype i = 0; i < tiles.size(); i++) {
648 query.addBindValue(tiles[i].hash);
651 qCWarning(QGCTileCacheWorkerLog) <<
"Map Cache SQL error (batch set TilesDownload state):" << query.lastError().text();
659 qCWarning(QGCTileCacheWorkerLog) <<
"Failed to commit getTileDownloadList transaction";
668 if (!_ensureConnected()) {
672 QSqlQuery query(_database());
674 if (!query.prepare(
"DELETE FROM TilesDownload WHERE setID = ? AND hash = ?")) {
677 query.addBindValue(setID);
678 query.addBindValue(hash);
680 if (!query.prepare(
"UPDATE TilesDownload SET state = ? WHERE setID = ? AND hash = ?")) {
683 query.addBindValue(state);
684 query.addBindValue(setID);
685 query.addBindValue(hash);
689 qCWarning(QGCTileCacheWorkerLog) <<
"Error:" << query.lastError().text();
698 if (!_ensureConnected()) {
702 QSqlQuery query(_database());
703 if (!query.prepare(
"UPDATE TilesDownload SET state = ? WHERE setID = ?")) {
706 query.addBindValue(state);
707 query.addBindValue(setID);
710 qCWarning(QGCTileCacheWorkerLog) <<
"Error:" << query.lastError().text();
719 if (!_ensureConnected()) {
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();
731 query.addBindValue(_getDefaultTileSet());
732 query.addBindValue(kPruneBatchSize);
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();
745 if (tileIDs.isEmpty()) {
754 if (!_deleteTilesByIDs(tileIDs)) {
768 if (!_ensureConnected()) {
777 QFile file(QStringLiteral(
":/res/BingNoTileBytes.dat"));
778 if (!file.open(QFile::ReadOnly)) {
779 qCWarning(QGCTileCacheWorkerLog) <<
"Failed to Open File" << file.fileName() <<
":" << file.errorString();
783 const QByteArray noTileBytes = file.readAll();
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";
792 query.addBindValue(noTileBytes.length());
793 query.addBindValue(noTileBytes);
795 qCWarning(QGCTileCacheWorkerLog) <<
"query failed";
799 QList<quint64> idsToDelete;
800 while (query.next()) {
801 idsToDelete.append(query.value(0).toULongLong());
802 qCDebug(QGCTileCacheWorkerLog) <<
"HASH:" << query.value(1).toString();
805 if (idsToDelete.isEmpty()) {
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;
825 if (allSucceeded && txn.
commit()) {
833 if (!_ensureConnected()) {
837 QSqlQuery query(_database());
839 if (query.exec(
"SELECT COUNT(size), SUM(size) FROM Tiles") && query.next()) {
841 result.
totalSize = query.value(1).toULongLong();
844 if (!query.prepare(QStringLiteral(
"SELECT COUNT(size), SUM(size) FROM Tiles WHERE tileID IN (%1)").arg(kUniqueTilesSubquery))) {
847 query.addBindValue(_getDefaultTileSet());
848 if (query.exec() && query.next()) {
870 if (!_ensureConnected()) {
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 = ?")) {
878 subquery.addBindValue(setID);
879 if (!subquery.exec() || !subquery.next()) {
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();
908 qCWarning(QGCTileCacheWorkerLog) <<
"Failed to prepare unique tiles query:" << subquery.lastError().text();
911 if (dbUniqueCount > 0) {
926 if (QFileInfo(path).canonicalFilePath() == QFileInfo(_databasePath).canonicalFilePath()) {
927 result.
errorString =
"Import path must differ from the active database";
932 const QString backupPath = _databasePath + QStringLiteral(
".bak");
933 (void) QFile::remove(backupPath);
934 const bool hasBackup = QFile::rename(_databasePath, backupPath);
936 (void) QFile::remove(_databasePath);
938 if (!QFile::copy(path, _databasePath)) {
940 (void) QFile::rename(backupPath, _databasePath);
942 result.
errorString =
"Failed to copy import database";
947 (void) QFile::remove(backupPath);
948 if (progressCb) progressCb(25);
951 result.
errorString = QStringLiteral(
"Failed to initialize tile cache database after import");
953 if (progressCb) progressCb(50);
956 result.
errorString = QStringLiteral(
"Failed to connect to tile cache database after import");
959 if (progressCb) progressCb(100);
967 if (QFileInfo(path).canonicalFilePath() == QFileInfo(_databasePath).canonicalFilePath()) {
968 result.
errorString =
"Import path must differ from the active database";
971 if (!_ensureConnected()) {
977 if (!importDB.
open()) {
978 result.
errorString =
"Error opening import database";
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();
989 bool tilesImported =
false;
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();
1012 result.
errorString =
"Failed to start transaction for import set";
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";
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";
1041 insertSetID = cQuery.lastInsertId().toULongLong();
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();
1066 qCWarning(QGCTileCacheWorkerLog) <<
"Failed to commit import transaction for set:" << name;
1070 if (tilesIterated > tilesSaved) {
1071 const quint64 alreadyExisting = tilesIterated - tilesSaved;
1072 tileCount = (alreadyExisting < tileCount) ? tileCount - alreadyExisting : 0;
1075 if ((tilesSaved == 0) && (defaultSet == 0)) {
1076 qCDebug(QGCTileCacheWorkerLog) <<
"No unique tiles in" << name <<
"Removing it.";
1085 if (!tilesImported && result.
errorString.isEmpty()) {
1086 result.
errorString =
"No unique tiles in imported database";
1095 if (!_ensureConnected()) {
1099 if (QFileInfo(path).canonicalFilePath() == QFileInfo(_databasePath).canonicalFilePath()) {
1100 result.
errorString =
"Export path must differ from the active database";
1104 (void) QFile::remove(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";
1112 if (!_createDB(*exportDB.
db,
false)) {
1113 result.
errorString =
"Error creating export database";
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();
1129 tileCount += (actualCount > 0) ? actualCount : set.numTiles;
1132 if (tileCount == 0) {
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;
1144 query.addBindValue(set.setID);
1145 if (!query.exec()) {
1146 qCWarning(QGCTileCacheWorkerLog) <<
"Failed to query tiles for export set" << set.name;
1152 qCWarning(QGCTileCacheWorkerLog) <<
"Failed to start transaction for export set" << set.name;
1153 result.
errorString =
"Failed to start export transaction";
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";
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";
1181 const quint64 exportSetID = exportQuery.lastInsertId().toULongLong();
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();
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();
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();
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();
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();
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);
1235 if (skippedTiles > 0) {
1236 qCWarning(QGCTileCacheWorkerLog) <<
"Skipped" << skippedTiles <<
"tiles during export of" << set.name;
1239 qCWarning(QGCTileCacheWorkerLog) <<
"Failed to commit export transaction for" << set.name;
1247bool QGCTileCacheDatabase::_createDB(QSqlDatabase db,
bool createDefault)
1249 QSqlQuery query(db);
1250 (void) query.exec(
"PRAGMA foreign_keys = ON");
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, "
1260 "date INTEGER DEFAULT 0)"))
1262 qCWarning(QGCTileCacheWorkerLog) <<
"Map Cache SQL error (create Tiles db):" << query.lastError().text();
1267 "CREATE TABLE IF NOT EXISTS TileSets ("
1268 "setID INTEGER PRIMARY KEY NOT NULL, "
1269 "name TEXT NOT NULL UNIQUE, "
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)"))
1282 qCWarning(QGCTileCacheWorkerLog) <<
"Map Cache SQL error (create TileSets db):" << query.lastError().text();
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)"))
1291 qCWarning(QGCTileCacheWorkerLog) <<
"Map Cache SQL error (create SetTiles db):" << query.lastError().text();
1296 "CREATE TABLE IF NOT EXISTS TilesDownload ("
1297 "setID INTEGER NOT NULL REFERENCES TileSets(setID) ON DELETE CASCADE, "
1298 "hash TEXT NOT NULL, "
1303 "state INTEGER DEFAULT 0)"))
1305 qCWarning(QGCTileCacheWorkerLog) <<
"Map Cache SQL error (create TilesDownload db):" << query.lastError().text();
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)",
1317 for (
const char *sql : indexStatements) {
1318 if (!query.exec(QLatin1String(sql))) {
1319 qCWarning(QGCTileCacheWorkerLog) <<
"Failed to create index:" << sql << query.lastError().text();
1323 if (!query.exec(QStringLiteral(
"PRAGMA user_version = %1").arg(
kSchemaVersion))) {
1324 qCWarning(QGCTileCacheWorkerLog) <<
"Failed to set schema version:" << query.lastError().text();
1327 if (!createDefault) {
1331 if (!query.prepare(
"SELECT name FROM TileSets WHERE name = ?")) {
1332 qCWarning(QGCTileCacheWorkerLog) <<
"Map Cache SQL error (prepare default set check):" << db.lastError();
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();
1344 if (!query.prepare(
"INSERT INTO TileSets(name, defaultSet, date) VALUES(?, ?, ?)")) {
1345 qCWarning(QGCTileCacheWorkerLog) <<
"Map Cache SQL error (prepare default tile set):" << db.lastError();
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();
1359quint64 QGCTileCacheDatabase::_getDefaultTileSet()
1365 if (!_ensureConnected()) {
1369 QSqlQuery query(_database());
1370 if (query.exec(
"SELECT setID FROM TileSets WHERE defaultSet = 1") && query.next()) {
1371 _defaultSet = query.value(0).toULongLong();
1375 qCWarning(QGCTileCacheWorkerLog) <<
"Default tile set not found in database";
1379bool QGCTileCacheDatabase::_deleteTilesByIDs(
const QList<quint64> &ids)
1381 if (ids.isEmpty()) {
1385 QSqlQuery query(_database());
1386 if (!query.prepare(QStringLiteral(
"DELETE FROM Tiles WHERE tileID IN (%1)").arg(
placeholders(ids.size())))) {
1389 for (
const quint64
id : ids) {
1390 query.addBindValue(
id);
1392 return query.exec();
1395QString QGCTileCacheDatabase::_deduplicateSetName(
const QString &name)
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);
1411 while (query.next()) {
1412 existing.insert(query.value(0).toString());
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)) {
1424 return QStringLiteral(
"%1 %2").arg(name, QUuid::createUuid().toString(QUuid::WithoutBraces).left(8));
1427quint64 QGCTileCacheDatabase::_copyTilesForSet(QSqlDatabase srcDB, quint64 srcSetID, quint64 dstSetID,
1428 quint64 ¤tCount, quint64 tileCount,
1430 quint64 *tilesIteratedOut,
bool useTransaction)
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;
1439 subQuery.addBindValue(srcSetID);
1440 if (!subQuery.exec()) {
1441 if (tilesIteratedOut) *tilesIteratedOut = 0;
1445 quint64 tilesFound = 0;
1446 quint64 tilesLinked = 0;
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;
1458 QSqlQuery cQuery(_database());
1459 while (subQuery.next()) {
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();
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();
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();
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) {
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);
1507 if (txn && !txn->commit()) {
1508 qCWarning(QGCTileCacheWorkerLog) <<
"Failed to commit merge import transaction";
1509 if (tilesIteratedOut) *tilesIteratedOut = tilesFound;
1513 if (tilesIteratedOut) *tilesIteratedOut = tilesFound;
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)
bool deleteTileSet(quint64 id)
void deleteBingNoTileTiles()
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)
TotalsResult computeTotals()
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)
ScopedExportDB(const ScopedExportDB &)=delete
ScopedExportDB & operator=(const ScopedExportDB &)=delete
ScopedExportDB(const QString &path)
std::unique_ptr< QSqlDatabase > db