详解QtLocation(六):实现自定义LBS插件的离线缓存

落花浮王杯 提交于 2020-03-23 03:32:07

3 月,跳不动了?>>>

写在前面

静态的插件参数

前面已经讲到,我们在QML端只有Map对象,各种各样的地图源是靠配置LBS插件来实现的;在配置插件的同时,还可以配置插件参数PluginParameter,官方LBS插件能够配置的插件参数参见Plugin References and Parameters。以官方的Open Street Map Plugin为例:

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" }
}

除了可以为LBS插件配置一些代理的参数以外,比较常用的就是动态缓存目录osm.mapping.cache.directory和离线缓存目录osm.mapping.offline.directory的配置,离线缓存目录的参数配置目前只看到OSM插件才有。

在QML文档中配置了插件参数之后,LBS工厂都会将其打包成一个QVariantMap类型的parameters,在后端用来构造QGeoTiledMappingManagerEngine及其派生类型。所以,自然而然的,我们实现自己的LBS插件的时候,也可以定义从前端的QML文档配置的插件参数,然后在继承QGeoTiledMappingManagerEngine实现的派生类型的构造函数中,将这些参数的键值对解析出来,进行相应的操作。例如在QGeoTiledMappingManagerEngineOsm的构造函数中,我们可以看到如下解析并配置缓存目录的代码:

/* 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);

动态的地图参数

动态的地图参数,在Qt 5.9中作为技术预览,到Qt Location 5.11的时候MapParameter已经改名为DynamicParameter了。MapParameter是通过Map对象的JS接口方法addMapParameter进行添加的,添加的地图参数集合可以通过Map对象的mapParameters属性访问到,例如:

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

利用动态的地图参数特性,我们可以在自己实现的LBS插件上,额外扩展一些前后端的交互功能,例如地图瓦片的离线缓存功能。

实现地图离线缓存

我们在使用地图的时候常常有一些离线场景,比如无人机在野外作业时地面站软件需要离线地图,这时就需要实现地图瓦片数据的离线缓存管理。

一、利用静态的插件参数进行默认的配置

我们在实例化地图插件的时候,可以允许前端通过“[xxx].mapping.offline.directory”参数,对地图的离线缓存目录进行配置,其对应在后端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);
}

二、利用动态的地图参数进行前后端交互

我们需要指定一个地理范围(例如通过按住鼠标左键在地图上框选出一个BoundingBox),然后指定瓦片地图的缩放级别(例如弹出表单让用户填写1-10级的ZoomRange),这时我们还需要点击“OK”按钮去执行离线缓存Task,那么这些参数和执行动作的触发信息要如何传递到后端呢?这就需要使用addMapParameter来帮助我们了。

QML的对象是支持属性扩展的(事实上QObject都支持属性扩展 -_-|| ),我们可以对一个MapParameter的具体实例进行扩展,例如:

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
}

我们在后端继承QGeoTiledMapPrivate实现QGeoTiledMap[XXX]Private类型时,就可在addMapParameter方法里解析该参数,并具体实现离线缓存功能了:

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; 
    }
}

当前端将该地图参数动态地加入到地图中的时候,就会触发上述方法,并执行到我们具体实现的fetchOfflineCache;当然我们还可以在removeParameter方法中具体实现cancelOfflineCache。

三、模仿瓦片文件缓存机制实现离线缓存

实现fetchOfflineCache的具体思路是:(1)遍历每一个需要离线缓存的Zoom级别,利用QGeoCameraTiles计算出所需的瓦片参数队列;(2)检查已经存在于文件缓存中的瓦片参数,组成需要从文件缓存中复制的队列,其余的瓦片参数组成需要从网络下载的队列;(3)遍历需要从文件缓存中复制的队列,将瓦片复制到离线缓存目录;(4)启动一个计时器,去依次请求下载队列里的瓦片,下载成功后存放到离线缓存目录。

// 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)) // don'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();

其中从网络下载瓦片的部分,参考QGeoTileFetcher进行实现。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!