Mapkit что это
Перейти к содержимому

Mapkit что это

  • автор:

В чем заключается отличие Яндекс Map Kit от Яндекс Карты API?

Стоит задача: добавить карты Яндекс в мобильное приложение. В ходе поиска информации выяснил, что это можно сделать двумя способами: Яндекс Map Kit и Яндекс Карты API. В чем состоит их отличие? Что из этого лучше использовать для мобильного приложения на Android? Работаю в Android Studio, пишу на Java.

Отслеживать

2,785 3 3 золотых знака 32 32 серебряных знака 64 64 бронзовых знака

задан 29 авг 2017 в 4:47

745 1 1 золотой знак 8 8 серебряных знаков 27 27 бронзовых знаков

Внедряем MapKit Яндекс Карт в iOS приложение

Всем привет! Меня зовут Серёжа, и я занимаюсь веб-разработкой и разработкой на iOS вот уже 5 лет. На одном из проектов мне поставили задачу: внедрить Яндекс Карты в приложение. Однако я столкнулся с тем, что в открытом доступе мало нужной и полезной информации об этой теме. Эта статья — мои способы решения задач на Swift с использованием Yandex MapKit. Делюсь с вами своим опытом!

В статье я рассказываю о том, как:

  • Показать точку на карте по координатам или адресу;
  • Выбрать определённую точку и вывести её данные;
  • Отобразить большое количество точек по адресу.

Подготовка

�� Для создания интерфейса я использовал программный метод (UIKit)

Для работы с Yandex MapKit на iOS необходимы:

  • MacOS Catalina и выше
  • Xcode (желательно 12.1 и выше)
  • Homebrew
  • Ruby
  • CocoaPods
  • Ключ для доступа к API Яндекса (можно запросить бесплатно)
  • Целевая версия iOS 12 и выше (iOS 13+ для Apple Silicon) Учтите, что требования могут измениться. Статья написана в июне 2023 года.

Во время работы на процессорах Apple Silicon, на версии MapKit SDK ниже 4.3.0 возможны вылеты приложения во время открытия карты. Особенно во время отладки. Пример ошибки:

Пример ошибки на MacBook с процессором M1

С версией MapKit SDK 4.3.0 (2 марта 2023) появилось следующее обновление:

Для эмуляторов с процессором M1 карта автоматически переключается на использование Metal API.
Источник: Версии MapKit – Яндекс MapKit. Руководство разработчика

После обновления до последней версии вылеты без причины прекратились. На реальном устройстве проблем тоже не возникло.

Демо-приложение Яндекса

Демо-приложение MapKit позволяет использовать возможности Яндекс.Карт в мобильных приложениях для iOS и Android. Здесь есть разделы:

Скриншоты Yandex MapKit Demo Application

  • Показ карты;
  • Добавление объектов и пинов;
  • Месторасположение пользователя;
  • Пробки;
  • Панорама;
  • Поиск и подсказки;
  • Выбор объекта по клику.

На всякий случай, оставлю ссылки на руководство для MapKit от разработчика и демо-версию приложения:

Работа с картой

Добавление карт в проект

Для начала необходимо добавить зависимость в CocoaPods, чтобы загрузить библиотеку.

�� Если в проекте не подключён CocoaPods, то нужно инициализировать его. Для этого в терминале в корневой директории прописываем pod init .

После инициализации, проект необходимо открывать с помощью нового файла проекта с расширением .xcworkspace (пример: MapKitDemo.xcworkspace )

pod 'YandexMapsMobile', '4.3.1-full’

Нужно учесть, что есть две версии библиотеки: lite и full. Lite-версия позволяет работать с онлайн и оффлайн картами, показывает локацию и пробки. Full-версия предоставляет маршруты автомобилей, велосипедов, пешеходов, общественного транспорта; поиск; подсказки; геокодирование; отображение панорам.

После добавления зависимостей, появляется возможность сделать import YandexMapsMobile . Добавляем следующие строчки в главный метод AppDelegate :

/* imports */ import YandexMapsMobile @main class AppDelegate: UIResponder, UIApplicationDelegate < func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) ->Bool < /* code */ /* Init YandexMaps MapKit */ YMKMapKit.setApiKey("your-api-key") YMKMapKit.setLocale("ru_RU") YMKMapKit.sharedInstance() /* code */ return true >>

Создаём базовый UIView для удобства использования в других модулях:

import UIKit import YandexMapsMobile class YBaseMapView: UIView < @objc public var mapView: YMKMapView! required init?(coder aDecoder: NSCoder) < super.init(coder: aDecoder) setup() >override init(frame: CGRect) < super.init(frame: frame) setup() >private func setup() < // OpenGl is deprecated under M1 simulator, we should use Vulkan mapView = YMKMapView(frame: bounds, vulkanPreferred: YBaseMapView.isM1Simulator()) mapView.mapWindow.map.mapType = .map >static func isM1Simulator() -> Bool < return (TARGET_IPHONE_SIMULATOR & TARGET_CPU_ARM64) != 0 >>

Создание карты и добавление точки

Добавляем заранее созданный view в новый компонент, чтобы отобразить карту:

final class YandexMapSampleViewController: UIViewController < // 1. Создать элемент lazy var mapView: YMKMapView = YBaseMapView().mapView override func viewDidLoad() < super.viewDidLoad() // 2. Добавить в родительский view во viewDidLoad() view.addSubview(mapView) // 3. Настроить constraints. Приведён пример со SnapKit mapView.snp.makeConstraints < $0.leading.trailing.top.equalToSuperview() $0.bottom.equalTo(view.safeAreaLayoutGuide) >// 4. Вызов функции добавления точки на карту self.addPlacemarkOnMap() > >

Теперь создаём отдельную функцию для добавления точки на карту:

func addPlacemarkOnMap() < // Задание координат точки let point = YMKPoint(latitude: 47.228836, longitude: 39.715875) let viewPlacemark: YMKPlacemarkMapObject = mapView.mapWindow.map.mapObjects.addPlacemark(with: point) // Настройка и добавление иконки viewPlacemark.setIconWith( UIImage(named: "map_search_result_primary")!, // Убедитесь, что у вас есть иконка для точки style: YMKIconStyle( anchor: CGPoint(x: 0.5, y: 0.5) as NSValue, rotationType: YMKRotationType.rotate.rawValue as NSNumber, zIndex: 0, flat: true, visible: true, scale: 1.5, tappableArea: nil ) ) >

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

