Карты на Android: единый API и картографические провайдеры
==========================================================
NodaLogic предоставляет единый набор инструментов для работы с картами в
Android-клиенте. Прикладной обработчик использует функции ``MapCenter``,
``MapPoint``, ``MapPolyline``, ``MapRoute`` и другие ``Map*``-функции, не
обращаясь напрямую к классам картографического SDK.
Конкретный картографический движок выбирается элементом динамической разметки.
Например, ``YandexMap`` создаёт карту на основе Yandex MapKit. Команды
направляются нужному экземпляру карты по его ``id``.
Единый API **не означает одинаковых возможностей** всех движков. Отображение
переданной полилинии — одна задача, а поиск адреса или самостоятельный расчёт
маршрута — другая: они могут зависеть от выбранного провайдера, лицензии и
наличия сети. Доступность операций приведена в разделе :ref:`map-capabilities`.
.. note::
Описанный здесь встроенный картографический слой предназначен для
Android-клиента. Для веб-интерфейса карта может быть реализована отдельно
средствами HTML и JavaScript. Тип ``YandexMap`` не является компонентом
веб-разметки.
Архитектура
-----------
Работа с картами состоит из трёх уровней:
#. **Решение NodaLogic** хранит координаты, точки, зоны и полученные извне
маршруты как обычные бизнес-данные.
#. **Единый картографический API** принимает команды ``Map*``, направляет их
карте с заданным ``id`` и передаёт события обратно в обработчики решения.
#. **Провайдер** отображает карту и выполняет поддерживаемые им операции:
управление камерой, рисование объектов, поиск или построение маршрута.
Поэтому, если координаты маршрута уже рассчитаны сервером, их можно передать
карте через ``MapPolyline`` без вызова провайдерской маршрутизации. Поиск,
обратное геокодирование и расчёт маршрута являются отдельными возможностями,
которые нельзя автоматически ожидать от любого провайдера.
.. _map-capabilities:
Возможности по провайдерам
--------------------------
Таблица относится к функциям, описанным в этой статье. При добавлении новых
провайдеров в неё могут появиться дополнительные колонки.
.. list-table:: Поддержка функций
:header-rows: 1
:widths: 44 56
* - Функция единого API
- YandexMap
* - Отображение карты и управление камерой
- Да; Yandex MapKit.
* - GPS и отображение положения устройства
- Да, при наличии разрешения Android и доступного местоположения.
* - Точки, SVG-маркеры и подписи
- Да.
* - Полилинии, полигоны, окружности
- Да.
* - Готовый маршрут от сервера как ``MapPolyline``
- Да; маршрутный сервис провайдера не требуется.
* - Автоматический расчёт автомобильного маршрута, ``MapRoute``
- Да; асинхронно, с учётом условий работы MapKit.
* - Поиск и обратное геокодирование
- Да; асинхронно, через MapKit.
* - События нажатий, результатов поиска и маршрутизации
- Да.
* - Офлайновое отображение предварительно загруженного региона
- Да, при наличии соответствующей платной лицензии MapKit.
* - Управление офлайновыми регионами
- Через менеджер карт Android-клиента и механизм MapKit.
* - Офлайновый поиск и автомобильная маршрутизация
- Поддерживаются MapKit для скачанных регионов при наличии платной
лицензии; конкретный сценарий нужно проверять с используемым ключом
и режимом маршрутизатора.
.. note::
Таблица различает функции **единого API NodaLogic** и возможности **SDK
провайдера**. Например, сам факт поддержки офлайнового поиска в MapKit
не гарантирует, что любой запрос конкретного решения будет обработан
локально. Для ``MapReverseGeocode`` не следует обещать офлайновую работу
без отдельной проверки.
Общий Python API
----------------
Функции импортируются из ``nodesclient`` в Android-обработчике:
.. code-block:: python
from nodesclient import (
MapCall, MapState, MapCamera, MapCenter,
MapPoint, MapPolyline, MapPolygon, MapCircle,
MapRemove, MapClear, MapRoute,
MapSearch, MapReverseGeocode,
MapUserLocation, MapCenterGPS, MapFit,
)
Первый аргумент большинства функций — ``id`` экземпляра карты в разметке.
Таким образом, обработчик может адресовать команды нужной карте, даже если
на экране находятся несколько картографических элементов.
Для редких действий и параметров существует универсальный низкоуровневый вызов:
.. code-block:: python
MapCall("map", "action", {...})
Наличие универсального вызова не делает произвольное действие доступным
на любом провайдере. В обычном коде предпочтительны готовые ``Map*``-функции.
Управление камерой и GPS
------------------------
``MapCenter`` принимает координаты из любого источника: поля узла, внешней
системы, результатов поиска или GPS.
.. code-block:: python
MapCenter("map", [55.75, 37.62], zoom=14)
MapCenter(
"map",
{"latitude": 55.75, "longitude": 37.62},
)
Если ``zoom`` не передан, текущий масштаб сохраняется.
Для дополнительных параметров камеры:
.. code-block:: python
MapCamera(
"map", 55.75, 37.62,
zoom=15, azimuth=0, tilt=0, animated=True,
)
Показ и центрирование по местоположению самого Android-устройства:
.. code-block:: python
MapUserLocation("map", visible=True)
MapCenterGPS("map", zoom=16)
Если разрешение ещё не выдано, картографический компонент запускает штатный
Android-запрос разрешения. Не нужно дублировать этот запрос в прикладном
обработчике. Координаты, ранее полученные из внешней системы, следует
передавать через ``MapCenter``, а не через ``MapCenterGPS``.
Объекты на карте
----------------
Точка и её идентификатор:
.. code-block:: python
MapPoint(
"map", "client_1", 55.75, 37.62,
text="Клиент", color="#3f7353",
)
Первый аргумент — ``id`` карты, второй — идентификатор объекта. Повторный
вызов с тем же идентификатором заменяет точку.
Можно использовать собственный SVG-маркер:
.. code-block:: python
FLAG_SVG = ''
MapPoint(
"map", "warehouse_1", 55.76, 37.64,
text="Склад 1", svg=FLAG_SVG,
svg_size=42, svg_color="#1E88E5", number="1",
text_placement="bottom", text_offset=5,
)
Дополнительные параметры включают ``size``, ``draggable``, ``z``,
``number_background``, ``number_color``, ``text_size`` и
``text_offset_from_icon``. Фактическое оформление зависит от реализации
маркеров у выбранного провайдера.
Удаление объекта:
.. code-block:: python
MapRemove("map", "warehouse_1")
Готовый маршрут или трек
------------------------
Если сервер или внешняя система уже вернули геометрию маршрута, передайте
массив координат в ``MapPolyline``. Карта сразу отображает линию: ей не
нужно повторно рассчитывать путь.
.. code-block:: python
external_route = [
[55.7510, 37.6100],
[55.7542, 37.6160],
[55.7580, 37.6210],
]
MapPolyline(
"map", "external_route", external_route,
color="#1565C0", width=7,
)
Две точки образуют обычный отрезок:
.. code-block:: python
MapPolyline(
"map", "segment_1",
[[55.75, 37.61], [55.76, 37.64]],
color="#db3030", width=5,
)
Для пунктирной линии доступны дополнительные параметры ``dash_length`` и
``gap_length``. Передавайте точки в порядке следования по маршруту; простая
полилиния не умеет находить дороги между произвольными точками самостоятельно.
Полигоны и окружности
---------------------
.. code-block:: python
MapPolygon(
"map", "zone_1",
[[55.75, 37.62], [55.75, 37.64], [55.77, 37.64]],
stroke="#3f7353", fill="#403f7353", width=3,
)
MapCircle(
"map", "radius_1", 55.75, 37.62, 250,
stroke="#1565C0", fill="#401565C0", width=3,
)
События карты
-------------
При ``events=true`` последний результат записывается в:
.. code-block:: python
self._data["map_event"]
self._data["_event"]
Например, у карты ``delivery_map`` второе поле называется
``delivery_map_event``. Если на экране несколько карт и важно исключить
путаницу между ними, используйте поле с конкретным ``id``.
Карта вызывает стандартное событие ``onInput``; ``listener`` равен ``id``
карты. Для элемента с ``id="map"`` задайте обработчик:
.. code-block:: json
{
"event": "onInput",
"listener": "map",
"actions": [
{
"action": "run",
"source": "internal",
"server": "internal",
"method": "MapEvent",
"order": 1
}
]
}
Пример Python-обработчика:
.. code-block:: python
def MapEvent(self, input_data=None):
event = self._data.get("map_event") or {}
kind = event.get("event")
data = event.get("data") or {}
if kind == "tap":
lat = data.get("latitude")
lon = data.get("longitude")
elif kind == "object_tap":
object_id = data.get("object_id")
lat = data.get("latitude")
lon = data.get("longitude")
elif kind == "search_ready":
results = data.get("results") or []
elif kind == "route_ready":
route_count = data.get("count")
elif kind in ("route_error", "search_error", "location_error"):
error = data.get("error")
return True, self._data
Возможные события: ``tap``, ``long_tap``, ``object_tap``, ``route_ready``,
``route_error``, ``search_ready``, ``search_error``,
``location_permission_required``, ``location_permission_granted``,
``location_permission_denied`` и ``location_error``. Некоторые из них
возникают только у провайдера, реализующего соответствующую функцию.
Настройка в onShow
------------------
Карту удобно описать декларативно, а объекты добавить в ``onShow``:
.. code-block:: python
def InitMap(self, input_data=None):
MapCenter("map", [55.80, 37.59], zoom=10, animated=False)
MapUserLocation("map", visible=True)
MapPoint("map", "office", 55.75, 37.62, "Офис")
return True, self._data
``onShow`` может сработать до окончательного создания дерева Android View.
Короткие команды к ещё не смонтированной карте ставятся в очередь и выполняются
после регистрации соответствующего элемента. При этом разметка процесса
должна содержать сам картографический элемент: последующий ``Show()`` с пустой
разметкой может заменить уже созданный экран.
Хранение собственных точек
--------------------------
Карта отвечает за отображение, а не за долговременное хранение данных решения.
Сохраняйте точки обычными словарями в ``_data``:
.. code-block:: python
self._data["my_locations"] = [
{
"id": "client_1",
"name": "Клиент",
"address": "Москва, ...",
"latitude": 55.75,
"longitude": 37.62,
}
]
Один массив можно показать в обычном ``Table`` и отрисовать через
``MapPoint``. Адрес является необязательным полем: название и координаты
можно сохранить без вызова геокодера. При выборе строки таблицы используйте
координаты соответствующего элемента в ``MapCenter``.
Открытие точки во внешнем навигаторе
------------------------------------
.. code-block:: python
from android import share_location, open_location
share_location(55.75, 37.62, "Клиент")
open_location(55.75, 37.62, "Клиент")
Используется системный ``geo:``-intent Android, без привязки к конкретному
приложению-навигации.
NodaScript
----------
На Android доступны следующие команды общего картографического API:
.. code-block:: text
map_call(map_id, action, params)
map_state(map_id)
map_camera(map_id, latitude, longitude, zoom)
map_center(map_id, latitude, longitude, optional_zoom)
map_point(map_id, object_id, latitude, longitude, text)
map_route(map_id, params)
map_search(map_id, query)
map_center_gps(map_id, zoom)
map_clear(map_id)
share_location(latitude, longitude, label)
open_location(latitude, longitude, label)
Для расширенных параметров используйте ``map_call``:
.. code-block:: text
map_call(
'map',
'add_polyline',
{
'id':'route_1',
'points':[[55.75,37.61],[55.76,37.64]],
'color':'#1565C0',
'width':7
}
);
Очистка, подбор масштаба и состояние
------------------------------------
.. code-block:: python
MapClear("map") # Удалить обычные объекты
MapClear("map", routes=True) # Также удалить построенные маршруты
MapFit(
"map", [[55.75, 37.62], [55.77, 37.64]],
max_zoom=14,
)
state = MapState("map")
Состояние содержит координаты и масштаб камеры, провайдера, число объектов и
диагностические поля. Некоторые поля относятся только к отдельному SDK
(например, источник API-ключа или состояние маршрутной сессии).
.. _map-providers:
Провайдеры
----------
.. _map-provider-yandex:
YandexMap (Yandex MapKit)
~~~~~~~~~~~~~~~~~~~~~~~~~
``YandexMap`` — элемент динамической разметки, использующий Yandex MapKit.
Его методы отображения и взаимодействия вызываются через общий ``Map*``-API;
особенности поискового и маршрутного SDK описаны ниже.
Элемент YandexMap
^^^^^^^^^^^^^^^^^
Минимальная разметка:
.. code-block:: json
{
"type": "YandexMap",
"id": "map",
"height": 420,
"center": [55.751225, 37.62954],
"zoom": 13,
"events": true
}
``id`` обязателен. Для нескольких карт используйте разные ``id``.
Параметры элемента:
* ``center`` — начальные координаты ``[latitude, longitude]``;
* ``zoom`` — начальный масштаб;
* ``azimuth`` и ``tilt`` — направление и наклон камеры;
* ``show_user_location`` — показывать местоположение устройства;
* ``center_gps`` — при открытии центрировать карту по местоположению;
* ``heading`` — направление устройства, если поддерживается SDK;
* ``night`` — ночной режим;
* ``style`` — пользовательский стиль MapKit;
* ``route_color`` и ``route_width`` — цвет и толщина построенных маршрутов;
* ``events`` — отправлять события NodaLogic, по умолчанию ``true``;
* ``objects`` — начальные объекты типов ``point``, ``polyline``, ``polygon``,
``circle``;
* ``route_debug`` — диагностические Toast-сообщения маршрутизатора; в обычном
решении оставляйте ``false``.
Если высота не указана, применяется стандартная высота Android-компонента.
Обычные параметры динамической разметки NodaLogic также применимы.
Ключ MapKit
^^^^^^^^^^^
Пользователь может задать ключ в настройках Android-клиента:
``Настройки -> Maps -> Yandex MapKit API key``.
Если в сборке предусмотрен платформенный ключ по умолчанию, пустое поле
использует его. Пользовательский ключ имеет приоритет. MapKit получает ключ
при первой инициализации процесса; после изменения ключа полностью
перезапустите приложение.
Не храните ключ в конфигурации решения или в Python-обработчиках. Для
коммерческих возможностей, включая офлайновые регионы, необходим ключ с
соответствующими правами и условиями лицензии.
Автоматическое построение маршрута
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Текущая обвязка реализует автомобильные маршруты через MapKit:
.. code-block:: python
MapRoute(
"map",
[
[55.9292, 37.5207],
[55.76594, 37.68549],
[55.70306, 37.53028],
],
routes=1, clear=True, fit=True,
max_zoom=12, router_type="online",
)
Для старта с текущего положения устройства:
.. code-block:: python
MapRoute(
"map",
[[55.9292, 37.5207], [55.70306, 37.53028]],
from_gps=True, fit=True, router_type="online",
)
``from_gps=True`` использует общий механизм GPS и при необходимости
запрашивает разрешение Android.
Вызов асинхронный: немедленный ответ обычно содержит ``pending=true``, затем
приходит ``route_ready`` или ``route_error``. Событие ``route_ready``
передаёт число маршрутов и число точек геометрии, но не весь массив координат.
Если решение должно получить рассчитанную MapKit геометрию как массив,
понадобится отдельное расширение платформенного API.
.. important::
``MapRoute`` просит MapKit **рассчитать** маршрут. ``MapPolyline`` просто
**отображает** переданную геометрию и подходит для маршрутов, рассчитанных
на сервере или полученных из другой системы.
Поиск и обратное геокодирование
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. code-block:: python
MapSearch("map", "Парк Горького", limit=10)
MapReverseGeocode("map", 55.75, 37.62)
Если область поиска не задана через ``MapCall``, используется видимая область
карты. Результаты приходят через событие ``search_ready``. Элементы результата
могут содержать ``name``, ``description``, ``address`` и ``points``.
При обратном геокодировании поле ``query`` события равно
``reverse_geocode``. Ошибки передаются в ``search_error``. Результаты можно
хранить как обычные данные решения; карта не сохраняет их автоматически.
Офлайновые карты Яндекса
^^^^^^^^^^^^^^^^^^^^^^^^
MapKit может заранее скачать регионы и использовать их без соединения с
интернетом. В Android-клиенте для управления такими загрузками предусмотрена
вкладка ``Yandex`` экрана ``Офлайновые карты`` (если она включена в сборке).
Список регионов и сами данные предоставляет MapKit: это **не** импорт
произвольного файла карты с устройства.
Во вкладке можно просмотреть каталог регионов, начать загрузку, отслеживать
прогресс, приостанавливать и возобновлять загрузку, обновлять и удалять
скачанные данные. При загрузке используются возможности MapKit по управлению
сетевым подключением и хранилищем офлайнового кеша. Загруженные регионы
используются SDK автоматически; выбирать один «активный регион» для
``YandexMap`` не требуется.
.. important::
Офлайновые регионы MapKit доступны **только по платной лицензии** Яндекса.
Наличие вкладки в Android-клиенте само по себе не даёт доступа к скачиванию.
Подготовьте нужные регионы при наличии сети и проверьте ключ и условия
лицензии до работы устройства без подключения.
Для скачанных регионов MapKit документирует отображение карты, офлайновый
поиск и построение автомобильных маршрутов без пробок. При этом не следует
считать, что ``router_type="online"`` автоматически переключится на
локальный расчёт: возможность и режим конкретного запроса нужно проверять
отдельно. Аналогично не гарантируйте локальную работу
``MapReverseGeocode`` без проверки соответствующего сценария.
Официальная документация: `Офлайновые карты MapKit для Android
`_.
Практические рекомендации
-------------------------
* Используйте общий ``Map*``-API для прикладных действий, но проверяйте
доступность провайдерских функций по таблице возможностей.
* Храните бизнес-точки и готовые маршруты в данных решения. Карта занимается
отображением и взаимодействием с пользователем.
* Для готового маршрута используйте ``MapPolyline``; для самостоятельного
расчёта маршрута SDK — ``MapRoute``, если выбранный провайдер его поддерживает.
* ``MapRoute``, ``MapSearch`` и ``MapReverseGeocode`` выполняются асинхронно;
обрабатывайте результат через события.
* Не помещайте API-ключ MapKit в конфигурацию решения.
* Если известны координаты, используйте ``MapCenter``; ``MapCenterGPS``
предназначен для координат самого устройства.
* В обычном интерфейсе отключайте ``route_debug``, а большие SVG храните
в константах обработчика.