Detailed QtLocation: Implementing offline caching for custom LBS plug-ins

Write before

Static plug-in parameters

As mentioned earlier, we only have one on the QML side Map Objects, a wide variety of map sources are achieved by configuring the LBS plug-in; plug-in parameters can also be configured while configuring the plug-in PluginParameter For plug-in parameters that can be configured by the official LBS plug-in, see Plugin References and Parameters .As official Open Street Map Plugin For example:

Plugin {
    name: "osm"
    PluginParameter { name: "osm.useragent"; value: "My great Qt OSM application" }
    PluginParameter { name: "osm.mapping.host"; value: "http://osm.tile.server.address/" }
    PluginParameter { name: "osm.mapping.copyright"; value: "All mine" }
    PluginParameter { name: "osm.routing.host"; value: "http://osrm.server.address/viaroute" }
    PluginParameter { name: "osm.geocoding.host"; value: "http://geocoding.server.address" }
}

In addition to configuring some proxy parameters for the LBS plug-in, the most common is the dynamic cache directory osm.mapping.cache.directory And offline cache directories osm.mapping.offline.directory Configuration, offline cache directory parameter configuration is currently only in Open Street Map Plugin Yes, you see.

Once the plug-in parameters are configured in the QML document, the LBS factory packages them into a parameter of type QVariantMap, which is used in the back end to construct the QGeoTiledMappingManagerEngine and its derived types.So, naturally, when we implement our own LBS plug-ins, we can also define plug-in parameters configured from the front-end QML document, and then parse out the key-value pairs of these parameters in constructors that inherit the derived types implemented by the QGeoTiledMappingManagerEngine.For example, in the constructor of QGeoTiledMappingManagerEngineOsm, we can see the following code to parse and configure the cache directory:

/* TILE CACHE */
if (parameters.contains(QStringLiteral("osm.mapping.cache.directory"))) 
{
    m_cacheDirectory = parameters.value(QStringLiteral("osm.mapping.cache.directory")).toString();
} 
else 
{
    // managerName() is not yet set, we have to hardcode the plugin name below
    m_cacheDirectory = QAbstractGeoTileCache::baseLocationCacheDirectory() + QLatin1String(pluginName);
}
if (parameters.contains(QStringLiteral("osm.mapping.offline.directory")))
    m_offlineDirectory = parameters.value(QStringLiteral("osm.mapping.offline.directory")).toString();

QGeoFileTileCacheOsm *tileCache = new QGeoFileTileCacheOsm(m_providers, m_offlineDirectory, m_cacheDirectory);

Dynamic map parameters

Dynamic map parameters, as technical preview in Qt 5.9, by Qt Location 5.11 MapParameter Already renamed DynamicParameter Yes. MapParameter Is through Map JS interface method of object addMapParameter The set of map parameters that you add can be added through the Map Object's mapParameters Attribute access, for example:

Map {
    id: map
    plugin: "xxx"
    MapParameter {
        id: mapParameter
        type: "xxx"
    }
    onMapReadyChanged: map.addMapParameter(mapParameter) 
}

Using the dynamic map parameter characteristics, we can extend some front-end and back-end interactive functions on our own LBS plug-ins, such as offline caching of map tiles.

Implement map offline caching

When using maps, we often have some offline scenarios, such as when the ground station software needs an offline map for unmanned aerial vehicles in the field, which requires offline cache management of map tile data.

1. Default configuration using static plug-in parameters

When instantiating a map plug-in, we can allow the front end to configure the map's offline cache directory with the'[xxx].mapping.offline.directory'parameter, which should be parsed in the derived class constructor of the back end QGeoTiledMappingManagerEngine:

// Parse cache offline directory from parameter.
if(parameters.contains(QStringLiteral("[xxx].mapping.offline.directory")))
{
    m_offlineDirectory = parameters.value(
                         QStringLiteral("[xxx].mapping.offline.directory")).toString();
}
else // Set default offline directory
{
    QString oldLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
    QString newLocation = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
    m_offlineDirectory =  m_cacheDirectory.replace(oldLocation, newLocation);
}