Интерактивные точки на карте

Чтобы сделать точку кликабельной, необходимо реализовать интерфейс YMKMapObjectTapListener и указать его при создании точки. Я использую расширение классов Swift, которое упрощает понимание кода и не загромождает основной класс.

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

extension YandexMapSampleViewController: YMKMapObjectTapListener < func onMapObjectTap(with mapObject: YMKMapObject, point: YMKPoint) ->Bool < // your code here >>
extension YandexMapSampleViewController: YMKMapObjectTapListener < func onMapObjectTap(with mapObject: YMKMapObject, point: YMKPoint) ->Bool < guard let placemark = mapObject as? YMKPlacemarkMapObject else < // Сценарий на случай ошибки return false >// Сценарий на случай успеха. Бизнес-логику добавляют сюда. // Пример self.focusOnPlacemark(placemark) return true > func focusOnPlacemark(_ placemark: YMKPlacemarkMapObject) < // Поменять расположение камеры, чтобы сфокусироваться на точке mapView.mapWindow.map.move( with: YMKCameraPosition(target: placemark.geometry, zoom: 18, azimuth: 0, tilt: 0), animationType: YMKAnimation(type: YMKAnimationType.smooth, duration: duration), cameraCallback: nil ) >>

Чтобы прослушиватель событий работал, нужно указать его при создании точки:

// Создание переменной точки не показано (см. пример выше) viewPlacemark.addTapListener(with: self)

После этого любой клик по точке будет следовать через созданную функцию.

Пользовательские данные точки

Задача: приблизить карту и отобразить данные на экране с помощью нажатия на точку на карте. Решение: при создании каждой точки нужно заполнить переменную YMKPlacemarkMapObject.userData нужными данными. Это может быть строка с названием/адресом, или же другой объект любого типа.

let viewPlacemark: YMKPlacemarkMapObject = mapView.mapWindow.map.mapObjects.addPlacemark(with: point) /* Здесь будет код, добавляющий иконку к точке */ viewPlacemark.userData = "Точка на карте" 

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

Пример функции фокусировки камеры на точке:

func focusOnPlacemark(placemark: YMKPlacemarkMapObject) < // Поменять расположение камеры, чтобы сфокусироваться на точке mapView.mapWindow.map.move( with: YMKCameraPosition(target: placemark.geometry, zoom: 18, azimuth: 0, tilt: 0), animationType: YMKAnimation(type: YMKAnimationType.smooth, duration: duration), cameraCallback: nil // Опциональный callback по завершению работы камеры ) if let placemarkName: String = placemark.userData as? String < // Пример self.displaySelectedPlacemarkName(placemarkName) >else < // do nothing >> func displaySelectedPlacemarkName(_ placemarkName: String) < // your code here >

Пример заранее созданной панели для отображения данных точки:

Пример действия при нажатии на точку

Поиск по адресу

Для поиска по адресу (прямое геокодирование), используйте full версию MapKit ( lite не подойдёт).

Делаем так: отправляем адрес и получаем в ответ координаты, которые можно показать на карте.

Пример класса с поиском:

final class YandexMapsAddressSearchInteractor < lazy var searchManager: YMKSearchManager? = YMKSearch.sharedInstance().createSearchManager(with: .combined) var searchSession: YMKSearchSession? // Окно поиска let BOUNDING_BOX = YMKBoundingBox( southWest: YMKPoint(latitude: 55.55, longitude: 37.42), northEast: YMKPoint(latitude: 55.95, longitude: 37.82) ) func searchAddress(_ address: String?, completion: @escaping(YMapsSearchVoid)) < guard let address = address else < return >searchManager = YMKSearch.sharedInstance().createSearchManager(with: .combined) // Callback функция, которая выполняется по завершению поиска let responseHandler = < (searchResponse: YMKSearchResponse?, error: Error?) ->Void in if let response = searchResponse < // Передаваемая callback функция. Обрабатывать результат нужно здесь completion(response) >else < let searchError = (error! as NSError).userInfo[YRTUnderlyingErrorKey] as! YRTError var errorMessage = L10n.ymapsUnknownError if searchError.isKind(of: YRTNetworkError.self) || searchError.isKind(of: YMKSearchCacheUnavailableError.self) < errorMessage = L10n.ymapsNetworkError >else if searchError.isKind(of: YRTRemoteError.self) < errorMessage = L10n.ymapsServerError >// showErrorMessage(errorMessage) > > searchSession = searchManager!.submit( withText: address, geometry: YMKGeometry(boundingBox: BOUNDING_BOX), searchOptions: YMKSearchOptions(), responseHandler: responseHandler ) > >

Пример использования поиска:

func showAddressOnMap(_ address: String) < interactor?.searchAddress(address) < [weak self] response in // Обработка только первого результата из ответа if response.collection.children.count >0 < let searchResults: [YMKGeoObjectCollectionItem] = response.collection.children if let mapObject = searchResults[0].obj < if let point = mapObject.geometry.first?.point < self?.view?.addPlacemarkOnMap(point) >> > else < // self?.showSearchError("No results found") >> >

Для более детального изучения поиска Яндекс Карт советую ознакомиться с этой статьёй. В ней описана работа поиска и его модулей с примерами. Учтите, что большинство примеров приведены для работы с Android, но они актуальны и для iOS.

Вот, как описывается параметр geometry в этой статье:

Параметр geometry чуть более хитрый. В зависимости от того, какая именно геометрия передана, поиск будет вести себя по-разному:

Если передать точку, то поиск будет производиться в небольшом окне рядом с этой точкой. Если передать прямоугольное окно (BoundingBox) или полигон из четырёх точек, то оно будет использовано как окно поиска. Простой пример такого окна – видимая область карты. Наконец, если передать полилинию, то описывающее её окно будет использовано как окно поиска, а ранжирование будет производиться с учётом этой полилинии.

Работа со множеством точек

Если нужно показать множество точек за один раз, добавляем несколько сотен (или тысяч) точек на карту по примеру, описанному выше. Но как большое количество точек будет выглядеть на карте? И что делать, если есть только список адресов?

Кластеризация

Чтобы визуально не нагружать карту, применим кластеризацию точек.

вверху – без кластеризации; внизу – с кластеризацией

Реализация похожа на обычное добавление точки на карту. Разница в том, что вместо mapView.mapWindow.map.mapObjects.addPlacemark() используется clusteredColletion.addPlacemark() , создаваемая с помощью mapView.mapWindow.map.mapObjects.addClusterizedPlacemarkCollection() . После добавления точек в кластер(-ы), вызываем функцию clusteredColletion?.clusterPlacemarks() для правильного отображения кластеров на карте.

final class YandexMapClusterSampleViewController: UIViewController < // Переменная коллекции-кластера private var clusteredColletion: YMKClusterizedPlacemarkCollection? = nil func viewDidLoad() < super.viewDidLoad() // Инициализация коллекции-кластера clusteredColletion = mapView.mapWindow.map.mapObjects.addClusterizedPlacemarkCollection(with: self) renderClusters() >// Фукнция для добавлении точки в кластер func addPlacemarkToCluster(point: YMKPoint) < guard let clusteredColletion = clusteredColletion else < return >// Добавление точки в кластер let viewPlacemark: YMKPlacemarkMapObject = clusteredColletion.addPlacemark(with: point) // Настройка и добавление иконки viewPlacemark.setIconWith( UIImage(named: "map_search_result_primary")!, // Убедитесь, что у вас есть иконка для точки style: YMKIconStyle( anchor: CGPoint(x: 0.5, y: 0.5) as NSValue, rotationType: YMKRotationType.rotate.rawValue as NSNumber, zIndex: 0, flat: true, visible: true, scale: 1.5, tappableArea: nil ) ) > // Фукнция для отображения кластера private func renderClusters() < self.clusteredColletion?.clusterPlacemarks(withClusterRadius: 60, minZoom: UInt(OutletMapView.DEFAULT_CAMERA_ZOOM)) >>

Чтобы кастомизировать визуальную часть (к примеру, как на скриншоте), нужно реализовать функцию, которая динамически создаёт новое изображение кластера в зависимости от количества точек, сгруппированных в нём. Это делается следующим образом:

extension YandexMapClusterSampleViewController: YMKClusterListener < func onClusterAdded(with cluster: YMKCluster) < cluster.appearance.setIconWith(clusterImage(cluster.size)) cluster.addClusterTapListener(with: self) >func clusterImage(_ clusterSize: UInt) -> UIImage < let scale = UIScreen.main.scale let text = (clusterSize as NSNumber).stringValue let font = UIFont.systemFont(ofSize: FONT_SIZE * scale) let size = text.size(withAttributes: [NSAttributedString.Key.font: font]) let textRadius = sqrt(size.height * size.height + size.width * size.width) / 2 let internalRadius = textRadius + MARGIN_SIZE * scale let externalRadius = internalRadius + STROKE_SIZE * scale let iconSize = CGSize(width: externalRadius * 2, height: externalRadius * 2) UIGraphicsBeginImageContext(iconSize) let ctx = UIGraphicsGetCurrentContext()! ctx.setFillColor(Asset.green.color.cgColor) ctx.fillEllipse(in: CGRect( origin: .zero, size: CGSize(width: 2 * externalRadius, height: 2 * externalRadius))); ctx.setFillColor(UIColor.white.cgColor) ctx.fillEllipse(in: CGRect( origin: CGPoint(x: externalRadius - internalRadius, y: externalRadius - internalRadius), size: CGSize(width: 2 * internalRadius, height: 2 * internalRadius))); (text as NSString).draw( in: CGRect( origin: CGPoint(x: externalRadius - size.width / 2, y: externalRadius - size.height / 2), size: size), withAttributes: [ NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: UIColor.black]) let image = UIGraphicsGetImageFromCurrentImageContext()! return image >>

Пример кода, который приближает камеру при нажатии на кластер:

extension YandexMapClusterSampleViewController: YMKClusterTapListener < func onClusterTap(with cluster: YMKCluster) ->Bool < mapView.mapWindow.map.move( with: YMKCameraPosition(target: cluster.appearance.geometry, zoom: 20, azimuth: 0, tilt: 0), animationType: YMKAnimation(type: YMKAnimationType.smooth, duration: 0,4), cameraCallback: completion ) return true >>

�� Рекомендую дополнительно реализовать интерфейс YMKMapCameraListener с его методом onCameraPositionChanged и хранить значение cameraPosition.zoom, чтобы можно было вычислять необходимый зум камеры и менять его без резких скачков.

Вы супер! Теперь карта визуально не перегружена множеством точек!

Множественное геокодирование

Если у точек нет заранее сохранённых координат, нужно искать их по адресу. В случае работы с одной-двумя точками запрашивать API Яндекса с клиента вполне обычное дело, но когда речь идёт о сотнях – или даже тысячах точек – разумным решением будет использовать множественное геокодирование на стороне сервера. Выполнять тысячи запросов с мобильного устройства невыгодно с точки зрения производительности и лимита запросов согласно лицензии.

Множественное геокодирование работает так: сервер получает список адресов, и по каждому делает запрос к HTTP Геокодеру Яндекса.

На момент написания статьи, Яндекс опубликовал всего один пост на эту тему (ещё в 2014 году). К сожалению, в своей документации Яндекс прилагает всего один пример и библиотеку, написанную для Node.js. Для применения множественного геокодирования в приложениях на других языках программирования придётся реализовывать вызовы к API вручную.

�� Для работы с HTTP Геокодером потребуется ключ разработчика для доступа к API JavaScript и Геокодеру.

Ниже приведён код, использующий библиотеку node-multi-geocoder .

Пример из GitHub не работал из-за параметра apikey , который был описан как key . Я проверил.

let geocoder = new MultiGeocoder(< provider: 'yandex', coordorder: 'latlong'>); let provider = geocoder.getProvider(); let getRequestParams = provider.getRequestParams; provider.getRequestParams = function() < let result = getRequestParams.apply(provider, arguments); result.apikey = "your-key-here"; return result; >let geoResponse = await geocoder.geocode([ "address 1", "address 2", "address 3", "address 4" ]);

Аналоги библиотеки

Нашёл репозиторий с аналогичным функционалом, написанном на C#:

Нюансы пользовательского соглашения

Пользоваться API Яндекс Карт можно бесплатно, но в таком случае запрещается сохранять результат работы геокодера. Другими словами, если искать координаты по адресу, то координаты сохранять к себе в базу данных запрещено.

Коммерческая лицензия позволяет снять это ограничение:

�� Коммерческая лицензия делится на стандартную (от 120 тыс. рублей в год) и расширенную (от 620 тыс. рублей в год). В стандартной лицензии запрещено сохранять или изменять данные, полученные с помощью API. Источник

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

Заключение

MapKit SDK Яндекс Карт для iOS открывает полезные возможности, которые важно уметь применять iOS разработчику в России. Документация Яндекса недавно обновилась, и теперь там можно найти описание классов и методов на Swift, но понятных примеров не так много. В своей статье я раскрыл основные подходы работы с MapKit от Яндекса и поделился личном опытом. Надеюсь, статья вам поможет!

Yandex MapKit для новичков: разрабатываем карты в Android-приложении

Yandex MapKit — это кроссплатформенная библиотека, позволяющая использовать картографические данные и технологии Яндекса в мобильных приложениях. Список доступных возможностей действительно впечатляет, но разработчику, впервые столкнувшемуся с необходимостью работать с Яндекс-картами, многое может показаться непонятным и неочевидным в использовании.

Поэтому, чтобы научиться применять полезные особенности MapKit’a, мы с вами напишем небольшое приложение, в которое внедрим и настроим данную библиотеку: откроем определённую область на карте; выставим метку в нужном месте; установим на неё желаемые растровые и векторные изображения; поиграемся с зумом; обработаем нажатие на пин; а также будем при клике визуально выделять объекты на карте и получать от них интересующую нас информацию.

1) Введение: внедрение и настройка Yandex MapKit в проекте

Чтобы создать и запустить приложение с Яндекс-картами, первым делом необходимо:

  • Получить ключ;
  • Установить библиотеку MapKit;
  • Настроить библиотеку;
  • Собрать и запустить приложение.

Эти пункты весьма понятно и доступно описаны в документации. Дабы не копировать информацию, читателям предлагается самим ознакомиться, повторить данные шаги и затем запустить приложение с Яндекс-картой. Отметим, что в нашем приложении мы будем использовать полную full-версию библиотеки (на момент написания статьи — версия 4.3.1-full), а писать код на языке Kotlin, используя View Binding.

Отметим несколько важных моментов:

  • Какими знаниями вы должны обладать: Kotlin базовый уровень; умение собрать проект, запустить приложение на эмуляторе или телефоне, загрузить необходимые библиотеки; View Binding.
  • Ознакомьтесь с условиями использования MapKit. Так, например, нельзя скрывать логотип Яндекса на карте за другими объектами. Также, в вашем приложении в разделе «о программе» должна быть ссылка на условия использования Яндекс-карт.
  • API-ключ должен быть задан единожды перед инициализацией MapKitFactory. Хорошим тоном будет задать ключ при запуске приложения в методе Application.onCreate() , а инициализировать уже в других необходимых активити и фрагментах. Если же при каких-то условиях будет повторно вызван MapKitFactory.setApiKey(«Ваш API-ключ»), вы получите краш приложения и ошибку в логах: «java.lang.AssertionError: You need to set the API key before using MapKit!». Примером появления подобной ошибки может быть следующий сценарий: переход с одного экрана во фрагмент с Яндекс-картами, где мы задаём API-ключ (карта при этом откроется и будет адекватно работать) -> возвращаемся на предыдущий экран -> вновь открываем фрагмент с картами -> происходит краш приложения.
  • Допущения в данном проекте: в случае, если по каким-то причинам (как, например, в нашем приложении), логика работы карт и API-ключ находятся в одном активити/фрагменте, раздувать макет необходимо только после того, как установлен API-ключ. Иначе, проявится ошибка, указанная в предыдущем пункте. Также необходимо учесть момент пересоздания активити/фрагмента, например, для случая изменения ориентации экрана, вследствие чего вновь будет вызван метод MapKitFactory.setApiKey(«Ваш API-ключ»). Воспользуемся проверкой: установили ли мы ранее API-ключ для Яндекс-карт. Для этого сохраним данную информацию:
override fun onSaveInstanceState(outState: Bundle)

А при создании активности, в методе onCreate будем проверять, был ли уже установлен API-ключ ранее при помощи функции setApiKey:

private fun setApiKey(savedInstanceState: Bundle?) < val haveApiKey = savedInstanceState?.getBoolean("haveApiKey") ?: false // При первом запуске приложения всегда false if (!haveApiKey) < MapKitFactory.setApiKey(MAPKIT_API_KEY)>> // API-ключ должен быть задан единожды перед инициализацией MapKitFactory 

Что же, надеюсь, у вас получилось собрать проект со своей первой Яндекс-картой и перед вами на экране отобразились материки, моря да океаны:

Земля в иллюминаторе, Земля в иллюминаторе, Земля в иллюминаторе видна.

А код выглядит примерно следующим образом:

class MainActivity : AppCompatActivity() < private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) < super.onCreate(savedInstanceState) setApiKey(savedInstanceState) // Проверяем: был ли уже ранее установлен API-ключ в приложении. Если нет - устанавливаем его. MapKitFactory.initialize(this) // Инициализация библиотеки для загрузки необходимых нативных библиотек. binding = ActivityMainBinding.inflate(layoutInflater) // Раздуваем макет только после того, как установили API-ключ setContentView(binding.root) // Размещаем пользовательский интерфейс в экране активности >private fun setApiKey(savedInstanceState: Bundle?) < val haveApiKey = savedInstanceState?.getBoolean("haveApiKey") ?: false // При первом запуске приложения всегда false if (!haveApiKey) < MapKitFactory.setApiKey(MAPKIT_API_KEY) // API-ключ должен быть задан единожды перед инициализацией MapKitFactory >> // Если Activity уничтожается (например, при нехватке памяти или при повороте экрана) - сохраняем информацию, что API-ключ уже был получен ранее override fun onSaveInstanceState(outState: Bundle) < super.onSaveInstanceState(outState) outState.putBoolean("haveApiKey", true) >// Отображаем карты перед моментом, когда активити с картой станет видимой пользователю: override fun onStart() < super.onStart() MapKitFactory.getInstance().onStart() binding.mapview.onStart() >// Останавливаем обработку карты, когда активити с картой становится невидимым для пользователя: override fun onStop() < binding.mapview.onStop() MapKitFactory.getInstance().onStop() super.onStop() >companion object < const val MAPKIT_API_KEY = "Ваш API-ключ" >>

Соответствующая разметка .xml:

Теперь, когда все готово, перейдём к ещё более интересным и полезным моментам – интерактивам с картой. Пример полного кода будет приведён в конце статьи, а также его можно найти в GitHub.

2) Открываем определённую область на карте

Первым делом сделаем так, чтобы при открытии карты нам сразу показывалась определённая область. Здесь и далее, при необходимости импорта, выбираем библиотеку Яндекса. Добавим новые переменные: startLocation – точку, содержащую координаты (широту и долготу), в которую должна переместиться камера и zoomValue – величину приближения к данной точке:

private val startLocation = Point(59.9402, 30.315) private var zoomValue: Float = 16.5f

Создадим функцию moveToStartLocation() , в которой у нашей mapview для карты вызовем метод, перемещающий камеру к необходимой позиции:

private fun moveToStartLocation()

Конструктор класса CameraPosition принимает, соответственно: точку с координатами, величину необходимого приближения, азимут и наклон (наклон камеры в градусах, добавляет визуально ощущение 3d). Максимальное возможное значение зума – 21.0f, минимальное – 0.0f. Также можем вызвать перегруженный метод move, чтобы перемещение в нужную область происходило красиво со стартовой анимацией:

binding.mapview.map.move( CameraPosition(startLocation, zoomValue, 0.0f, 0.0f), Animation(SMOOTH, 5f), null)

Здесь, помимо CameraPosition, дополнительно необходимо указать параметры анимации (которая зависит от типа animationType и длительности duration) и функцию CameraCallback , которая принимает логический аргумент, обозначающий завершение действия камеры. Если движение камеры по каким-то причинам прерывается, то в качестве аргумента передаётся «false»; если движение камеры завершилось успешно – «true». CameraCallback имеет необязательный тип, т.е. может быть не инициализирован – укажем здесь null.

Не забываем вызвать moveToStartLocation() в методе onCreate – и можем проверять работу перемещения камеры на стартовую локацию:

У моря над вольной Невой раскинулся каменный град - дворцов и соборов парад!

3) Устанавливаем метку на карте

Отметим нашу стартовую точку пином. Для начала загрузим в проект иконку с расширением png – скачайте файл ic_pin_png и перетащите его в папку drawable проекта.

Заранее создадим две переменные, которые проинициализируем и о которых более подробно расскажем далее:

private lateinit var mapObjectCollection: MapObjectCollection private lateinit var placemarkMapObject: PlacemarkMapObject

Реализуем функцию setMarkerInStartLocation() , отвечающую за установку метки на карте. В ней первым делом создаём ссылку на нашу картинку — marker. Затем инициализируем коллекцию различных возможных объектов на карте mapObjectCollection, которая может содержать любой набор элементов MapObject. MapObject – это пользовательский объект, отображаемый на карте, например, метка с иконкой или геометрическая фигура. Создадим такой геопозиционированный объект PlacemarkMapObject , являющийся наследником MapObject – метку с иконкой — который будет располагаться по координате, используемой ранее.

Для этого к mapObjectCollection применим метод addPlacemark , в котором укажем необходимую точку с координатами и, с помощью класса ImageProvider , ссылку на иконку. Таким образом мы создадим новую метку с нашим изображением и добавим её в текущую коллекцию. Если указать только точку с координатами, тогда получится пин со значком и стилем по умолчанию (будет выглядеть, как обычная точка на карте).

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

private fun setMarkerInStartLocation() < val marker = R.drawable.ic_pin_black_png // Добавляем ссылку на картинку mapObjectCollection = binding.mapview.map.mapObjects // Инициализируем коллекцию различных объектов на карте placemarkMapObject = mapObjectCollection.addPlacemark(startLocation, ImageProvider.fromResource(this, marker)) // Добавляем метку со значком placemarkMapObject.opacity = 0.5f // Устанавливаем прозрачность метке placemarkMapObject.setText("Обязательно к посещению!") // Устанавливаем текст сверху метки >

Вызываем setMarkerFirstOpen() в onCreate, запускаем приложение и наблюдаем метку над Эрмитажем.

Вот он - один из крупнейших в мире художественных и культурно-исторических музеев.

4) Использование векторных изображений

Обычно мы используем не png файлы, а векторные изображения. Тут имеется одна неприятная особенность: векторные изображения в качестве маркеров в MapKit не поддерживаются – они просто не будут отображаться. Но данный момент можно обойти. ImageProvider умеет работать с bitmap. Таким образом, нам стоит просто перевести векторное изображение в bitmap и использовать по назначению. Для этого создадим соответствующую функцию:

private fun createBitmapFromVector(art: Int): Bitmap?

Используем её: загрузите приложенные векторные изображения ic_pin_black_svg, ic_pin_blue_svg и ic_pin_red_svg. Затем добавьте их в свой проект: правой кнопкой по папке drawable -> Vector Asset -> Local file -> установите размер 64dp X 64dp -> Finish.

После этого в функции setMarkerInStartLocation() исправим marker и placemarkMapObject на:

val marker = createBitmapFromVector(R.drawable.ic_pin_black_svg) placemarkMapObject = mapObjectCollection.addPlacemark(startLocation, ImageProvider.fromBitmap(marker))

Таким образом мы можем использовать в качестве иконок для меток и векторные и растровые изображения.

5) Работа с зумом: меняем иконку маркера при отдалении и приближении камеры

Теперь обработаем моменты увеличения и уменьшения масштаба карты пользователем: пусть при пересечении некоторой границы величины зума маркер становится красным при отдалении камеры и синим при приближении. Данную величину границы зума сразу вынесем в константу в companion object:

const val ZOOM_BOUNDARY = 16.4f

Наследуемся от интерфейса CameraListener , который позволяет следить за обновлениями положения камеры:

class MainActivity : AppCompatActivity(), CameraListener 

и переопределяем метод onCameraPositionChanged , который срабатывает при изменении положения камеры:

override fun onCameraPositionChanged( map: Map, cameraPosition: CameraPosition, cameraUpdateReason: CameraUpdateReason, finished: Boolean ) <>

Здесь параметрами являются:

  • map - новая область карты (будьте внимательны при импорте – это не коллекция Map);
  • cameraPosition - текущее положение камеры;
  • cameraUpdateReason - причина обновления камеры. Это enum-класс, включающий в себя две константы: APPLICATION – т.е. причиной обновления камеры является вызов приложением метода move; GESTURES – причиной являются действия пользователя, такие как масштабирование, поворот и прочее;
  • finished – завершилось ли движение камеры окончательно. Будет «true», если камера закончила движение, «false» - в противном случае.

Обсудим логику работы изменения иконки метки при регулировании масштаба отображения карты. После того как пользователь успешно приблизил или отдалил карту, т.е. finished==true, проверим величину нового зума. Если величина зума cameraPosition.zoom будет меньше фиксированного значения ZOOM_BOUNDARY - мы поменяем изображение метки на красную иконку. Иначе, будет установлен пин синего цвета. Так же, чтобы каждый раз не производить излишнюю замену иконки при изменении масштаба пользователем, когда порог ZOOM_BOUNDARY не был преодолён, добавим дополнительную проверку: стала ли новая величина зума больше или меньше фиксированного значения? Для этого будем перезаписывать значение zoomValue после каждого изменения масштаба карты пользователем. В итоге имеем:

override fun onCameraPositionChanged( map: Map, cameraPosition: CameraPosition, cameraUpdateReason: CameraUpdateReason, finished: Boolean ) < if (finished) < // Если камера закончила движение when < cameraPosition.zoom >= ZOOM_BOUNDARY && zoomValue  < placemarkMapObject.setIcon(ImageProvider.fromBitmap(createBitmapFromVector(R.drawable.ic_pin_blue_svg))) >cameraPosition.zoom = ZOOM_BOUNDARY -> < placemarkMapObject.setIcon(ImageProvider.fromBitmap(createBitmapFromVector(R.drawable.ic_pin_red_svg))) >> zoomValue = cameraPosition.zoom // После изменения позиции камеры сохраняем величину зума > >

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

binding.mapview.map.addCameraListener(this)

Далековато будет! А так уже и рассмотреть все можно в мельчайших деталях

6) Обработка события нажатия на метку

Перейдем к обработке события нажатия на пин – выведем всплывающее окно Toast с надписью.

Для этого нам понадобится интерфейс MapObjectTapListener, как раз отвечающий за тапы по различным объектам на карте. Создадим экземпляр класса MapObjectTapListener и переопределим onMapObjectTap, который возвращает «true», если событие было обработано, иначе – «false». При этом, мы можем использовать информацию о mapObject’е и точке с координатами, по которой произошёл тап. Мы же просто воспользуемся Toast’ом с фиксированным текстом:

private val mapObjectTapListener = object : MapObjectTapListener < override fun onMapObjectTap(mapObject: MapObject, point: Point): Boolean< Toast.makeText(applicationContext, "Эрмитаж — музей изобразительных искусств", Toast.LENGTH_SHORT).show() return true >>

Интерфейс MapObjectTapListener может быть присоединён к любому MapObject’у. Подключим этот слушатель в функции setMarkerFirstOpen() к нашей метке placemarkMapObject :

placemarkMapObject.addTapListener(mapObjectTapListener)

Готово! Теперь, при тапе на метку, будет появляться всплывающее окно с необходимой информацией.

Осведомлён - значит вооружён!

Отдельно отметим, почему мы создаем отдельную переменную mapObjectTapListener , а не сразу пишем в функции setMarkerFirstOpen подобным образом:

placemarkMapObject.addTapListener(object : MapObjectTapListener < override fun onMapObjectTap(mapObject: MapObject, point: Point): Boolean< Toast.makeText(applicationContext, "Эрмитаж — музей изобразительных искусств", Toast.LENGTH_SHORT).show() return true>>)

Дело в том, что MapKit хранит слабые ссылки на передаваемые ему Listener-объекты, поэтому их необходимо сохранять на стороне приложения. Иначе, первое время клики будут работать адекватно, а затем перестанут реагировать на тапы. Это связано с тем, что сборщик мусора, который не учитывает связь ссылки и объекта в куче при выявлении объектов, подлежащих удалению, в какой-то момент удалит наш слушатель. После этого при тапе на пин в логах возникнет неприятное сообщение: «yandex.maps.runtime: Java object is already finalized. Nothing to do» и клики перестанут обрабатываться должным образом. Поэтому, необходимо использовать строгую ссылку на объект MapObjectTapListener, что мы и делаем выше.

Бонусные разделы

Весьма полезной и интересной темой в Mapkit является получения информации об объекте, используя поиск и метаданные. Поскольку наш туториал рассчитан на новичков, сейчас мы не будем подробно разбираться в том, как именно устроен поиск и как работать с метаданными в mapkit. Но, в качестве дополнительной полезной информации, без особой конкретики и не раскрывая темы в полном объёме, приведём примеры кода с использованием этих моментов в пунктах 7*) и 8*). Более подробно прочитать о данных возможностях Mapkit можно в статье «Поиск в MapKit: Tips & Tricks».

7) Выделение объекта на карте

