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


AliExpress RU&CIS

Привет, Хабр! Хотим рассказать о том, как создать плагин 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):