2. Using dynamic map parameters for front-end and back-end interaction

We need to specify a geographic range (for example, by holding down the left mouse button to select a BoundingBox in the map box) and then specify the zoom level of the tile map (for example, by popping up a form to let the user fill out ZoomRange levels 1-10), then we need to click the OK button to perform the offline caching Task, and how the trigger information for these parameters and actions will be passed toWhat about the back end?This requires the use of addMapParameter Help us.

QML objects support property extensions (QObject s actually support property extensions - -||), so we can add one MapParameter Expand specific examples, such as:

MapParameter {
    id: offlineCache
    type: "[xxx].mapping.offline.settings"
    property string directory: "[your offline cache directory]"
    property var boundingBox: QtPositioning.rectangle(
                                  QtPositioning.coordinate("[top left corner]"),
                                  QtPositioning.coordinate("[bottom right corner]"))
    property int minZoom: 0
    property int maxZoom: 10
}

When we inherit QGeoTiledMapPrivate on the back end to implement the QGeoTiledMap[XXX]Private type, we can addMapParameter Method parses the parameter and implements offline caching:

void addParameter(QGeoMapParameter* param) override
{
    Q_Q(QGeoTiledMap[XXX]);

    static const QStringList acceptedParameterTypes = QStringList()
            << QStringLiteral("[xxx].mapping.offline.settings");

    QVariantMap kvm = param->toVariantMap();
    switch (acceptedParameterTypes.indexOf(param->type())) {
    default:
    {
        qWarning() << "[XXX]Map: Invalid value for property 'type' " + param->type();
        break;
    }
    case 0:
    {
        QString directory;
        QVector<int> zoomRange;
        QGeoRectangle boundingBox;
        
        // Parse the "[xxx].mapping.offline.settings" parameters.
        if(kvm.contains("directory")) 
            directory = kvm["directory"].toString();
        else 
            qWarning() << "[XXX]Map: Can not catch the offline cache directory.";

        if(kvm.contains("boundingBox")) 
            boundingBox = kvm["boundingBox"].value<QGeoRectangle>();
        else 
            qWarning() << "[XXX]Map: Can not catch the bounding box.";

        if(kvm.contains("minZoom") && kvm.contains("maxZoom")) 
        {
            bool minZoomOk = false, maxZoomOk = false;
            int minZoom = qMax(m_minZoomLevel, kvm["minZoom"].toInt(&minZoomOk));
            int maxZoom = qMin(m_maxZoomLevel, kvm["maxZoom"].toInt(&maxZoomOk));
            
            if(minZoomOk && maxZoomOk) 
            {
                for(int zIt = minZoom; zIt <= maxZoom; zIt++)
                    zoomRange.append(zIt);
            }
            else 
                qWarning() << "[XXX]Map: Can not catch the zooms when fetch offline cache.";

            // To call the fetch offline cache function.
            q->fetchOfflineCache(boundingBox, zoomRange, directory);
        }
        break; 
    }
}

When the current end dynamically adds this map parameter to the map, it triggers the above method and executes our specific implementation of fetchOfflineCache method; of course, we can also implement cancelOfflineCache, loadOfflineCache, saveOfflineCache and other methods in the removeParameter method, all of which can be done through the front end addMapParameter Implement dynamic invocation.

3. Implement offline caching by mimicking tile file caching mechanism

The specific implementation ideas of fetchOfflineCache are: (1) traverse each Zoom level that needs to be offline cached, and use QGeoCameraTiles to calculate the required tile parameter queue; (2) check the tile parameters that already exist in the file cache, make up the queue that needs to be copied from the file cache, and make up the queue that needs to be downloaded from the network; and (3) traverse the queue that needs to be downloaded from the network.Queues copied from the file cache, copy tiles to the offline cache directory; (4) Start a timer to request the download of tiles in the queue in turn, and store them in the offline cache directory after successful download.

// fetchOfflineCache function : -------------------------------------------------------------------

// Get camera data
QGeoCameraData cameraData = d->m_visibleTiles->cameraData();
QGeoCoordinate center = boundingBox.center();
cameraData.setCenter(center);

// Create camera tiles factory
QGeoCameraTiles cameraTiles;
cameraTiles.setMapType(d->m_visibleTiles->activeMapType()); // 1
cameraTiles.setTileSize(d->m_visibleTiles->tileSize());     // 2
cameraTiles.setMapVersion(d->m_tileEngine->tileVersion());  // 3
cameraTiles.setViewExpansion(1.0);                          // 4

QString pluginString(d->m_engine->managerName()
                     + QLatin1Char('_')
                     + QString::number(d->m_engine->managerVersion()));
cameraTiles.setPluginString(pluginString);                  // 5

// Prepare queues to fetch tiles
QList<QGeoTileSpec> createCopyQueue;
for(int i = 0; i < zoomRange.length(); i++)
{
    int currentIntZoom = zoomRange.at(i);
    cameraData.setZoomLevel(currentIntZoom);

    QGeoProjectionWebMercator projection;
    projection.setCameraData(cameraData, true);
    QPointF topLeft = projection.coordinateToItemPosition(boundingBox.topLeft(), false).toPointF();
    QPointF bottomRight = projection.coordinateToItemPosition(boundingBox.bottomRight(), false).toPointF();
    QRectF selectRect = QRectF(topLeft, bottomRight);
    QSize selectSize = selectRect.size().toSize();
    if(selectSize.isNull()) selectSize = QSize(1, 1); // At least one pixel!

    cameraTiles.setScreenSize(selectSize);                   // 6
    cameraTiles.setCameraData(cameraData);                   // 7

    // Create all tiles
    QSet<QGeoTileSpec> tiles = cameraTiles.createTiles();
        
    // Check tiles in cache directory
    for(const QGeoTileSpec& tile : qAsConst(tiles))
    {
        if(!d->m_tileCache->offlineStorageContains(tile)) // isn't in offline cache
        {
            if(d->m_tileCache->diskCacheContains(tile))
                createCopyQueue << tile; // copy queue from disk cache to offline cache
            else
                d->m_downloadQueue << tile; // add to download queue
        }
    }
}

d->m_progressTotal = d->m_downloadQueue.count() + createCopyQueue.count();

if(d->m_downloadQueue.count() == 0 && createCopyQueue.count() == 0)
{
    int percentage = 100;
    QString message = QStringLiteral("All tiles needed to fetch have been in offline storage.");
    emit fetchTilesProgressChanged(percentage, message);
    return;
}

// Copy tiles from cache directory to offline directory
for(const QGeoTileSpec& tile : qAsConst(createCopyQueue))
{
    int percentage = 100 * (++d->m_progressValue) / d->m_progressTotal;
    const QString tileName = tileSpecToName(tile);

    QString message;
    if(d->m_tileCache->addToOfflineStorage(tile))
        message = QString("Fetched the %1 from disk cache successfully.").arg(tileName);
    else
        message = QString("Fetched the %1 from disk cache unsuccessfully.").arg(tileName);

    emit fetchTilesProgressChanged(percentage, message);
}

// Start a timer to request tiles online
if(!d->m_downloadQueue.isEmpty() && !d->m_timer.isActive())
    d->m_timer.start(0, this);

// timerEvent function: ---------------------------------------------------------------------------

Q_D(QGeoTiledMap[XXX]);

if (event->timerId() != d->m_timer.timerId())
{
    QObject::timerEvent(event);
    return;
}

QMutexLocker ml(&d->m_queueMutex);
if (d->m_downloadQueue.isEmpty())
{
    d->m_timer.stop();
    return;
}
ml.unlock();

// request the next tile
requestNextTile();

The part where tiles are downloaded from the network is implemented with reference to QGeoTileFetcher.

Tags: Qt network Attribute

Posted on Sun, 22 Mar 2020 15:52:54 -0400 by speedyslow