Продолжим знакомство со слушателями объектов на карте. Теперь будем работать с GeoObject – объектом в слоях карты, примером которого может выступать здание или памятник. Слушателем будет выступать экземпляр класса GeoObjectTapListener, в котором требуется переопределить onObjectTap. Метод onObjectTap позволяет извлечь краткую информацию затронутого геообъекта и возвращает булевское значение. Так, если мы вернём «false» – тогда событие клика распространится на карту, и его сможет перехватить другой слушатель. Если «true» – «дальше» событие никуда не пойдёт. Пример: если пользователь клацает на объект, а в проекте имеется дополнительный слушатель, обрабатывающий действия по всей карте (как, например, в пункте 8), то при значении «true» для onObjectTap данное событие более перехвачено не будет.

Давайте подсветим какое-либо здание при тапе на него, иначе говоря, отобразим для пользователя «выделение» объекта. Для этого нам понадобятся идентификаторы объекта и слоя. Получим геообъект из geoObjectTapEvent методом getGeoObject. Информация про геообъект хранится в метаданных. Метаданные бывают разных видов, более подробно можно ознакомиться в «Поиск в MapKit: Tips & Tricks». Доступ к информации можно получить с помощью метода getMetadataContainer(), для которого с помощью метода getItem указываем ключ для этого контейнера - тип необходимых метаданных. У нас данный ключ – GeoObjectSelectionMetadata.

После того как мы получили метаданные, для карты mapview используем метод selectGeoObject, в который передаем необходимые идентификаторы объекта и слоя:

private val tapListener = object : GeoObjectTapListener < override fun onObjectTap(geoObjectTapEvent: GeoObjectTapEvent): Boolean < val selectionMetadata: GeoObjectSelectionMetadata = geoObjectTapEvent .geoObject .metadataContainer .getItem(GeoObjectSelectionMetadata::class.java) binding.mapview.map.selectGeoObject(selectionMetadata.id, selectionMetadata.layerId) return false >>

Подключаем данный слушатель:

binding.mapview.map.addTapListener(tapListener) // Добавляем карте слушатель тапов по объектам

Теперь, при клике на здание, оно выделится следующим образом:

Ага, теперь я тебя точно не потеряю из виду!

8) Получаем информацию об объекте при тапе на него

Осуществим следующую идею: при тапе в любую область карты будет появляться всплывающее окно с информацией об улице в данном месте. Если по каким-то причинам улицы нет, например, если тапнули по реке или не пришел ответ от сервера – будем выводить соответствующее сообщение.

В переопределённой функции onSearchResponse, последовательно используя различные вызовы, мы сможем докопаться до интересующей нас сущности – будь то улица, дом, маршрут, страна и прочее. А данную информацию можем использовать уже как угодно, например, поместить в Toast:

private val searchListener = object : Session.SearchListener < override fun onSearchResponse(response: Response) < val street = response.collection.children.firstOrNull()?.obj ?.metadataContainer ?.getItem(ToponymObjectMetadata::class.java) ?.address ?.components ?.firstOrNull < it.kinds.contains(Address.Component.Kind.STREET)>?.name ?: "Информация об улице не найдена" Toast.makeText(applicationContext, street, Toast.LENGTH_SHORT).show() > override fun onSearchError(p0: Error) < >>

Предварительно создадим две новые переменные: поисковую сессию searchSession и интерфейс для начала поиска searchManager, которые проинициализируем далее:

lateinit var searchManager: SearchManager lateinit var searchSession: Session

Теперь searchListener надо поместить в другой слушатель – inputListener, который обрабатывает тапы по всей карте. В нем переопределяются два метода – короткое и длинное нажатие. onMapTap - вызывается при быстром касании, если оно не было обработано геообъектами или объектами карты. Как уже говорилось ранее в пункте 7: если касание было обработано onObjectTap, то onMapTap может не сработать. Чтобы метод адекватно обработал тап – в onObjectTap необходимо вернуть «false». В самом методе onMapTap инициализируем поисковую сессию searchSession:

private val inputListener = object : InputListener < override fun onMapTap(map: Map, point: Point) < searchSession = searchManager.submit(point, 20, SearchOptions(), searchListener) >override fun onMapLongTap(map: Map, point: Point) <> >

Метод обратного поиска submit требует на вход точку с координатами, величину зума (в окрестности которой будет происходить поиск), настройки поиска и слушатель.

После, в методе onCreate инициализируем searchManager и добавляем к нашей карте слушатель тапов по карте с извлечением информации:

searchManager = SearchFactory.getInstance().createSearchManager(SearchManagerType.ONLINE) binding.mapview.map.addInputListener(inputListener) // Добавляем слушатель тапов по карте с извлечением информации

В итоге, после проведенных манипуляций, при клике на какое-либо место в городе на экране будет появляться название соответствующей улицы:

Окей, Яндекс, а где тут самые вкусные пышки, не подскажешь?

Заключение

Yandex MapKit предоставляет гигантское количество возможностей, с помощью которых можно реализовать самые различные интересные и полезные идеи. Сегодня мы познакомились с основными моментами использования Яндекс-карт: были показаны часто используемые методы, даны различные рекомендации и хитрости для проблемных мест, ответы на вероятно возникающие вопросы при первом знакомстве с данной библиотекой.

Данная статья не является исчерпывающей, наоборот - с помощью Yandex MapKit’а можно решить множество разнообразных задач: построить маршрут для пешей прогулки иль поездки на общественном транспорте; отобразить на карте ближайший к пользователю банкомат; найти нужную организацию; узнать о пробках на дорогах в реальном времени; предоставить местоположение пользователя; использовать панорамы; получить какую-либо информацию об объекте на карте и многое, многое другое.

Дополнительно ознакомиться с примерами реализации части функционала можно здесь.

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

Дерзайте! И помните: «Дорогу осилит идущий».

Список используемой литературы:

  • Руководство MapKit;
  • Условия использования MapKit;
  • Статья «Поиск в MapKit: Tips & Tricks»;
  • Примеры некоторых возможностей Mapkit;
  • Проект на Github.

Код программы

Код программы

