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>
27 : _databasePath(databasePath)
37QSqlDatabase QGCTileCacheDatabase::_database()
const
39 return QSqlDatabase::database(_connectionName);
47bool QGCTileCacheDatabase::_ensureConnected()
const
49 if (!_connected || !_valid) {
50 qCWarning(QGCTileCacheDatabaseLog) <<
"Database not connected";
56bool QGCTileCacheDatabase::_checkSchemaVersion()
58 QSqlDatabase db = _database();
61 qCWarning(QGCTileCacheDatabaseLog) <<
"Failed to read schema version";
65 const int version = *current;
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.";
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");
88 qCWarning(QGCTileCacheDatabaseLog) <<
"Unknown schema version" << version <<
"(expected" <<
kSchemaVersion <<
"). Resetting cache.";
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");
100 if (!_databasePath.isEmpty()) {
101 qCDebug(QGCTileCacheDatabaseLog) <<
"Mapping cache directory:" << _databasePath;
103 if (!_checkSchemaVersion()) {
108 _valid = _createDB(_database());
111 (void) QFile::remove(_databasePath);
118 qCCritical(QGCTileCacheDatabaseLog) <<
"Could not find suitable cache directory.";
131 QSqlDatabase db = QSqlDatabase::addDatabase(
"QSQLITE", _connectionName);
132 db.setDatabaseName(_databasePath);
138 qCCritical(QGCTileCacheDatabaseLog) <<
"Map Cache SQL error (open db):" << db.lastError();
139 QSqlDatabase::removeDatabase(_connectionName);
151 if (!QCoreApplication::instance()) {
156 QSqlDatabase db = QSqlDatabase::database(_connectionName,
false);
161 QSqlDatabase::removeDatabase(_connectionName);
166 if (!_ensureConnected()) {
172 qCWarning(QGCTileCacheDatabaseLog) <<
"Failed to start transaction for saveTile";
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();
181 query.addBindValue(hash);
182 query.addBindValue(format);
183 query.addBindValue(img);
184 query.addBindValue(img.size());
186 query.addBindValue(QDateTime::currentSecsSinceEpoch());
188 qCWarning(QGCTileCacheDatabaseLog) <<
"Map Cache SQL error (saveTile INSERT):" << query.lastError().text();
192 if (!query.prepare(
"SELECT tileID FROM Tiles WHERE hash = ?")) {
193 qCWarning(QGCTileCacheDatabaseLog) <<
"Map Cache SQL error (prepare tile lookup):" << query.lastError().text();
196 query.addBindValue(hash);
197 if (!query.exec() || !query.next()) {
198 qCWarning(QGCTileCacheDatabaseLog) <<
"Map Cache SQL error (tile lookup):" << query.lastError().text();
201 const quint64 tileID = query.value(0).toULongLong();
203 const quint64 setID = (tileSet ==
kInvalidTileSet) ? _getDefaultTileSet() : tileSet;
205 qCWarning(QGCTileCacheDatabaseLog) <<
"Cannot save tile: no valid tile set";
208 if (!query.prepare(
"INSERT OR IGNORE INTO SetTiles(tileID, setID) VALUES(?, ?)")) {
209 qCWarning(QGCTileCacheDatabaseLog) <<
"Map Cache SQL error (prepare SetTiles):" << query.lastError().text();
212 query.addBindValue(tileID);
213 query.addBindValue(setID);
215 qCWarning(QGCTileCacheDatabaseLog) <<
"Map Cache SQL error (add tile into SetTiles):" << query.lastError().text();
220 qCWarning(QGCTileCacheDatabaseLog) <<
"Failed to commit saveTile transaction";
224 qCDebug(QGCTileCacheDatabaseLog) <<
"HASH:" << hash;
230 if (!_ensureConnected()) {
234 QSqlQuery query(_database());
235 if (!query.prepare(
"SELECT tile, format, type FROM Tiles WHERE hash = ?")) {
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();
243 qCDebug(QGCTileCacheDatabaseLog) <<
"(Found in DB) HASH:" << hash;
244 return std::make_unique<QGCCacheTile>(hash, tileData, format, type);
247 qCDebug(QGCTileCacheDatabaseLog) <<
"(NOT in DB) HASH:" << hash;
253 if (!_ensureConnected()) {
257 QSqlQuery query(_database());
258 if (!query.prepare(
"SELECT tileID FROM Tiles WHERE hash = ?")) {
261 query.addBindValue(hash);
262 if (query.exec() && query.next()) {
263 return query.value(0).toULongLong();
271 QList<TileSetRecord> records;
272 if (!_ensureConnected()) {
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")) {
284 while (query.next()) {
286 rec.
setID = query.value(0).toULongLong();
287 rec.
name = query.value(1).toString();
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();
306 double topleftLat,
double topleftLon,
307 double bottomRightLat,
double bottomRightLon,
308 int minZoom,
int maxZoom,
const QString &type, quint32 numTiles)
310 if (!_ensureConnected()) {
316 qCWarning(QGCTileCacheDatabaseLog) <<
"Failed to start transaction for createTileSet";
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();
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);
336 query.addBindValue(numTiles);
337 query.addBindValue(QDateTime::currentSecsSinceEpoch());
339 qCWarning(QGCTileCacheDatabaseLog) <<
"Map Cache SQL error (add tileSet into TileSets):" << query.lastError().text();
343 const quint64 setID = query.lastInsertId().toULongLong();
346 constexpr int kHashBatchSize = 500;
349 struct TileCoord {
int x, y; QString hash; };
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);
360 while (lookup.next()) {
361 existingTiles.insert(lookup.value(0).toString(), lookup.value(1).toULongLong());
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(?, ?)")) {
372 query.addBindValue(it.value());
373 query.addBindValue(setID);
375 qCWarning(QGCTileCacheDatabaseLog) <<
"Map Cache SQL error (add tile into SetTiles):" << query.lastError().text();
379 if (!query.prepare(
"INSERT OR IGNORE INTO TilesDownload(setID, hash, type, x, y, z, state) VALUES(?, ?, ?, ?, ?, ?, ?)")) {
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);
390 qCWarning(QGCTileCacheDatabaseLog) <<
"Map Cache SQL error (add tile into TilesDownload):" << query.lastError().text();
398 for (
int z = minZoom; z <= maxZoom; z++) {
401 QList<TileCoord> batch;
402 batch.reserve(kHashBatchSize);
408 if (batch.size() >= kHashBatchSize) {
409 if (!processBatch(batch, z))
return std::nullopt;
415 if (!batch.isEmpty()) {
416 if (!processBatch(batch, z))
return std::nullopt;
421 qCWarning(QGCTileCacheDatabaseLog) <<
"Failed to commit createTileSet transaction";
430 if (!_ensureConnected()) {
436 qCWarning(QGCTileCacheDatabaseLog) <<
"Failed to start transaction for deleteTileSet";
440 QSqlQuery query(_database());
443 if (!query.prepare(
"DELETE FROM TilesDownload WHERE setID = ?")) {
444 qCWarning(QGCTileCacheDatabaseLog) <<
"Failed to prepare download delete:" << query.lastError().text();
447 query.addBindValue(
id);
454 QList<quint64> uniqueTileIDs;
455 if (query.prepare(QStringLiteral(
"SELECT tileID FROM SetTiles WHERE tileID IN (%1)").arg(kUniqueTilesSubquery))) {
456 query.addBindValue(
id);
458 while (query.next()) {
459 uniqueTileIDs.append(query.value(0).toULongLong());
465 if (!query.prepare(
"DELETE FROM SetTiles WHERE setID = ?")) {
466 qCWarning(QGCTileCacheDatabaseLog) <<
"Failed to prepare SetTiles delete:" << query.lastError().text();
469 query.addBindValue(
id);
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);
481 qCWarning(QGCTileCacheDatabaseLog) <<
"Failed to delete unique tiles:" << query.lastError().text();
488 if (!query.prepare(
"DELETE FROM TileSets WHERE setID = ?")) {
489 qCWarning(QGCTileCacheDatabaseLog) <<
"Failed to prepare TileSets delete:" << query.lastError().text();
492 query.addBindValue(
id);
497 if (
id == _defaultSet) {
506 if (!_ensureConnected()) {
510 QSqlQuery query(_database());
511 if (!query.prepare(
"UPDATE TileSets SET name = ? WHERE setID = ?")) {
514 query.addBindValue(newName);
515 query.addBindValue(setID);
521 if (!_ensureConnected()) {
525 QSqlQuery query(_database());
526 if (!query.prepare(
"SELECT setID FROM TileSets WHERE name = ?")) {
529 query.addBindValue(name);
530 if (query.exec() && query.next()) {
531 return query.value(0).toULongLong();
539 if (!_ensureConnected()) {
547 qCWarning(QGCTileCacheDatabaseLog) <<
"Failed to start transaction for resetDatabase";
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();
559 qCWarning(QGCTileCacheDatabaseLog) <<
"Failed to commit table drops in resetDatabase";
562 _valid = _createDB(_database());
568 QList<QGCTile> tiles;
569 if (!_ensureConnected()) {
575 qCWarning(QGCTileCacheDatabaseLog) <<
"Failed to start transaction for getTileDownloadList";
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();
584 query.addBindValue(setID);
586 query.addBindValue(count);
591 while (query.next()) {
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));
601 if (!tiles.isEmpty()) {
602 if (query.prepare(QStringLiteral(
"UPDATE TilesDownload SET state = ? WHERE setID = ? AND hash IN (%1)").arg(
QGCSqlHelper::placeholders(tiles.size())))) {
604 query.addBindValue(setID);
605 for (qsizetype i = 0; i < tiles.size(); i++) {
606 query.addBindValue(tiles[i].hash);
609 qCWarning(QGCTileCacheDatabaseLog) <<
"Map Cache SQL error (batch set TilesDownload state):" << query.lastError().text();
617 qCWarning(QGCTileCacheDatabaseLog) <<
"Failed to commit getTileDownloadList transaction";
626 if (!_ensureConnected()) {
630 QSqlQuery query(_database());
632 if (!query.prepare(
"DELETE FROM TilesDownload WHERE setID = ? AND hash = ?")) {
635 query.addBindValue(setID);
636 query.addBindValue(hash);
638 if (!query.prepare(
"UPDATE TilesDownload SET state = ? WHERE setID = ? AND hash = ?")) {
641 query.addBindValue(state);
642 query.addBindValue(setID);
643 query.addBindValue(hash);
647 qCWarning(QGCTileCacheDatabaseLog) <<
"Error:" << query.lastError().text();
656 if (!_ensureConnected()) {
660 QSqlQuery query(_database());
661 if (!query.prepare(
"UPDATE TilesDownload SET state = ? WHERE setID = ?")) {
664 query.addBindValue(state);
665 query.addBindValue(setID);
668 qCWarning(QGCTileCacheDatabaseLog) <<
"Error:" << query.lastError().text();
677 if (!_ensureConnected()) {
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();
689 query.addBindValue(_getDefaultTileSet());
690 query.addBindValue(kPruneBatchSize);
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();
703 if (tileIDs.isEmpty()) {
712 if (!_deleteTilesByIDs(tileIDs)) {
726 if (!_ensureConnected()) {
735 QFile file(QStringLiteral(
":/res/BingNoTileBytes.dat"));
736 if (!file.open(QFile::ReadOnly)) {
737 qCWarning(QGCTileCacheDatabaseLog) <<
"Failed to Open File" << file.fileName() <<
":" << file.errorString();
741 const QByteArray noTileBytes = file.readAll();
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";
750 query.addBindValue(noTileBytes.length());
751 query.addBindValue(noTileBytes);
753 qCWarning(QGCTileCacheDatabaseLog) <<
"query failed";
757 QList<quint64> idsToDelete;
758 while (query.next()) {
759 idsToDelete.append(query.value(0).toULongLong());
760 qCDebug(QGCTileCacheDatabaseLog) <<
"HASH:" << query.value(1).toString();
763 if (idsToDelete.isEmpty()) {
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;
783 if (allSucceeded && txn.
commit()) {
791 if (!_ensureConnected()) {
795 QSqlQuery query(_database());
797 if (query.exec(
"SELECT COUNT(size), SUM(size) FROM Tiles") && query.next()) {
799 result.
totalSize = query.value(1).toULongLong();
802 if (!query.prepare(QStringLiteral(
"SELECT COUNT(size), SUM(size) FROM Tiles WHERE tileID IN (%1)").arg(kUniqueTilesSubquery))) {
805 query.addBindValue(_getDefaultTileSet());
806 if (query.exec() && query.next()) {
828 if (!_ensureConnected()) {
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 = ?")) {
836 subquery.addBindValue(setID);
837 if (!subquery.exec() || !subquery.next()) {
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();
866 qCWarning(QGCTileCacheDatabaseLog) <<
"Failed to prepare unique tiles query:" << subquery.lastError().text();
869 if (dbUniqueCount > 0) {
884 if (QFileInfo(path).canonicalFilePath() == QFileInfo(_databasePath).canonicalFilePath()) {
885 result.
errorString =
"Import path must differ from the active database";
890 const QString backupPath = _databasePath + QStringLiteral(
".bak");
891 (void) QFile::remove(backupPath);
892 const bool hasBackup = QFile::rename(_databasePath, backupPath);
894 (void) QFile::remove(_databasePath);
896 if (!QFile::copy(path, _databasePath)) {
898 (void) QFile::rename(backupPath, _databasePath);
900 result.
errorString =
"Failed to copy import database";
905 (void) QFile::remove(backupPath);
906 if (progressCb) progressCb(25);
909 result.
errorString = QStringLiteral(
"Failed to initialize tile cache database after import");
911 if (progressCb) progressCb(50);
914 result.
errorString = QStringLiteral(
"Failed to connect to tile cache database after import");
917 if (progressCb) progressCb(100);
925 if (QFileInfo(path).canonicalFilePath() == QFileInfo(_databasePath).canonicalFilePath()) {
926 result.
errorString =
"Import path must differ from the active database";
929 if (!_ensureConnected()) {
935 QStringLiteral(
"QGeoTileImportSession"));
937 result.
errorString =
"Error opening import database";
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();
948 bool tilesImported =
false;
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();
971 result.
errorString =
"Failed to start transaction for import set";
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";
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";
1000 insertSetID = cQuery.lastInsertId().toULongLong();
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();
1025 qCWarning(QGCTileCacheDatabaseLog) <<
"Failed to commit import transaction for set:" << name;
1029 if (tilesIterated > tilesSaved) {
1030 const quint64 alreadyExisting = tilesIterated - tilesSaved;
1031 tileCount = (alreadyExisting < tileCount) ? tileCount - alreadyExisting : 0;
1034 if ((tilesSaved == 0) && (defaultSet == 0)) {
1035 qCDebug(QGCTileCacheDatabaseLog) <<
"No unique tiles in" << name <<
"Removing it.";
1044 if (!tilesImported && result.
errorString.isEmpty()) {
1045 result.
errorString =
"No unique tiles in imported database";
1054 if (!_ensureConnected()) {
1058 if (QFileInfo(path).canonicalFilePath() == QFileInfo(_databasePath).canonicalFilePath()) {
1059 result.
errorString =
"Export path must differ from the active database";
1063 (void) QFile::remove(path);
1065 QStringLiteral(
"QGeoTileExportSession"));
1067 qCCritical(QGCTileCacheDatabaseLog) <<
"Map Cache SQL error (create export database):" << exportDB.
database().lastError();
1068 result.
errorString =
"Error opening export database";
1072 if (!_createDB(exportDB.
database(),
false)) {
1073 result.
errorString =
"Error creating export database";
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();
1089 tileCount += (actualCount > 0) ? actualCount : set.numTiles;
1092 if (tileCount == 0) {
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;
1104 query.addBindValue(set.setID);
1105 if (!query.exec()) {
1106 qCWarning(QGCTileCacheDatabaseLog) <<
"Failed to query tiles for export set" << set.name;
1112 qCWarning(QGCTileCacheDatabaseLog) <<
"Failed to start transaction for export set" << set.name;
1113 result.
errorString =
"Failed to start export transaction";
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";
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";
1141 const quint64 exportSetID = exportQuery.lastInsertId().toULongLong();
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();
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();
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();
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();
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();
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);
1195 if (skippedTiles > 0) {
1196 qCWarning(QGCTileCacheDatabaseLog) <<
"Skipped" << skippedTiles <<
"tiles during export of" << set.name;
1199 qCWarning(QGCTileCacheDatabaseLog) <<
"Failed to commit export transaction for" << set.name;
1207bool QGCTileCacheDatabase::_createDB(QSqlDatabase db,
bool createDefault)
1211 QSqlQuery query(db);
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, "
1221 "date INTEGER DEFAULT 0)"))
1223 qCWarning(QGCTileCacheDatabaseLog) <<
"Map Cache SQL error (create Tiles db):" << query.lastError().text();
1228 "CREATE TABLE IF NOT EXISTS TileSets ("
1229 "setID INTEGER PRIMARY KEY NOT NULL, "
1230 "name TEXT NOT NULL UNIQUE, "
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)"))
1243 qCWarning(QGCTileCacheDatabaseLog) <<
"Map Cache SQL error (create TileSets db):" << query.lastError().text();
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)"))
1252 qCWarning(QGCTileCacheDatabaseLog) <<
"Map Cache SQL error (create SetTiles db):" << query.lastError().text();
1257 "CREATE TABLE IF NOT EXISTS TilesDownload ("
1258 "setID INTEGER NOT NULL REFERENCES TileSets(setID) ON DELETE CASCADE, "
1259 "hash TEXT NOT NULL, "
1264 "state INTEGER DEFAULT 0)"))
1266 qCWarning(QGCTileCacheDatabaseLog) <<
"Map Cache SQL error (create TilesDownload db):" << query.lastError().text();
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)",
1278 for (
const char *sql : indexStatements) {
1279 if (!query.exec(QLatin1String(sql))) {
1280 qCWarning(QGCTileCacheDatabaseLog) <<
"Failed to create index:" << sql << query.lastError().text();
1285 qCWarning(QGCTileCacheDatabaseLog) <<
"Failed to set schema version";
1288 if (!createDefault) {
1292 if (!query.prepare(
"SELECT name FROM TileSets WHERE name = ?")) {
1293 qCWarning(QGCTileCacheDatabaseLog) <<
"Map Cache SQL error (prepare default set check):" << db.lastError();
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();
1305 if (!query.prepare(
"INSERT INTO TileSets(name, defaultSet, date) VALUES(?, ?, ?)")) {
1306 qCWarning(QGCTileCacheDatabaseLog) <<
"Map Cache SQL error (prepare default tile set):" << db.lastError();
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();
1320quint64 QGCTileCacheDatabase::_getDefaultTileSet()
1326 if (!_ensureConnected()) {
1330 QSqlQuery query(_database());
1331 if (query.exec(
"SELECT setID FROM TileSets WHERE defaultSet = 1") && query.next()) {
1332 _defaultSet = query.value(0).toULongLong();
1336 qCWarning(QGCTileCacheDatabaseLog) <<
"Default tile set not found in database";
1340bool QGCTileCacheDatabase::_deleteTilesByIDs(
const QList<quint64> &ids)
1342 if (ids.isEmpty()) {
1346 QSqlQuery query(_database());
1350 for (
const quint64
id : ids) {
1351 query.addBindValue(
id);
1353 return query.exec();
1356QString QGCTileCacheDatabase::_deduplicateSetName(
const QString &name)
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);
1372 while (query.next()) {
1373 existing.insert(query.value(0).toString());
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)) {
1385 return QStringLiteral(
"%1 %2").arg(name, QUuid::createUuid().toString(QUuid::WithoutBraces).left(8));
1388quint64 QGCTileCacheDatabase::_copyTilesForSet(QSqlDatabase srcDB, quint64 srcSetID, quint64 dstSetID,
1389 quint64 ¤tCount, quint64 tileCount,
1391 quint64 *tilesIteratedOut,
bool useTransaction)
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;
1400 subQuery.addBindValue(srcSetID);
1401 if (!subQuery.exec()) {
1402 if (tilesIteratedOut) *tilesIteratedOut = 0;
1406 quint64 tilesFound = 0;
1407 quint64 tilesLinked = 0;
1409 std::unique_ptr<QGCSqlHelper::Transaction> txn;
1410 if (useTransaction) {
1411 txn = std::make_unique<QGCSqlHelper::Transaction>(_database());
1413 qCWarning(QGCTileCacheDatabaseLog) <<
"Failed to start transaction for merge import";
1414 if (tilesIteratedOut) *tilesIteratedOut = 0;
1419 QSqlQuery cQuery(_database());
1420 while (subQuery.next()) {
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();
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();
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();
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) {
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);
1468 if (txn && !txn->commit()) {
1469 qCWarning(QGCTileCacheDatabaseLog) <<
"Failed to commit merge import transaction";
1470 if (tilesIteratedOut) *tilesIteratedOut = tilesFound;
1474 if (tilesIteratedOut) *tilesIteratedOut = tilesFound;
#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)
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)
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)
bool setUserVersion(QSqlDatabase &db, int v)
void applySqlitePragmas(QSqlDatabase &db)
QString placeholders(int n)
std::optional< int > userVersion(QSqlDatabase &db)