Здравствуйте! Данная статья является продолжением цикла статей, посвящённых разработке приложений для мобильной платформы Sailfish OS. На этот раз речь пойдёт о том, как можно реализовать в приложении получение информации о географическом положении устройства, отображение карты с текущим местоположением и пройденным маршрутом.

Приложение GPS-трекер


Для демонстрации возможностей API геолокации мы будем использовать приложение GPS-трекер, которое включает в себя все базовые функции, связанные с геолокацией:

  • Просмотр карты местности на экране телефона.
  • Отображение пройденного пути на карте.
  • Отображение записанных и загруженных GPS-треков из памяти устройства.
  • Ведение списка точек интереса с возможностью включения / выключения их отображения на карте.
  • Перемещение карты к выбранной точке интереса.

Средства работы с географическими данными


Платформа Sailfish OS использует стандартные классы фреймворка Qt для предоставления приложениям доступа к геолокационной информации. В свою очередь, фреймворк Qt предоставляет два модуля для работы с данными: Qt Positioning и Qt Location. Первая подсистема позволяет получить информацию о текущем местоположении устройства, а вторая предоставляет средства для геокодирования и отображения этой информации на карте. В статье мы рассмотрим только использование QML-типов (без C++-классов), так как этого обычно достаточно для реализации основных сценариев использования координат в приложениях.

Получение данных о местоположении с помощью Qt Positioning


Описание координат в Positioning API


Для описания координат используется тип Position, содержащий в себе большое количество полей, которые можно разделить на несколько групп. Стоит заметить, что не все поля Position обязательно задаются.

К первой группе полей относится информация о местоположении: в неё входит свойство coordinate, ссылающееся на экземпляр соответствующего типа coordinate, а также свойства altitudeValid, latitudeValid и longitudeValid, которые показывают, были ли соответствующие свойства получены или нет. Также для каждой координаты хранится время (timestamp), когда она была получена.

Во вторую группу входят свойства, связанные с координатами, и которые могут быть дополнительно получены с GPS-датчика или вычислены путём агрегации последовательности измеренных координат: скорость движения по поверхности земли (speed), скорость вертикального движения (verticalSpeed), направление движения (direction), а также соответствующие свойства с суффиксом Valid, которые указывают были ли соответствующие свойства получены.

Сам тип coordinate является достаточно простым и содержит информацию о географическом положении (latitude, longitude, altitude) и о том, являются ли указанные ширина и долгота корректными (isValid). Тип предоставляет дополнительные методы для вычисления расстояния между координатами (distanceTo), азимута между координатами (azimuth), а также вычисления координаты по расстоянию и азимута (atDistanceAndAzimuth).

Источник данных о географическом положении


Для получения текущих координат устройства необходимо использовать тип PositionSource. Данный объект предоставляет доступ к текущей позиции через свойство position. Пока источник для получения информации не установлен, данное свойство пусто.

Сначала необходимо определиться с источником информации, который требуется для приложения: достаточно ли ему примерных координат, полученных на основании вышек мобильных операторов или необходима высокая точность позиционирования с использованием систем глобального позиционирования. Вы можете обратиться к свойству supportedPositioningMethods для того, чтобы узнать какие методы поддерживаются платформой:

  • NoPositioningMethods — никакие методы не поддерживаются.
  • SatellitePositioningMethods — можно получить данные со спутника.
  • NonSatellitePosititiongMethods — можно получить данные с мобильной сети.
  • AllPosititiongMethods — поддерживаются все виды позиционирования.

Устройства под управлением операционной системы Sailfish OS поддерживают все перечисленные выше методы. И вы можете выбрать необходимые, установив свойство preferredPosititioningMethods в одно из указанных выше значений. Сначала будут использованы данные источники, затем источник по умолчанию. Если ни один из перечисленных источников не будет доступен, то свойство valid будет содержать false.

Стоит отметить, что на эмуляторе Sailfish OS у вас не будет возможности воспользоваться никакими источниками данных из перечисленных. Для решения этой задачи придётся прибегнуть к отдельному механизму, который мы обсудим позже.

Частота обновления информации о местоположении определяется с помощью свойства updateInterval. Не стоит устанавливать это значение слишком коротким — слишком частые обновления приведут к избыточному энергопотреблению. Для управления получением данных можно воспользоваться слеудющими методами:

  • start для запуска;
  • stop для остановки;
  • NonSatellitePosititiongMethods — можно получить данные с мобильной сети.
  • update для принудительного обновления местоположения.

Запускать и останавливать можно также декларативным способом — с помощью установки свойства active: присваивание ему положительного значения равносильно вызову метода start.

Когда источник данных активен, то он начинает обновлять свойство position. Оно может быть привязано к другим свойствам с помощью механизма связывания (binding) в QML. Кроме того, вы можете использовать сигнал onPositionChanged, чтобы обрабатывать новые значения координат по мере их получения из источника данных.

Использование Qt Positioning в Sailfish OS


Для использования информации о географическом положении в ваших приложениях необходимо в список зависимостей добавить пакет qt5-qtdeclarative-import-positioning (если используются только нативные классы, то достаточно пакета qt5-qtpositioning). Его необходимо указать в списке Requires YAML-файла вашего проекта. После добавления содержимое может выглядеть следующим образом:

Requires:
- sailfishsilica-qt5 >= 0.10.9
- qt5-qtdeclarative-import-positioning

Затем в QML-файле нам необходимо подключить модуль Qt Positioning и создать элемент PositionSource. Рассмотрим короткий пример.

import QtPositioning 5.3
PositionSource {
    id: src
    updateInterval: 1000
    active: true

    onPositionChanged: {
        var coordinate = src.position.coordinate;
        console.log("Coordinate:", coordinate.longitude, coordinate.latitude);
    }
}

В данном примере мы не настраиваем большинство свойств PositionSource и используем значения по умолчанию. Настраивается только свойство active, которому присваиваем значение true. Как только страница с данным элементом будет загружена, будет начат поиск местоположения.

Мы определяем обработчик события onPositionChanged, в котором сначала получаем доступ к текущей координате, а затем используем полученную ссылку для вывода ширины и долготы текущей координаты. Кроме того, можно просто использовать console.log(coordinate), в этом случае будет выведена строка в виде градусов и минут.

Отладка логики работы с PositionSource


Как уже упоминалось ранее, невозможно получить данные о местоположении, когда вы запускаете приложение на эмуляторе. Также во время разработки приложения вам скорее всего захочется сэкономить время, которое тратится на изначальное определение местоположения при каждом открытии соответствующей страницы.

Для решения этих проблем удобно использовать свойство nmeaSource типа PositionSource. Данное свойство принимает URL файла, в котором записан GPS-трек в формате NMEA. Данный формат менее распространён у нас по сравнению с GPX, однако, вы можете воспользоваться одним из онлайн-преобразователей для получения таких файлов из имеющихся у вас GPX-треков. Либо создать NMEA-трек, воспользовавшись, например, сервисом NMEA Generator.

После того как вы получили файл, его необходимо добавить в пакет приложения. Для этого достаточно в pro-файле проекта указать его как устанавливаемый ресурс с помощью следующей конструкции (подробнее можно прочитать здесь):

OTHER_FILES += nmea/*
nmea.files = nmea/*.nmea
nmea.path = /usr/share/$$TARGET/nmea
INSTALLS += nmea

Таким образом после установки приложения данный файл будет также доступен на устройстве. Использование данного свойства может выглядеть следующим образом:

PositionSource {
  id: positionSource
  active: true
  nmeaSource: "/usr/share/%1/nmea/path.nmea".arg(Qt.application.name)
}

В этом случае PositionSource будет предоставлять данные из указанного файла. Поэтому с помощью свойства nmeaSource можно также отображать уже сохраненные на устройстве NMEA-треки. Важно помнить, что установка этого значения отключает объект PositionSource от реального источника данных. Если вы хотите использовать реальный источник, то придётся пересоздать такой объект.

Теперь вы можете реализовать любую форму отслеживания координат ваших пользователей, а также реализовать отображение этой информации на картах.

Отображение карт и информации на них с помощью Qt Location


Модуль Qt Location предоставляет широкий набор классов, которые позволяют реализовать показ пользователю карты, информации на этой карте, построение маршрутов и поиск точек интереса (points of interest, POI) относительно текущей географической координаты. Хорошее введение в большинство возможностей данного модуля можно прочитать в официальной документации. Мы не можем осветить все особенности работы с картами, поэтому остановимся только на следующих моментах: как организовать показ карты в приложении, как показать маркер на карте и как показать пройденный путь.

Краткий обзор ключевых типов Qt Location


В рамках нашего сценария ключевым элементом является карта, Map. Этот элемент позволяет отображать изображение поверхности Земли со спутника или географическую карту. Источниками данных изображений и другой полезной информации являются плагины, которые могут предоставляться как разработчиками платформы, так и независимыми разработчиками.

На платформе Sailfish OS доступны два плагина: OpenStreetMap и HERE WeGo. Вы можете выбрать один из них, установив свойство plugin карты равным osm или here соответственно (для использования here необходимо получить API-ключ). Выбор карт зависит от качества их покрытия для конкретных территорий, на которых вы собираетесь использовать приложение. Также необходимо указать требуемый плагин в зависимостях вашего приложения, как будет показано ниже.

Для управления областью просмотра карты необходимо использовать свойство center, указывающее координаты центра изображения, а также свойство zoomLevel, указывающее уровень масштабирования. В свойство center необходимо записать координаты в соответствии с типом coordinate, который мы упомянули ранее в разделе по работе с Positioning API.

Отображение карты в Sailfish OS


Для использования модуля Qt Location в вашем приложении необходимо обеспечить установку как минимум одного из провайдеров географических данных:

Requires:
  - qt5-qtdeclarative-import-location
  - qt5-plugin-geoservices-osm
  - qt5-plugin-geoservices-here

Из двух последних пакетов можно оставить один, если вы планируете использовать только один провайдер географической информации.

Теперь можно размещать все необходимые компоненты на странице приложения. Рассмотрим минимальный пример отображения карты.

import QtPositioning 5.3
import QtLocation 5.0
import Sailfish.Silica 1.0
import QtQuick 2.0
Page {
  PositionSource {
    id: positionSource
    active: true
  }
  Plugin {
    id: osmPlugin
    allowExperimental: true
    preferred: ["osm"]
  }
  Map {
    id: map
    anchors.fill: paren
    plugin: osmPlugin
    center: positionSource.position.coordinate
  }
}

Сначала мы подключаем модули Qt Positioning и Qt Location, чтобы все необходимые нам типы стали доступны на странице.

В основе страницы лежит объект Page, предоставляемый библиотекой Sailfish Silica. На странице мы формируем источник данных о местоположении, который сразу активируем, а также плагин для географических данных. В качестве источника этих данных используем OpenStreetMap.

Основным видимым элементом страницы является карта, которая заполняет всё доступное на странице пространство. Её мы настраиваем следующим образом: настраиваем плагин и связываем свойство center карты с координатой, которую предоставляет источник данных.

Добавление элементов на карту


Существует два подхода по созданию элементов на карте: статический и динамический. Статический хорошо подходит для объектов, количество которых не изменяется с течением времени. Для нашего приложения такими объектами стали указатель текущего местоположения и пройденный маршрут. Динамическими элементами приложения стали указатели точек интереса, которые пользователи добавляют самостоятельно.

Для отображения текущего местоположения мы воспользовались типом MapQuickItem. Он позволяет расположить на поверхности карты произвольный объект, например, объект Image, отображающий маркер. Рассмотрим детально его использование.

Map {
  MapQuickItem {
    coordinate: positionSource.position.coordinate
    anchorPoint.x: image.width / 2
    anchorPoint.y: image.height
    sourceItem: Image {
      id: image
      width: 35
      height: 50
      source: "file:buttons/main_marker.png”
    }
  }
}

Мы объявили данный объект как дочерний объект к Map, таким образом связав их вместе. Для позиционирования объекта мы указали свойство coordinate, которое связали с текущим географическим положением мобильного устройства. Также было использовано свойство anchorPoint, которое позволило отодвинуть изображение маркера таким образом, чтобы его острие указывало на текущую позицию.

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

Map {	
  MapPolyline {
    id: mapline
    line.width: 3
    line.color: 'blue
  }
}

Объект данного типа должен быть декларирован как дочерний по отношению к Map. Мы также настроили цвет и ширину отображения текущего трека.

Для добавления новых точек к линии отвечает следующий код:

PositionSource {
  id: positionSource
  active: true
  onPositionChanged: {
    mapline.addCoordinate(position.coordinate)
  }
}

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

Для отображения набора точек интереса мы использовали тип MapItemView. Данный тип представляет собой реализацию подхода MVC для карт: вам необходимо определить модель для отображения, а также вид для каждого элемента данной модели. Если вы знакомы с тем как наполнять списки элементов в QML, то работа с данным компонентом не составит труда.

Рассмотрим основные аспекты применения данной модели в приложении.

Map {
  MapItemView {
    id: mapView
    model: MapViewModel { }
    delegate: mapPlace
  }
  Component {
    id: mapPlace
    MapQuickItem {
      coordinate: QtPositioning.coordinate(latitude, longitude)
      anchorPoint.x: imagePlace.width / 2
      anchorPoint.y: imagePlace.height
      sourceItem: Image {
        id: imagePlace
        width: 35
        height: 50
        source: "file:buttons/additional_marker.png"
      }
    }
  }
}

Мы определили объект MapItemView дочерним объектом к Map — таким образом его элементы тоже будут отображаться на карте. В качестве модели мы использовали собственную модель MapViewModel, которая фактически является расширением стандартной ListModel, в которую добавлены методы для добавления и обновления элементов. Элементами модели являются объекты с указанными свойствами latitude и longitude. Чтобы добавить новую точку интереса на карту, необходимо вызвать метод addView() модели, передав в него идентификатор точки, её широту и долготу. Реализация метода выглядит следующим образом:

function addView(id, latitude, longitude) {
  append({
    id: id,
    latitude: latitude,
    longitude: longitude
  })
}

Делегатом объекта mapView мы указали компонент mapPlace, который содержит уже знакомый нам MapQuickItem. Отличие данного элемента от предыдущего состоит в том, что тип coordinate нам приходится создавать явно с помощью соответствующего метода типа QtPositioning, а также в указании другого изображения для отображения маркера точки интереса.

Заключение


В данной статье были рассмотрены особенности использования средств геолокации при разработке для Sailfish OS. Приложение, разработанное в рамках данной статьи, можно установить на свой телефон, воспользовавшись ресурсом OpenRepos.net. Вы также можете посмотреть на исходный код приложения, который доступен в нашем репозитории на bitbucket.

Комментарии (3)


  1. Zifix
    18.04.2018 18:47

    Господа, минусующие техническую статью — вы бы хоть указывали, что именно не так.


  1. 1cubik
    19.04.2018 17:19

    Добрый день!


    Прочитав Вашу статью, хотелось бы увидеть в статье хотя бы одну картинку, как результат выглядит в реальной жизни на экране смартфона. ИМХО, достоинством Sailfish OS являются UI-компоненты, сильно отличающиеся от привычных компонентов iOS и Android-а. Так покажите в статье интерфейс к этому приложению, чтобы можно было "зацепить" пользователей/читателей =)
    Плюс к этому, возможно, чтобы Вы показали пример на каком-нибудь более интересном usecase-е. Потому что на данный момент, как я понял, Вы просто получаете некоторые координаты от GPS и просто "лепите" их на карту. При этом не учитывая погрешность показаний GPS, например… не соотносите полученные значения, скажем, с улицей на карте(что было бы интересно). В т.ч. на принсткрине приложения в OpenRepos у Вас, видимо, человек решил прыгнуть с моста в реку.
    Думаю, что было бы гораздо лучше, если бы это была ПЕРВАЯ статья из цикла использования сервисов геолокации. Например, создание мини-навигатора на Sailfish OS. Тем более, что это уже не первая статья про LocationService в Qt на Хабре(тут, например).


    P.S. Поправьте ссылки в статье, пожалуйста. На момент написания коммента не работала ссылка на Qt Location.


    1. FRUCT Автор
      19.04.2018 17:30

      Здравствуйте!

      Спасибо за комментарий. Цель данной статьи — познакомить читателя с базовыми возможностями работы с геолокационными сервисами на платформе Sailfish OS. Она рассчитана на начинающих разработчиков, которые просто хотят познакомиться с данной платформой и попробовать свои силы в разработке под нее. Именно поэтому было решено не усложнять примеры.

      Ссылку поправили.