Карты на 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 храните в константах обработчика.