class MainActivity : AppCompatActivity(), CameraListener < private lateinit var binding: ActivityMainBinding private lateinit var mapObjectCollection: MapObjectCollection // Коллекция различных объектов на карте private lateinit var placemarkMapObject: PlacemarkMapObject // Геопозиционированный объект (метка со значком) на карте private val startLocation = Point(59.9402, 30.315) // Координаты Эрмитажа private var zoomValue: Float = 16.5f // Величина зума lateinit var searchManager: SearchManager lateinit var searchSession: Session private val mapObjectTapListener = object : MapObjectTapListener < override fun onMapObjectTap(mapObject: MapObject, point: Point): Boolean < Toast.makeText(applicationContext, "Эрмитаж — музей изобразительных искусств", Toast.LENGTH_SHORT).show() return true >> private val geoObjectTapListener = object : GeoObjectTapListener < override fun onObjectTap(geoObjectTapEvent: GeoObjectTapEvent): Boolean < val selectionMetadata: GeoObjectSelectionMetadata = geoObjectTapEvent .geoObject .metadataContainer .getItem(GeoObjectSelectionMetadata::class.java) binding.mapview.map.selectGeoObject(selectionMetadata.id, selectionMetadata.layerId) return false >> private val searchListener = object : Session.SearchListener < override fun onSearchResponse(response: Response) < val street = response.collection.children.firstOrNull()?.obj ?.metadataContainer ?.getItem(ToponymObjectMetadata::class.java) ?.address ?.components ?.firstOrNull < it.kinds.contains(Address.Component.Kind.STREET) >?.name ?: "Информация об улице не найдена" Toast.makeText(applicationContext, street, Toast.LENGTH_SHORT).show() > override fun onSearchError(p0: Error) < >> private val inputListener = object : InputListener < override fun onMapTap(map: Map, point: Point) < searchSession = searchManager.submit(point, 20, SearchOptions(), searchListener) >override fun onMapLongTap(map: Map, point: Point) <> > override fun onCreate(savedInstanceState: Bundle?) < super.onCreate(savedInstanceState) setApiKey(savedInstanceState) // Проверяем: был ли уже ранее установлен API-ключ в приложении. Если нет - устанавливаем его. MapKitFactory.initialize(this) // Инициализация библиотеки для загрузки необходимых нативных библиотек. binding = ActivityMainBinding.inflate(layoutInflater) // Раздуваем макет только после того, как установили API-ключ setContentView(binding.root) // Размещаем пользовательский интерфейс в экране активности moveToStartLocation() // Перемещаем камеру в определенную область на карте setMarkerInStartLocation() // Устанавливаем маркер на карте binding.mapview.map.addCameraListener(this) // Добавляем карте слушатель камеры для слежки за изменением величины зума binding.mapview.map.addTapListener(geoObjectTapListener) // Добавляем слушатель тапов по объектам searchManager = SearchFactory.getInstance().createSearchManager(SearchManagerType.ONLINE) binding.mapview.map.addInputListener(inputListener) // Добавляем слушатель тапов по карте с извлечением информации >override fun onCameraPositionChanged( map: Map, cameraPosition: CameraPosition, cameraUpdateReason: CameraUpdateReason, finished: Boolean ) < if (finished) < // Если камера закончила движение when < cameraPosition.zoom >= ZOOM_BOUNDARY && zoomValue  < placemarkMapObject.setIcon(ImageProvider.fromBitmap(createBitmapFromVector(R.drawable.ic_pin_blue_svg))) >cameraPosition.zoom = ZOOM_BOUNDARY -> < placemarkMapObject.setIcon(ImageProvider.fromBitmap(createBitmapFromVector(R.drawable.ic_pin_red_svg))) >> zoomValue = cameraPosition.zoom // После изменения позиции камеры сохраняем величину зума > > private fun setMarkerInStartLocation() < val marker = createBitmapFromVector(R.drawable.ic_pin_black_svg) mapObjectCollection = binding.mapview.map.mapObjects // Инициализируем коллекцию различных объектов на карте placemarkMapObject = mapObjectCollection.addPlacemark(startLocation, ImageProvider.fromBitmap(marker)) // Добавляем метку со значком placemarkMapObject.opacity = 0.5f // Устанавливаем прозрачность метке placemarkMapObject.setText("Обязательно к посещению!") // Устанавливаем текст сверху метки placemarkMapObject.addTapListener(mapObjectTapListener) >private fun createBitmapFromVector(art: Int): Bitmap? < val drawable = ContextCompat.getDrawable(this, art) ?: return null val bitmap = Bitmap.createBitmap( drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888 ) ?: return null val canvas = Canvas(bitmap) drawable.setBounds(0, 0, canvas.width, canvas.height) drawable.draw(canvas) return bitmap >private fun moveToStartLocation() < binding.mapview.map.move( CameraPosition(startLocation, zoomValue, 0.0f, 0.0f), // Позиция камеры Animation(Type.SMOOTH, 2f), // Красивая анимация при переходе на стартовую точку null ) >private fun setApiKey(savedInstanceState: Bundle?) < val haveApiKey = savedInstanceState?.getBoolean("haveApiKey") ?: false // При первом запуске приложения всегда false if (!haveApiKey) < MapKitFactory.setApiKey(MAPKIT_API_KEY) // API-ключ должен быть задан единожды перед инициализацией MapKitFactory >> // Если Activity уничтожается (например, при нехватке памяти или при повороте экрана) - сохраняем информацию, что API-ключ уже был получен ранее override fun onSaveInstanceState(outState: Bundle) < super.onSaveInstanceState(outState) outState.putBoolean("haveApiKey", true) >// Отображаем карты перед тем моментом, когда активити с картой станет видимой пользователю: override fun onStart() < super.onStart() MapKitFactory.getInstance().onStart() binding.mapview.onStart() >// Останавливаем обработку карты, когда активити с картой становится невидимым для пользователя: override fun onStop() < binding.mapview.onStop() MapKitFactory.getInstance().onStop() super.onStop() >companion object < const val MAPKIT_API_KEY = "Ваш API-ключ" const val ZOOM_BOUNDARY = 16.4f >>

activity_main.xml

Также код можно посмотреть в GitHub.

Разбор библиотеки MapKit — SDK Яндекс.Карт для iOS

MapKit — это программная библиотека, которая позволяет использовать картографические данные и технологии Яндекса в мобильных приложениях. В этом видео вы узнаете как быстро подключить и использовать данную библиотеку на iOS устройствах.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *