company_banner

Создаём плагин Qt GeoServices на примере ОС Аврора, OpenStreetMap и Sight Safari

    Привет, Хабр! Хотим рассказать о том, как создать плагин Qt GeoServices и использовать его в своём приложении на ОС Аврора. В этом посте мы подробно объясним, как научить приложение определять координаты устройства на карте и прокладывать оптимальные маршруты с помощью сервиса Sight Safari. Самые нетерпеливые могут пощупать готовый код плагина и демо-приложения на GitHub, всех остальных приглашаем под кат.

    Зачем писать свой плагин

    Когда приложению на Qt требуется поддержка карт, то первое, что приходит на ум — использовать QML-компонент Map. Но к нему нужен плагин, реализующий работу с провайдером данных для карты. Так что если ваше приложение должно работать offline или использовать сторонний API, стандартные плагины вас могут не устроить. Придётся реализовывать либо свой плагин, либо свою карту.

    Мы пойдём по первому пути: создадим плагин Qt GeoServices, который будет обращаться к offline-провайдеру тайлов для отображения карты и к Sight Safari. На Хабре уже рассказывали об этом сервисе для построения маршрутов (раз и два). Возможно, кому-то не помешает изучить базовую информацию о работе с картами в Аврора.

    Подготовка: настраиваем OSM Scout Server

    Для offline-доступа к тайлам карты устанавливаем на наш телефон под управлением ОС Аврора OSM Scout Server — полностью автономное решение для навигации. Ещё нам понадобится дополнительный модуль со шрифтами Noto, которые используются для рендеринга карт.

    Чтобы тайлы возвращались в подходящем для отображения формате, выбираем профиль «Рекомендовано для карт с векторными и растровыми тайлами». Теперь осталось выбрать и скачать карты необходимого района в «Диспетчере карт». Они отобразятся списком, как на рисунке ниже.

    С чего начинается плагин: конфигурационный файл

    Театр начинается с вешалки, а Qt-плагин — с конфигурационного json-файла.

    {
        "Keys": ["osmscoutoffline"],
        "Provider": "osmscoutoffline",
        "Version": 100,
        "Experimental": false,
        "Features": [
            "OfflineMappingFeature",
            "OnlineRoutingFeature"
        ]
    }

    Keys — уникальное имя плагина, Provider — имя сервиса-провайдера, Version — версия плагина, Experimental — статус плагина, Features — список поддерживаемых функций. Согласно нашей конфигурации, плагин osmscoutoffline версии 1.0.0, поддерживает функции отображения карт offline и построения маршрутов online и не является экспериментальным, то есть доступен для всех приложений.

    Регистрируем плагин в системе

    qgeoserviceproviderfactoryosmscoutoffline.h
    #ifndef QGEOSERVICEPROVIDERFACTORYOSMSCOUTOFFLINE_H
    #define QGEOSERVICEPROVIDERFACTORYOSMSCOUTOFFLINE_H
    
    #include <QObject>
    #include <QGeoServiceProviderFactory>
    
    class QGeoServiceProviderFactoryOsmScoutOffline : public QObject, public QGeoServiceProviderFactory
    {
        Q_OBJECT
        Q_INTERFACES(QGeoServiceProviderFactory)
        Q_PLUGIN_METADATA(IID "org.qt-project.qt.geoservice.serviceproviderfactory/5.0"
                          FILE "../osmscoutoffline_plugin.json")
    
    public:
        QGeoRoutingManagerEngine *createRoutingManagerEngine(const QVariantMap &parameters,
                                                             QGeoServiceProvider::Error *error,
                                                             QString *errorString) const;
    
        QGeoMappingManagerEngine *createMappingManagerEngine(const QVariantMap &parameters,
                                                             QGeoServiceProvider::Error *error,
                                                             QString *errorString) const;
    };
    
    #endif // QGEOSERVICEPROVIDERFACTORYOSMSCOUTOFFLINE_H
    qgeoserviceproviderfactoryosmscoutoffline.cpp
    #include "qgeoserviceproviderfactoryosmscoutoffline.h"
    #include "qgeoroutingmanagerengineosmscoutoffline.h"
    #include "qgeotiledmappingmanagerengineosmscoutoffline.h"
    
    QGeoRoutingManagerEngine *QGeoServiceProviderFactoryOsmScoutOffline::createRoutingManagerEngine(
            const QVariantMap &parameters, QGeoServiceProvider::Error *error,
            QString *errorString) const
    {
        return new QGeoRoutingManagerEngineOsmScoutOffline(parameters, error, errorString);
    }
    
    QGeoMappingManagerEngine *QGeoServiceProviderFactoryOsmScoutOffline::createMappingManagerEngine(
            const QVariantMap &parameters, QGeoServiceProvider::Error *error,
            QString *errorString) const
    {
        return new QGeoTiledMappingManagerEngineOsmScoutOffline(parameters, error, errorString);
    }

    Обратите внимание на макрос Q_PLUGIN_METADATA, который регистрирует json-файл в системе. Параметр IID — название упомянутого выше класса для реализации интерфейса (org.qt-project.qt.geoservice.serviceproviderfactory/5.0). Параметр FILE — путь к json-файлу.

    Объявленные в public-секции методы createRoutingManagerEngine и createMappingManagerEngine создают объекты, отвечающие за ту или иную функцию указанного в Q_PLUGIN_METADATA интерфейса. Класс QGeoRoutingManagerEngineOsmScoutOffline отвечает за составление маршрутов, с ним мы разберёмся позже. Сейчас нас больше интересует QGeoTiledMappingManagerEngineOsmScoutOffline — наследник класса QGeoTiledMappingManagerEngine, работающий с OSM Scout Server. В конструкторе этого класса устанавливаем параметры, необходимые для работы нашего плагина.

    QGeoTiledMappingManagerEngineOsmScoutOffline::QGeoTiledMappingManagerEngineOsmScoutOffline
    QGeoTiledMappingManagerEngineOsmScoutOffline::QGeoTiledMappingManagerEngineOsmScoutOffline(
            const QVariantMap &parameters, QGeoServiceProvider::Error *error, QString *errorString)
    {
        QGeoCameraCapabilities cameraCaps;
        cameraCaps.setMinimumZoomLevel(0.0);
        cameraCaps.setMaximumZoomLevel(19.0);
        setCameraCapabilities(cameraCaps);
        setTileSize(QSize(256, 256));
        QList<QGeoMapType> mapTypes;
        mapTypes << QGeoMapType(QGeoMapType::StreetMap, tr("Street Map"), tr("OSM Street Map"), false, false, 1);
        setSupportedMapTypes(mapTypes);
        QGeoTileFetcherOsmScoutOffline *tileFetcher = new QGeoTileFetcherOsmScoutOffline(this);
        tileFetcher->setParams(parameters);
        setTileFetcher(tileFetcher);
        *error = QGeoServiceProvider::NoError;
        errorString->clear();
    }

    Указываем минимальный и максимальный уровни масштабирования карты, а также размер получаемых от сервера тайлов. Затем прописываем поддерживаемые типы карт — в данном случае только карты улиц (StreetMap). Наконец, создаём объект, который будет обращаться к серверу за тайлами. Далее рассмотрим его подробнее.

    Объект, который обращается к серверу

    QGeoTiledMapReply *QGeoTileFetcherOsmScoutOffline::getTileImage(const QGeoTileSpec &spec)
    {
        QUrlQuery query;
        for (QString &key : m_params.keys())
            query.addQueryItem(key, m_params[key].toString());
        query.addQueryItem(QStringLiteral("x"), QString::number(spec.x()));
        query.addQueryItem(QStringLiteral("y"), QString::number(spec.y()));
        query.addQueryItem(QStringLiteral("z"), QString::number(spec.zoom()));
        QUrl url(QStringLiteral("http://localhost:8553/v1/tile"));
        url.setQuery(query);
        QNetworkRequest remoteRequest(url);
        QNetworkReply *reply = m_networkManager->get(remoteRequest);
        return new QGeoMapReplyOsmScoutOffline(reply, spec);
    }

    Метод getTileImage принимает параметры, которым должен соответствовать запрашиваемый тайл, и возвращает объект, содержащий изображение. Запрос к серверу создаётся внутри метода, поэтому его дополнительная синхронизация не требуется даже в случае нелокального расположения сервера.

    Важно указать формат API, к которому мы обращаемся. В нашей реализации OSM Scout Server крутится непосредственно на телефоне (http://localhost) на стандартном порте (8553). Запрос тайла с сервера (метод tile) идёт через первую версию API (v1). Параметры x, y и z — координаты и масштаб запрашиваемого тайла. Цикл по m_params в начале метода позволяет добавить к запросу параметры, переданные из определения плагина в QML.

    Сохраняем тайл в наследнике класса QGeoTiledMapReply — для отображения в компоненте Map.

    void QGeoMapReplyOsmScoutOffline::networkReplyFinished()
    {
        if (!m_reply)
            return;
        if (m_reply->error() != QNetworkReply::NoError)
            return;
        setMapImageData(m_reply->readAll());
        setMapImageFormat("png");
        setFinished(true);
        m_reply->deleteLater();
        m_reply = 0;
    }

    Здесь после проверки на наличие ошибок пришедшее изображение считывается в виде массива байтов. Затем указывается формат изображения (в данном случае — png). Сигнал finished уведомляет компонент Map о том, что тайл готов к отрисовке.

    А что с маршрутом? Обзор Sight Safari API

    Итак, наш Qt GeoServices-плагин может определять и показывать положение пользователя на карте. Теперь научим его отображать маршруты, построенные с помощью сервиса Sight Safari.

    Для поиска маршрута сервис предоставляет метод findpath, который принимает на вход шесть параметров. Впрочем, мы собираемся передавать только три:

    • from — начало пути;

    • to — конец пути;

    • ratio — «интересность».

    Чем больше значение последнего параметра, тем больше достопримечательностей и просто красивых мест встретится на пути. Мы зададим ratio равным единице — в качестве оптимального сочетания интересности и протяжённости маршрута.

    Отладочная информация в нашем приложении не нужна, поэтому параметр debug оставим установленным по умолчанию. Якорная точка через desiredCoordinates не передаётся, так как в параметрах from и to будут указаны точные координаты. Параметр apiKey тоже опустим: для демонстрационных целей хватает бесплатных возможностей API.

    В качестве ответа findpath возвращает json-объект, в котором нас интересует массив latLonPoints: здесь хранятся точки, по которым проходит маршрут — его-то и надо отобразить на нашей карте. Кстати, несмотря на указанный в документации Sight Safari тип запроса POST, API прекрасно работает и через GET-запросы.

    Расширение функциональности плагина

    Настало время разобраться с классом QGeoServiceProviderFactoryOsmScoutOffline, с которым мы уже сталкивались в ходе регистрации плагина в системе. В нём имплементирован метод createRoutingManagerEngine, возвращающий указатель на объект класса, отвечающего за построение маршрутов:

    QGeoRoutingManagerEngine *QGeoServiceProviderFactoryOsmScoutOffline::createRoutingManagerEngine(
            const QVariantMap &parameters, QGeoServiceProvider::Error *error, QString *errorString) const
    {
        return new QGeoRoutingManagerEngineOsmScoutOffline(parameters, error, errorString);
    }

    Основной метод в этом классе — calculateRoute, принимающий запрос с точками, через которые проходит маршрут, и возвращающий ответ с одним или несколькими маршрутами для отображения:

    QGeoRouteReply
    QGeoRouteReply *QGeoRoutingManagerEngineOsmScoutOffline::calculateRoute(
            const QGeoRouteRequest &request)
    {
       QGeoCoordinate start = request.waypoints()[0];
       QGeoCoordinate end = request.waypoints()[1];
       QString from = QStringLiteral("%1,%2").arg(QString::number(start.latitude()), QString::number(start.longitude()));
       QString to = QStringLiteral("%1,%2").arg(QString::number(end.latitude()), QString::number(end.longitude()));
       QUrlQuery query;
       query.addQueryItem(QStringLiteral("from"), from);
       query.addQueryItem(QStringLiteral("to"), to);
       query.addQueryItem(QStringLiteral("ratio"), QStringLiteral("1"));
       QUrl url(QStringLiteral("https://sightsafari.city/api/v1/routes/direct"));
       url.setQuery(query);
       QNetworkRequest remoteRequest(url);
       QNetworkReply *reply = mNetworkManager->get(remoteRequest);
       QGeoRouteReplyOsmScoutOffline routeReply = new QGeoRouteReplyOsmScoutOffline(reply, request, this);
       connect(routeReply, &QGeoRouteReplyOsmScoutOffline::finished,
               this, &QGeoRoutingManagerEngineOsmScoutOffline::replyFinished);
       connect(routeReply, static_cast<void(QGeoRouteReplyOsmScoutOffline::)
               (QGeoRouteReplyOsmScoutOffline::Error, const QString &)>(&QGeoRouteReplyOsmScoutOffline::error),
               this, &QGeoRoutingManagerEngineOsmScoutOffline::replyError);
       return routeReply;
    }

    Сначала из пришедшего запроса получаются координаты начала и конца маршрута — подразумевается, что запрос содержит только две координаты. Далее формируется и отправляется GET-запрос к описанному ранее методу API. Здесь поле mUrlPrefix содержит endpoint сервера.

    После этого формируется и возвращается указатель на объект с полученными маршрутами. У этого объекта могут быть сигналы finished или error, в соответствии с которыми выполняется обработка успешно либо неуспешно завершённого запроса. Обработка у нас простая:

    void QGeoRoutingManagerEngineOsmScoutOffline::replyFinished()
    {
        QGeoRouteReply *reply = qobject_cast<QGeoRouteReply *>(sender());
        if (reply)
            emit finished(reply);
    }
    
    void QGeoRoutingManagerEngineOsmScoutOffline::replyError(QGeoRouteReply::Error errorCode,
                                                             const QString &errorString)
    {
        QGeoRouteReply *reply = qobject_cast<QGeoRouteReply *>(sender());
        if (reply)
            emit error(reply, errorCode, errorString);
    }

    Метод calculateRoute создаёт и возвращает объект типа QGeoRouteReplyOsmScoutOffline. Всё, что нужно для работы объекта, находится в конструкторе:

    QGeoRouteReplyOsmScoutOffline::QGeoRouteReplyOsmScoutOffline
    QGeoRouteReplyOsmScoutOffline::QGeoRouteReplyOsmScoutOffline(QNetworkReply *reply,
                                                                 const QGeoRouteRequest &request,
                                                                 QObject parent)
        : QGeoRouteReply(request, parent)
    {
       if (reply == nullptr) {
           setError(UnknownError, QStringLiteral("Null reply"));
           return;
       }
       connect(reply, &QNetworkReply::finished,
               this, &QGeoRouteReplyOsmScoutOffline::networkReplyFinished);
       connect(reply, static_cast<void(QNetworkReply::)(QNetworkReply::NetworkError)>(&QNetworkReply::error),
               this, &QGeoRouteReplyOsmScoutOffline::networkReplyError);
       connect(this, &QGeoRouteReplyOsmScoutOffline::destroyed,
               reply, &QNetworkReply::deleteLater);
    }

    После проверки переданного в конструктор указателя на корректность, обработчики привязываются к посылаемым сигналам. Последние две связки — для обработки ошибки сетевого соединения и для освобождения памяти — не представляют большого интереса. А обработку успешного ответа от сервера рассмотрим подробнее:

    void QGeoRouteReplyOsmScoutOffline::networkReplyFinished
    void QGeoRouteReplyOsmScoutOffline::networkReplyFinished()
    {
        QNetworkReply *reply = static_cast<QNetworkReply *>(sender());
        reply->deleteLater();
        if (reply->error() != QNetworkReply::NoError)
            return;
        QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll());
        QJsonObject jsonBody = jsonDoc.object().value(QStringLiteral("body")).toObject();
        QJsonArray jsonPath = jsonBody.value(QStringLiteral("latLonPoints")).toArray();
        QList<QGeoCoordinate> coords;
        for (QJsonValue value : jsonPath) {
            QJsonArray coord = value.toArray();
            coords.append(QGeoCoordinate(coord.at(0).toDouble(), coord.at(1).toDouble()));
        }
        QGeoRoute route;
        route.setPath(coords);
        route.setRequest(request());
        setRoutes({ route });
        setFinished(true);
    }

    В этом методе из пришедшего с сервера json-ответа извлекаем массив latLonPoints с точками маршрута, который преобразуем в список объектов координат, сохраняем как маршрут (здесь мы подразумеваем, что маршрут один) и указываем, что обработка запроса произведена успешно.

    После вызова метода setFinished посылается сигнал finished. Он обрабатывается упомянутым ранее методом replyFinished, сообщающим приложению, использующему плагин, что маршрут построен.

    Кстати, что там с приложением? Особенности yaml-файла

    Мы почти готовы собрать и установить rpm-пакет, чтобы перейти к использованию плагина в своих приложениях. Осталось подкорректировать стандартный yaml-файл, создаваемый Аврора IDE, или написать свой с нуля.

    qtgeoservices-osmscoutoffline.yaml
    Name: qtgeoservices_osmscoutoffline
    Summary: QtGeoServices OSM Scout Offline with Sight Safary routing
    Version: 0.5.0
    Release: 1
    Group: System/Libraries
    URL: https://github.com/osanwe/qtgeoservices-osmscoutoffline
    License: BSD-3-Clause
    
    Sources:
    - '%{name}-%{version}.tar.bz2'
    
    Description: |
      QtGeoServices OSM Scout Offline with Sight Safary routing
    
    Configure: none
    Builder: qtc5
    
    PkgConfigBR:
      - Qt5Core
      - Qt5Location
      - Qt5Positioning
      - Qt5Network
    
    Files:
      - '%{_libdir}/qt5/plugins/geoservices/libqtgeoservices_osmscoutoffline.so'

    Самое важное изменение — блок Files, в котором прописывается путь установки скомпилированной библиотеки

    Ищем себя на карте

    В новом проекте, созданном Aurora IDE, по умолчанию создаются четыре QML элемента:

    • CoverPage.qml — данный элемент отвечает за вывод наиболее важной информации о приложении когда оно свернуто.

    • FirstPage.qml — в новом проекте эта страница является первой.

    • SecondPage.qml — не будем использовать.

    • harbour-walking.qml — название данного файла совпадает с названием проекта; в нем задаются элемент инициализации и элемент обложки приложения.

    Для нашего примера потребуется только FirstPage.qml, поэтому SecondPage.qml можно удалить. Далее мы удаляем всё содержимое FirstPage.qml, переименовываем его в MainPage.qml и приступаем к реализации нашего приложения.

    Первым делом добавляем все необходимые импорты для отображения карты и получения информации с GPS-приемника устройства. Также у нас есть некоторое множество дополнительно реализованных элементов, которые расположены в директории qml/views. Для того, чтобы у нас появился доступ ко всем элементам в данном каталоге необходимо добавить соответствующий импорт.

    import QtQuick 2.5
    import QtLocation 5.0
    import QtPositioning 5.0
    import Sailfish.Silica 1.0
    import "../views"

    Добавляем корневой для данного QML файла элемент Page и начинаем его заполнять.

    Page
    Page {
        id: page
    
        property bool mapFollowing: false
        property var mapGpsPosition: positionSource.position.coordinate
        property var mapCenterPosition: QtPositioning.coordinate(NaN, NaN)
        property var pressCoords: QtPositioning.coordinate(NaN, NaN)
    
        allowedOrientations: Orientation.Portrait
    
        Drawer {
            id: drawer
    
            anchors.fill: parent
            open: true
            backgroundSize: background.height
            background: Item {
                id: background
    
                // some code
            }
    
            Plugin {
                id: mapPlugin
    
                name: "osmscoutoffline"
            }
    
            PositionSource {
                id: positionSource
    
                updateInterval: 1000
                active: true
                preferredPositioningMethods: PositionSource.AllPositioningMethods
            }
    
            Map {
                id: map
    
                function initMapCenter() {
                    var moscowCenterPos = QtPositioning.coordinate(55.751244, 37.618423)
                    if (mapGpsPosition.isValid) {
                        map.center = mapGpsPosition
                        mapFollowing = true
                    } else {
                        map.center = moscowCenterPos
                    }
                    map.zoomLevel = 15
                }
    
                function setMapCenterFromGps() {
                    if (mapGpsPosition.isValid) {
                        map.zoomLevel = 17
                        map.center.latitude = mapGpsPosition.latitude
                        map.center.longitude = mapGpsPosition.longitude
                        mapFollowing = true
                    }
                }
    
                anchors.fill: parent
                plugin: mapPlugin
    
                onCenterChanged: {
                    mapCenterPosition = center
                    if (mapFollowing && center !== mapGpsPosition)
                        mapFollowing = false
                }
    
                Component.onCompleted: map.initMapCenter()
    
                MapMarker {
                    coordinate: mapGpsPosition
                    visible: mapGpsPosition.isValid
                    source: "../images/mylocation.svg"
                }
    
                Connections {
                    target: page
    
                    onMapGpsPositionChanged: {
                        if (mapFollowing)
                            map.setMapCenterFromGps()
                    }
                }
            }
        }
    }

    Что здесь было добавлено:

    • Дополнительные переменные:

      • mapFollowing — переменная в которой хранится флаг следования за изменяющийся позицией, получаемой с GPS датчика устройства; данное поведение будет воспроизводиться в том случае, если мы вызовем далее описанную функцию setMapCenterFromGps у объекта map и не будем изменять область вывода карты вручную.

      • mapGpsPosition — переменная в которой хранится текущая координата, полученная от элемента PositionSource.

      • mapCenterPosition — в данной переменной хранится координата центра карты.

      • pressCoords — переменная для хранения координаты нажатия по карте.

    • Drawer — контейнер который позволяет выдвинуть некоторую область на передний план, где могут быть расположены некоторые второстепенные элементы управления; выезжающая область задаётся через свойство: background (содержимое контейнера мы опишем позже).

    • Plugin — непосредственно сам плагин, описанный ранее, который будет взаимодействовать с картой и предоставлять ей необходимые тайлы. В качестве значения параметра name устанавливаем имя плагина, указанное в json-файле.

    • PositionSource — объект, предоставляющий информацию о текущей позиции телефона (для его работы необходимо включить GPS датчик устройства). Установка свойства active в значение true запускает работу данного элемента. Свойство updateInterval равное 1000 задает элементу таймаут по которому он будет сообщать о текущем положении раз в одну секунду. Свойство preferredPositioningMethods со значением PositionSource.AllPositioningMethods говорит о том, что данным элементом будут использоваться любые методы позиционирования.

    • Map — элемент для отображения карты. С помощью свойства anchors.fill: parent указываем, что карта должна занимать все доступное пространство. Значение свойства plugin — идентификатор объявленного ранее плагина. По сигналу onCenterChanged выполняется обновление переменной mapCenterPosition, объявленной в начале элемента Page и в случае, если флаг mapFollowing был выставлен в true, он сбрасывается (это означает, что мы не хотим чтобы центр карты автоматически обновлялся в соответствии с данными, получаемыми с GPS-приемника; для того, чтобы вернуть данное поведение обратно, необходимо нажать на кнопку в элементе Drawer, который будет описан позже). По сигналу Component.onCompleted выполняются действия, которые необходимо произвести после полной инициализации карты; здесь вызывается функция initMapCenter у объекта Map, которая выставляет начальный масштаб и в случае, если у нас есть валидные данные с GPS-приемника, то они выбираются в качестве начальной позиции центра карты, в обратном случае выставляются координаты центра Москвы.

    • MapMarker — элемент, унаследованный от MapQuickItem и расположенный в каталоге qml/views (с его реализацией можно ознакомиться в файлах исходника проекта) с предустановленными свойствами отображения границ изображений, их размером и якорной точкой. Таким образом, для данного элемента достаточно установить только иконку в свойстве source, его позицию на карте через свойство coordinate и флаг отображения в свойстве visible (так, например, если нам по каким-либо причинам не удалось получить валидные координаты с GPS-приемника, то и отображать этот элемент смысла нет).

    • Connections — данный элемент позволяет задать ему объект, у которого мы хотим обрабатывать сигналы через свойство target. При подключении к сигналам в QML обычным способом является создание обработчика вида on<Signal>. Здесь мы отслеживаем изменение текущей координаты полученной от PositionSource и, если свойство mapFollowing выставлено в true, то происходит автоматическое обновление центра отображаемой области карты при изменении его координат.

    Собираем данные исходники и проверяем. Результат должен получиться аналогично изображению ниже.

    Добавляем в приложение возможность построения маршрутов

    Добавим в элемент Map элемент BusyIndicator, отвечающий за индикацию процесса выполнения запроса к online-сервису по построению маршрутов. Данный элемент мы центрируем по отношению к родительскому, а именно к Map через свойство anchors.centerIn. Мы делаем именно так, потому что, если мы отобразим выезжающую область Drawer, то размер отображаемой области карты станет меньше, и таким образом мы избежим наложение этих элементов друг на друга. Так как на картах используется достаточно большое количество цветов, для данного индикатора лучше использовать контрастный цвет, а именно черный. Его мы получаем из системной темы и задаем свойству color. Устанавливаем свойство size, используя стандартное значение перечисления данного элемента. В завершении по данному элементу мы указываем, что изначально он не отображается через свойство running, в дальнейшем это свойство будет изменяться по определенным событиям.

    BusyIndicator {
        id: routeLoadingIndicator
    
        anchors.centerIn: parent
        size: BusyIndicatorSize.Large
        color: Theme.rgba(Theme.darkPrimaryColor, Theme.opacityOverlay)
        running: false
    }

    Добавим в элемент Map еще два элемента типа MapMarker для отображения начальной и конечной точек маршрута. Данные элементы аналогичны тому, что был описан ранее и отличаются только иконкой. Туда же добавим элемент MapRoute для вывода самого маршрута. Он аналогично MapMarker расположен в каталоге qml/views (с его реализацией можно ознакомиться в файлах исходника проекта). Данный элемент унаследован от MapPolyline с предустановленными свойствами отображения границ линии и ее толщиной. Этот элемент имеет свойство path которое будет заполняться чуть дальше.

    MapRoute { id: mapRoute }
    
    MapMarker {
        id: markerStart
    
        visible: false
        source: "../images/location.svg"
    }
    
    MapMarker {
        id: markerFinish
    
        visible: false
        source: "../images/location.svg"
    }

    Добавляем в элемент Drawer объекты, позволяющие получать информацию о маршрутах. Элемент RouteQuery отвечает за формирование запроса на построение маршрута к онлайн сервису Sight Safari. Элемент RouteModel хранит полученные маршруты и связывается с RouteQuery с помощью параметра query. В качестве значения параметра plugin указывается наш плагин объявленный ранее. Параметру autoUpdate присваиваем false, чтобы маршрут перестраивался не при изменении начальных и конечных координат, а только по запросу (по нажатии на кнопку, которая будет располагаться в Drawer). Также, при получении сигнала onRoutesChanged мы устанавливаем параметр path у элемента MapRoute и останавливаем работу индикатора загрузки информации о маршруте.

    RouteQuery { id: mapRouteQuery }
    
    RouteModel {
        id: mapRouteModel
    
        plugin: mapPlugin
        query: mapRouteQuery
        autoUpdate: false
    
        onRoutesChanged: {
            routeLoadingIndicator.running = false
            mapRoute.path = mapRouteModel.get(0).path
        }
    }

    Теперь подготовим выезжающую область в элементе Drawer. Для этого заполним элементами управления Item который установили свойству background в Drawer. Опустим описание всех свойств следующих элементов, достаточно будет описать их назначение. В данном элементе отображаются точки начала и конца маршрута, и две кнопки. Первая кнопка перемещает центр отображаемой области карты в определившиеся GPS координаты (кнопка будет неактивна, если GPS координаты невалидны или текущий отображаемый центр совпадает с координатами полученными с GPS-приемника). Вторая кнопка выполняет построение маршрута между двумя заданными точками (кнопка становится активной только, если обе точки будут заданы). В следующем фрагменте кода мы используем элемент CoordField, который аналогично остальным пользовательским элементам расположен в каталоге qml/views (с его реализацией можно ознакомиться в файлах исходника проекта).

    Item
    Item {
        id: background
    
        anchors.fill: parent
        height: column.implicitHeight + column.anchors.topMargin + column.anchors.bottomMargin
    
        Column {
            id: column
    
            anchors {
                fill: parent
                margins: Theme.paddingMedium
            }
            spacing: Theme.paddingMedium
            width: parent.width
    
            Label {
                text: qsTr("Route from:")
                font.bold: true
            }
    
            CoordField { id: startCoordField }
    
            Label {
                text: qsTr("Route to:")
                font.bold: true
            }
    
            CoordField { id: endCoordField }
    
            Row {
                anchors.horizontalCenter: parent.horizontalCenter
                spacing: Theme.paddingMedium
    
                Button {
                    text: qsTr("My position")
                    enabled: mapGpsPosition.isValid
                             && (mapGpsPosition.latitude !== mapCenterPosition.latitude
                                 || mapGpsPosition.longitude !== mapCenterPosition.longitude)
    
                    onClicked: map.setMapCenterFromGps()
                }
    
                Button {
                    text: qsTr("Route")
                    enabled: startCoordField.coordinate.isValid && endCoordField.coordinate.isValid
    
                    onClicked: {
                        routeLoadingIndicator.running = true
                        mapRouteQuery.clearWaypoints()
                        mapRouteQuery.addWaypoint(startCoordField.coordinate)
                        mapRouteQuery.addWaypoint(endCoordField.coordinate)
                        mapRouteModel.update()
                    }
                }
            }
        }
    }

    Данный элемент будет выглядеть так, как представлено на изображении ниже.

    Добавим возможность управления отображения данным выезжающим элементом. Для этого поместим в элемент Map кнопку типа MapButton, которая аналогично остальным пользовательским элементам расположена в каталоге qml/views (с его реализацией можно ознакомиться в файлах исходника проекта). Этот элемент унаследован от IconButton и дополнен рамкой и фоном. Тут мы его прижимаем к правому нижнему углу и задаем отступы по краям. Устанавливаем элементу в свойстве icon.source стандартную иконку для меню. Указываем, что иконка элемента будет подсвечиваться тогда, когда элемент Drawer открыт. Также при получении сигнала onClicked мы будем менять состояние отображения Drawer на обратное.

    MapButton {
        anchors {
            bottom: parent.bottom
            right: parent.right
            bottomMargin: Theme.paddingLarge
            rightMargin: Theme.horizontalPageMargin
        }
        icon.source: "image://theme/icon-m-menu"
        highlighted: drawer.open
    
        onClicked: drawer.open ? drawer.hide() : drawer.show()
    }

    Раз уж мы добавили одну кнопку, добавим еще парочку для управления масштабом в элемент Map.

    Map
    Column {
        anchors {
            right: parent.right
            verticalCenter: parent.verticalCenter
            rightMargin: Theme.horizontalPageMargin
        }
        spacing: Theme.paddingLarge
    
        MapButton {
            id: buttonZoomIn
    
            icon.source: "../images/zoom-plus.svg"
            enabled: map.zoomLevel < map.maximumZoomLevel
    
            onClicked: map.zoomLevel = Math.min(map.zoomLevel + 1.0, map.maximumZoomLevel)
        }
    
        MapButton {
            id: buttonZoomOut
    
            icon.source: "../images/zoom-minus.svg"
            enabled: map.zoomLevel > map.minimumZoomLevel
    
            onClicked: map.zoomLevel = Math.max(map.zoomLevel - 1.0, map.minimumZoomLevel)
        }
    }

    Теперь, когда большая часть реализована, необходимо добавить возможность выбора начальной и конечной точек на карте к элементу Map. Для этого добавляем в него MouseArea, отвечающий за обработку нажатий по экрану устройства. При нажатии по области карты, в случае, если диалог был закрыт, то он отображается и координата нажатия сохраняется в объявленную в самом начале переменную pressCoords, после чего отображается диалог PointDialog, который аналогично остальным пользовательским элементам расположен в каталоге qml/views (с его реализацией можно ознакомиться в файлах исходника проекта). В нем выводится координата нажатия, сохраненная ранее в pressCoords, и две кнопки: “От” и “До”. При нажатии на кнопку любую из кнопок происходит передача текущей координаты в соответствующее поле в выезжающей области Drawer. Как только будут заданы обе координаты, кнопка построения маршрута станет активной.

    MouseArea {
        anchors.fill: parent
        z: -1
    
        onClicked: {
            if (choosePointDialog.visible) {
                choosePointDialog.visible = false
            } else {
                pressCoords = map.toCoordinate(Qt.point(mouse.x, mouse.y))
                choosePointDialog.visible = true
            }
        }
    }
    
    PointDialog { id: choosePointDialog }

    После нажатия на кнопку построения маршрута получится что-то наподобие изображения ниже.

    На этом всё, подробнее о создании плагинов можно почитать на Хабре или в документации Qt. Мы же со своей стороны готовы дать любые пояснения в комментариях.

    Материалы для публикации подготовлены Петром Вытовтовым и Павлом Казеко с комментариями и правками от Кирилла Чувилина.

    Открытая мобильная платформа
    Российская мобильная доверенная ОС Аврора

    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое