Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- Support for setting individual MAVLink message rates in the Inspector.
- Enabled MAVLink 2 signing.
- **Battery Display**: Dynamic bars with configurable thresholds (100%, Config 1, Config 2, Low, Critical).
- **Offline Maps**: Added pause/resume/retry controls, live download metrics, a Default Cache toggle, and documented cache schema version 2 (existing caches upgrade automatically).

---

Expand Down
1 change: 1 addition & 0 deletions docs/en/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
- [MAVLink Logs](qgc-dev-guide/file_formats/mavlink.md)
- [Developer Tools](qgc-dev-guide/tools/index.md)
- [Mock Link](qgc-dev-guide/tools/mock_link.md)
- [Offline Map Cache](qgc-dev-guide/tools/map_cache.md)
- [Command Line Options](qgc-dev-guide/command_line_options.md)
- [Custom Builds](qgc-dev-guide/custom_build/custom_build.md)
- [Initial Repository Setup For Custom Build](qgc-dev-guide/custom_build/fork_repo.md)
Expand Down
22 changes: 22 additions & 0 deletions docs/en/qgc-dev-guide/tools/map_cache.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Offline map cache internals

QGroundControl stores downloaded map tiles in an SQLite database located under the platform cache directory (for example `~/.cache/QGroundControl/QGCMapCache` on Linux). The file is named `qgcMapCache.db` and is created automatically the first time the QtLocation plugin runs.

## Schema version

The cache schema is versioned through `PRAGMA user_version`. QGroundControl v4.4 introduced schema version **2** (`kCurrentSchemaVersion` in `QGCTileCacheWorker`). On startup we verify the on-disk version and automatically rebuild the cache if it is missing or outdated. If you change the schema, update `kCurrentSchemaVersion`, describe the migration path in this document, and add a release note so that integrators know the cache will be rebuilt.

## Migration behaviour

* If the schema is missing or corrupted, `QGCTileCacheWorker::_verifyDatabaseVersion()` deletes the database and creates a fresh one.
* When importing a cache file, `_checkDatabaseVersion()` ensures the version matches `kCurrentSchemaVersion` and surfaces a user-facing error if it does not.
* Default tile sets are recreated on demand and always use map ID `UrlFactory::defaultSetMapId()`.

## Disabling caching

Two separate switches control caching:

* `setCachingPaused(true/false)` is used internally to stop saving tiles while maintenance tasks such as deletes and resets are in progress. Downloads are paused and resumed automatically.
* `MapsSettings.disableDefaultCache` exposes a user-facing “Default Cache” toggle. When disabled, tiles fetched during normal browsing are not persisted, but manually created offline sets continue to work.

When adding new features that manipulate the cache, prefer to go through `QGCMapEngineManager` so that pause/resume, download bookkeeping, and schema checks remain in sync.
8 changes: 8 additions & 0 deletions docs/en/qgc-user-guide/settings_view/offline_maps.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,11 @@ To create a new offline map set, click "Add new set". Which will take you to thi
From here you can name your set as well as specify the zoom levels you want to cache. Move the map to the position you want to cache and then set the zoom levels and click Download to cache the tiles.

To the left you can see previews of the min and max zoom levels you have chosen.

## Managing downloads

Each offline tile set shows live download statistics (pending, active, and error tiles) so you can see whether work is still in progress. You can pause an in-flight download, resume it later, or retry only the tiles that previously failed. Pausing keeps your place in the queue, which is especially useful when you need to temporarily disable connectivity or suspend caching from the main Map Settings page.

## Default cache toggle

The **Default Cache** switch near the top of the Offline Maps page controls whether QGroundControl stores tiles that are fetched during normal map browsing. Leave it enabled if you rely on the automatic cache for day-to-day flying, or disable it to save disk space and rely exclusively on the offline tile sets you create manually. The toggle simply affects the default/system cache—the user-defined offline sets continue to work normally.
78 changes: 54 additions & 24 deletions src/QmlControls/OfflineMapEditor.qml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import QtQuick.Controls
import QtLocation
import QtPositioning

Expand All @@ -35,8 +34,9 @@ FlightMap {

property var _settingsManager: QGroundControl.settingsManager
property var _settings: _settingsManager ? _settingsManager.offlineMapsSettings : null
property var _mapsSettings: _settingsManager ? _settingsManager.mapsSettings : null
property var _fmSettings: _settingsManager ? _settingsManager.flightMapSettings : null
property var _appSettings: _settingsManager.appSettings
property var _appSettings: _settingsManager ? _settingsManager.appSettings : null
property Fact _tiandituFact: _settingsManager ? _settingsManager.appSettings.tiandituToken : null
property Fact _mapboxFact: _settingsManager ? _settingsManager.appSettings.mapboxToken : null
property Fact _mapboxAccountFact: _settingsManager ? _settingsManager.appSettings.mapboxAccount : null
Expand Down Expand Up @@ -72,18 +72,26 @@ FlightMap {
QGCPalette { id: qgcPal }

Component.onCompleted: {
QGroundControl.mapEngineManager.loadTileSets()
if (QGroundControl.mapEngineManager.tileSets.count === 0) {
QGroundControl.mapEngineManager.loadTileSets()
}
resetMapToDefaults()
updateMap()
savedCenter = _map.toCoordinate(Qt.point(_map.width / 2, _map.height / 2), false /* clipToViewPort */)
settingsPage.enabled = false // Prevent mouse events from bleeding through to the settings page which is below this in hierarchy
}

Component.onDestruction: settingsPage.enabled = true
Component.onDestruction: {
settingsPage.enabled = true
}

Connections {
target: QGroundControl.mapEngineManager
onErrorMessageChanged: errorDialogComponent.createObject(mainWindow).open()
function onErrorMessageChanged() {
var dialog = errorDialogComponent.createObject(mainWindow)
dialog.closed.connect(function() { dialog.destroy() })
dialog.open()
}
}

function handleChanges() {
Expand Down Expand Up @@ -212,12 +220,7 @@ FlightMap {
color: Qt.rgba(qgcPal.window.r, qgcPal.window.g, qgcPal.window.b, 0.85)
radius: ScreenTools.defaultFontPixelWidth * 0.5

property bool _extraButton: {
if(!tileSet)
return false;
var curSel = tileSet;
return !_defaultSet && ((!curSel.complete && !curSel.downloading) || (!curSel.complete && curSel.downloading));
}
property bool _extraButton: tileSet && !_defaultSet && !tileSet.complete

property real _labelWidth: ScreenTools.defaultFontPixelWidth * 10
property real _valueWidth: ScreenTools.defaultFontPixelWidth * 14
Expand Down Expand Up @@ -309,36 +312,61 @@ FlightMap {
QGCLabel { text: qsTr("Tile Count:"); width: infoView._labelWidth; }
QGCLabel { text: tileSet ? tileSet.savedTileCountStr : ""; horizontalAlignment: Text.AlignRight; width: infoView._valueWidth; }
}
Row {
spacing: ScreenTools.defaultFontPixelWidth
anchors.horizontalCenter: parent.horizontalCenter
visible: tileSet && !_defaultSet
QGCLabel { text: qsTr("Queue:"); width: infoView._labelWidth; }
QGCLabel {
width: ScreenTools.defaultFontPixelWidth * 24
horizontalAlignment: Text.AlignRight
wrapMode: Text.NoWrap
elide: Text.ElideNone
text: tileSet ? qsTr("%1 pending / %2 active / %3 error")
.arg(tileSet.pendingTiles)
.arg(tileSet.downloadingTiles)
.arg(tileSet.errorTiles) : ""
}
}
Row {
spacing: ScreenTools.defaultFontPixelWidth
anchors.horizontalCenter: parent.horizontalCenter
QGCButton {
text: qsTr("Resume Download")
visible: tileSet && tileSet && !_defaultSet && (!tileSet.complete && !tileSet.downloading)
width: ScreenTools.defaultFontPixelWidth * 16
visible: tileSet && !_defaultSet && (!tileSet.complete && !tileSet.downloading)
width: ScreenTools.defaultFontPixelWidth * 18
onClicked: {
if(tileSet)
tileSet.resumeDownloadTask()
}
}
QGCButton {
text: qsTr("Cancel Download")
visible: tileSet && tileSet && !_defaultSet && (!tileSet.complete && tileSet.downloading)
width: ScreenTools.defaultFontPixelWidth * 16
text: qsTr("Pause Download")
visible: tileSet && !_defaultSet && (!tileSet.complete && tileSet.downloading)
width: ScreenTools.defaultFontPixelWidth * 18
onClicked: {
if(tileSet)
tileSet.pauseDownloadTask()
}
}
QGCButton {
text: qsTr("Retry Failed (%1)").arg(tileSet ? tileSet.errorTiles : 0)
visible: tileSet && !_defaultSet && (tileSet.errorTiles > 0)
width: ScreenTools.defaultFontPixelWidth * 18
onClicked: {
if(tileSet)
tileSet.cancelDownloadTask()
tileSet.retryFailedTiles()
}
}
QGCButton {
text: qsTr("Delete")
width: ScreenTools.defaultFontPixelWidth * (infoView._extraButton ? 6 : 10)
width: ScreenTools.defaultFontPixelWidth * (infoView._extraButton ? 8 : 10)
onClicked: deleteConfirmationDialogComponent.createObject(mainWindow).open()
enabled: tileSet ? (tileSet.savedTileSize > 0) : false
enabled: tileSet ? (!tileSet.deleting && (_defaultSet ? tileSet.savedTileSize > 0 : true)) : false
}
QGCButton {
text: qsTr("Ok")
width: ScreenTools.defaultFontPixelWidth * (infoView._extraButton ? 6 : 10)
width: ScreenTools.defaultFontPixelWidth * (infoView._extraButton ? 8 : 10)
visible: !_defaultSet
enabled: editSetName.text !== ""
onClicked: {
Expand All @@ -350,7 +378,7 @@ FlightMap {
}
QGCButton {
text: _defaultSet ? qsTr("Close") : qsTr("Cancel")
width: ScreenTools.defaultFontPixelWidth * (infoView._extraButton ? 6 : 10)
width: ScreenTools.defaultFontPixelWidth * (infoView._extraButton ? 8 : 10)
onClicked: _map.destroy()
}
}
Expand Down Expand Up @@ -691,10 +719,13 @@ FlightMap {
} // Rectangle - Zoom info

QGCLabel {
text: qsTr("Too many tiles")
text: qsTr("Too many tiles: %1 exceeds limit of %2").arg(QGroundControl.mapEngineManager.tileCountStr).arg(_settings ? _settings.maxTilesForDownload.valueString : "")
visible: _tooManyTiles
color: qgcPal.warningText
anchors.horizontalCenter: parent.horizontalCenter
wrapMode: Text.WordWrap
anchors.left: parent.left
anchors.right: parent.right
horizontalAlignment: Text.AlignHCenter
}

Row {
Expand Down Expand Up @@ -765,4 +796,3 @@ FlightMap {
}
}
}

35 changes: 30 additions & 5 deletions src/QmlControls/OfflineMapInfo.qml
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,49 @@ RowLayout {

signal clicked

property int _tileCount: tileSet.totalTileCount
property int _tileCount: tileSet ? tileSet.totalTileCount : 0

QGCPalette { id: qgcPal }

QGCLabel {
Layout.fillWidth: true
text: tileSet.name
text: tileSet ? (tileSet.defaultSet ? qsTr("%1 (System Cache)").arg(tileSet.name) : tileSet.name) : ""
font.italic: tileSet && tileSet.defaultSet
}

QGCLabel {
id: sizeLabel
text: tileSet.downloadStatus + (_tileCount > 0 ? " (" + _tileCount + " tiles)" : "")
text: _computeDisplayText()
function _computeDisplayText() {
if (!tileSet) return ""
if (tileSet.defaultSet) {
return tileSet.savedTileSizeStr + " (" + tileSet.savedTileCount + " tiles)"
}
var result = tileSet.downloadStatus
if (_tileCount > 0) result += " (" + _tileCount + " tiles)"
return result + _queueSuffix()
}
function _queueSuffix() {
if (!tileSet || tileSet.defaultSet) {
return ""
}
var parts = []
if (tileSet.pendingTiles > 0)
parts.push(qsTr("%1 pending").arg(tileSet.pendingTiles))
if (tileSet.downloadingTiles > 0)
parts.push(qsTr("%1 active").arg(tileSet.downloadingTiles))
if (tileSet.errorTiles > 0)
parts.push(qsTr("%1 error").arg(tileSet.errorTiles))
return parts.length ? " [" + parts.join(", ") + "]" : ""
}
}

Rectangle {
width: sizeLabel.height * 0.5
height: sizeLabel.height * 0.5
radius: width / 2
color: tileSet.complete ? "#31f55b" : "#fc5656"
opacity: sizeLabel.text.length > 0 ? 1 : 0
color: tileSet && tileSet.defaultSet ? qgcPal.text : (tileSet && tileSet.complete ? qgcPal.colorGreen : qgcPal.colorRed)
opacity: sizeLabel.text.length > 0 ? (tileSet && tileSet.defaultSet ? 0.4 : 1) : 0
}

QGCButton {
Expand Down
13 changes: 8 additions & 5 deletions src/QtLocationPlugin/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ qt_add_plugin(QGCLocation
# ----------------------------------------------------------------------------
# Platform-Specific Configuration
# ----------------------------------------------------------------------------
# Google Maps not available on iOS
# iOS: Disable Google Maps due to App Store licensing restrictions
# Google Maps requires additional licensing agreements for iOS distribution
# through the App Store that differ from other platforms
if(IOS)
target_compile_definitions(QGCLocation PRIVATE QGC_NO_GOOGLE_MAPS)
endif()
Expand All @@ -64,18 +66,19 @@ endif()
# ----------------------------------------------------------------------------
target_link_libraries(QGCLocation
PRIVATE
Qt6::Positioning
Qt6::Sql
PUBLIC
Qt6::Core
Qt6::Location
Qt6::LocationPrivate
Qt6::Network
Qt6::Positioning
Qt6::Sql
)

target_include_directories(QGCLocation
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/Providers>
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/..
${CMAKE_CURRENT_SOURCE_DIR}/../FactSystem
${CMAKE_CURRENT_SOURCE_DIR}/../QmlControls
Expand Down
Loading
Loading