Как правильно высчитать угол у 2 точек на карте
Есть координаты 2 точек на карте ([x1, y1], [x2, y2]) Получены так Polygon.geometry.getCoordinates() Зеленой стрелкой показана прямая и из её двух точек получен угол в 63 градуса, хотя на глаз там должно быть около 45 градусов. Подскажите как мне верно рассчитать угол?
var c = myPolygon.geometry.getCoordinates(); var pointOne = c[0].reverse(); var pointTwo = c[1].reverse(); var offset = < x: pointTwo[0] - pointOne[0], y: pointTwo[1] - pointOne[1] >; var angle = Math.atan2(offset.y, -(offset.x)) * (180 / Math.PI) - 90; angle = angle < 0 ? angle + 360 : angle; angle = angle % 360; console.log(angle);
Отслеживать
56k 10 10 золотых знаков 83 83 серебряных знака 136 136 бронзовых знаков
задан 18 мар 2019 в 12:01
Иван Грозный Иван Грозный
не понятно совсем, про какие точки и угол идет речь
18 мар 2019 в 12:06
Неочевидно, о каком именно угле речь, но если судить по скриншоту на глаз, то угол между вертикалью и зеленой стрелкой составляет как раз около 60 градусов.
18 мар 2019 в 12:10
Есть точка A возьмем её за начало координат 0, 0 допустим. Вторая точка B лежит на воображаемой окружности ) И надо найти угол prnt.sc/mzi6u1
18 мар 2019 в 14:00
И фишка в том что я получаю координаты по которым градус угла не совсем верный
18 мар 2019 в 14:01
Покажите значения координат. По двум точкам угол найти нельзя, угол задаётся тремя точками. Вот азимут - можно.
18 мар 2019 в 15:24
1 ответ 1
Сортировка: Сброс на вариант по умолчанию
Если я Вас правильно понял вы ищите азимут.
Эта функция, про которую вы спрашиваете, вы ее уже использовали Math.atan2(y, x) , только она возвращает результат относительно оси абсцисс и против часовой стрелки, если говорить про угол на карте это скорее всего азимут и он откладывается от направления на север и по часовой стрелке; Для преобразования к этому углу необходимо то что вернул atan2 вычесть из направления на север (90 градусов относительно оси абсцисс или PI/2) и потом получившийся угол привести к положительному, если он отрицательный, т.к. отрицательный азимут особо не используется
azimuth(0, 0, 1, 1) azimuth(0, 0, 1, -1) azimuth(0, 0, -1, -1) azimuth(0, 0, -1, 1) function azimuth(x1,y1,x2,y2) < let a = 90 - Math.atan2(y2-y1, x2-x1)*180/Math.PI; if (a
Однако, это справедливо для плоскости, а карта - это проекция геоида на плоскость
Если Вам нужна математика на геоиде для javascript, рекомендую воспользоваться turf.js.
По поводу проекций:
Все существующие картографические проекции могут быть подразделены на классы по характеру искажений

Равноугольные проекции. На этих проекциях углы не искажаются, т. е. углы на местности между какими-либо направлениями равны углам на карте между теми же направлениями.
Бесконечно малые фигуры на карте в силу свойства равноугольности будут подобны тем же фигурам на Земле.
Если остров круглой формы в природе, то и на кар- те в равноугольной проекции он изобразится кружком некоторого радиуса.
Но линейные же размеры на картах этой проекции будут искажены.


Равновеликие проекции. На этих проекциях сохраняется пропорциональность площадей фигур, т. е. если площадь какого-либо участка на Земле в два раза больше другого, то на проекции изображение первого участка по площади тоже будет в два раза больше изображения второго.
Однако в равновеликой проекции не сохраняется подобие фигур.
Остров круглой формы будет изображен на проекции в виде равновеликого ему эллипса.
Произвольные проекции.
Эти проекции не сохраняют ни подобия фигур, ни равенства площадей, но могут иметь какие-нибудь другие специальные свойства, необходимые для решения на них определенных практических задач.
Наибольшее применение в судовождении из карт произвольных проекций получили ортодромические, на которых ортодромии (большие круги шара) изображаются прямыми линиями, а это очень важно при использовании некоторых радионавигационных систем при плавании по дуге большого круга.
Как определить азимут по карте
Это таинственное слово азимут. Далеко не сразу понял, как им пользоваться. По правде говоря, не понимал по причине ненадобности, лишь появление детей все изменило. После штудирования учебника по навигации и небольшой практики начал осознавать, как много современные люди теряют в этом суетливом, вечно бегущем, технологическом мире. Так давайте вспомним озорное детство, вперёд за сокровищами!

Что такое азимут
Представьте себе, что от вас отходит две линии. Одна - на север. Вторая - в то место, к которому идете. Угол между этими линиями - и есть азимут.
На картинке более наглядно изображено.

Азимуты бывают разные, например:
- астрономический;
- географический;
- магнитный;
- прямой;
- обратный.
Я перечислил только основные, есть и более специфические. К счастью, они нужны только в конкретных специальностях, а обычным людям можно глубоко не влазить в дебри.
Определение азимута по карте
Это самый легкий способ.
Чтобы определить азимут, необходимо знать своё текущее положение и положение цели.
Если вы знаете нужные координаты, то вычисление производится очень просто.
Ниже написал пошаговые действия для поиска азимута по карте:
- Отметить свое положение на карте.
- Отметить положение цели.
- Под линейку провести прямую через цель и текущее положение до пересечения с ближайшим меридианом с левой стороны.
- Замерить угол по часовой стрелке между проведенной прямой и меридианом.
Этот угол и есть истинный или, что то же самое, географический азимут.
Обратите внимание, что измерять угол нужно именно по часовой стрелке.

Как использовать полученный азимут
Но беда в том, что для путешествия истинный азимут малопригоден. Ведь магнитное поле неоднородно, и не везде стрелка компаса показывает на север.
Для устранения этой погрешности существует магнитное склонение, для каждой местности оно свое и указывается на картах.
Для перехода к магнитному азимуту, по которому будете путешествовать, есть формула №1: Ам=А-(±δ), (1),
где Ам - магнитный азимут (он и нужен); А - истинный азимут; δ - магнитное склонение.

Надеюсь, мой ответ был полезен. Удачи вам.
Яндекс Карты и React Native. Часть 1. Установка и примитивы
Начну с небольшой предыстории. Во время новогодних праздников мне нечем было особо заняться и я решил написать мобильное приложение, что-то вроде Zenly (социальная сеть, в которой в real-time можно увидеть, где сейчас находятся твои друзья), которое сейчас закрывает Snapchat. Решил делать это мобильное приложение на фреймворке js - React Native - для меня это казалось идеальным решением, так как такие языки как Kotlin или Swift я не особо уважаю, в силу того, что с ними ни разу не взаимодействовал. Первая задача с которой я столкнулся - это отображение карты в приложении и как на ней оставлять маркеры (в моём случае маркеры - это друзья в социальной сети). Эта тема на самом деле очень глубокая и копать в ней можно вплоть до написания своего картографического движка, но я решил воспользоваться чем-то из уже готовых решений. Из готовых решений с первым с чем я столкнулся - это интеграция Google Maps в приложение с помощью react-native-maps, однако, в современных реалиях Google не хочет предоставлять в Россию свои средства для разработчиков. Следующий вариант, который я рассматривал, это интеграция Mapbox в React Native. У Mapbox Maps SDK инструментарий мне понравился, там можно и стиль карты кастомизировать очень удобно, и весь нужный мне функционал присутствует. Однако, я всё же решил пойти дальше и наткнулся на Яндекс Карты для React Native. Они мне показались гораздо удобнее и легче в использовании в конкретно моём случае и я остановился на Яндекс Картах. В этой статье расскажу про начало работы с Яндекс Картами для React Native и базовые примеры того, что можно сделать с этой библиотекой.
Установка и инициализация Яндекс Карт для React Native
Установить библиотеку в проект можно с помощью менеджеров пакетов:
npm i react-native-yamap --save
yarn add react-native-yamap
Также, если у вас версия React Native ниже 0.60 необходима линковка библиотеки:
react-native link react-native-yamap
Далее переходим к инициализации библиотеки в наш проект. Для этого необходимо библиотеку импортировать и записать функцию инициализации, и передать в функцию строку с API-ключом, который можно получить на сайте кабинета Яндекс Разработчика.
import < YaMap, Marker >from 'react-native-yamap'; YaMap.init('ВАШ_API_КЛЮЧ');
И если вы пишете приложение для iOS, документация подсказывает, что необходимо инициализировать библиотеку в didFinishLaunchingWithOptions - функцию, которая находится в AppDelegate.m/AppDelegate.mm
#import . - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
Добавляем карту в приложение
Библиотеку мы инициализировали, теперь можно добавлять карту в приложение. Для этого воспользуемся компонентом карты. При добавлении карты наш компонент будет выглядеть как-то так:
export default function Habr() < return ( <> >); >
И что мы увидим, когда добавим компонент карты? Да вобщем-то ничего, он добавился, но для корректного отображения ему следует задать размеры. Для примера я сделал это как-то так. Наш компонент:
import MapStyles from './HabrStyles'; . export default function Habr() < return ( <> > >); >
Стили выглядят так:
import from 'react-native'; import < vw, vh >from 'react-native-expo-viewport-units'; export default MapStyles = StyleSheet.create(< map: < width: vw(100), height: vh(100), >, >)
Я для указания размеров использую viewport по старой привычке с css, однако, размеры там можно задавать как угодно.
Теперь у нас карта отобразилась и теперь наше приложение выглядит вот так:

Для компонента YaMap доступно использование пропсов, которые позволят настроить карту, как нам необходимо. Например, давайте сделаем, карту, на которой будет выключен показ текущей позиции пользователя, карта будет отображаться векторным изображением в ночном режиме и также я хочу отключить поворот камеры на карте. Также хочу, чтобы при открытии карты, камера находилась на координатах 30 по широте, 30 по долготе, уровень зума был выставлен на 7, а поворот по азимуту был выставлен на 0 градусов. Это будет выглядеть так:
export default function Habr() < return ( <> style=> >); >
Тогда при открытии карты мы видим следующее:

Добавляем примитивы на карту
Следующая задача с которой я столкнулся при разработке своего приложения, это отображение информации, в моём случае, пользователя, на карте. Нам доступно несколько примитивов с помощью которых мы может отображать ту или иную информацию на карте.
Конкретно для своей задачи я воспользовался компонентом Marker, который поместил в компонент YaMap. Давайте для примера на координатах 30 по широте и 30 по долготе установим маркер на нашей карте. Это будет выглядеть так:
export default function Habr() < return ( <> style= > > /> >); >
И тогда на экране мы увидим следующее:

Мы добавили совсем маленький, практически незаметный маркер, с помощью пропса увеличим его:
export default function Habr() < return ( <> style= > > scale= /> >); >

Вот так уже лучше. Но я хочу маркер как-то кастомизировать. Для этого можно с помощью пропса souce передать изображение в маркер, но мне интереснее другое, с помощью пропса children можно передать в маркер компонент, который собственно говоря у нас и будет отображаться на заданных координатах. Именно компонентом я хочу передавать изображение в маркер, потому что в случае если мы передаём ссылку на изображение без компонента, то если изображение имеет разрешение 100x100 пикселей, оно так и отображаться будет. Масштабировать можно при помощью scale, однако, тогда если мы отрисовываем много маркеров, у каждого из которого своё изображение и их надо как-то унифицировать, придётся задавать-подбирать scale для каждого отдельного маркера. Также я хочу добавить изображению обводку и сгладить углы.
export default function Habr() < return ( <> style= > source=> />> point=> /> >); >

Рассмотрим следующий примитив - Circle. Тут, я думаю, всё понятно из названия. Принимает он координаты центра окружности и её радиус. Также можно передавать другие параметры. Давайте допустим сделаем синий круг с тёмно-синей обводкой. Стоит также обратить внимание, что все примитивы принимают пропсом параметр zIndex, от которого будет завить, как они будут друг на друга накладываться.
export default function Habr() < return ( <> style= > source=> />> point=> zIndex= /> > radius= fillColor='#5789d9' strokeColor='#154ca3' strokeWidth= zIndex= /> >); >

Также существуют примитивы Polygon и Polyline, которые позволяют по точкам строить фигуры и линии соответственно. Стиль для этих примитивов задаётся также, как и для круга. Для примера давайте закрасим фигурой город Каир на карте, а также обозначим реку Нил линией.
export default function Habr() < return ( <> style= > source=> />> point=> zIndex= /> > radius= fillColor='#5789d9' strokeColor='#154ca3' strokeWidth= zIndex= /> , < lat: 30.070791, lon: 31.928108 >, < lat: 29.233658, lon: 31.228942 >, < lat: 29.983355, lon: 30.622998 >, ]> fillColor='#5789d9' strokeColor='#154ca3' strokeWidth= zIndex= /> , < lat: 29.364817, lon: 31.176281 >, < lat: 28.634771, lon: 30.862143 >, < lat: 27.490336, lon: 30.839704>, ]> fillColor='#5789d9' strokeColor='#154ca3' strokeWidth= zIndex= /> >); >

Думаю, общая концепция работы с примитивами на Яндекс Картах понятна. Также стоит отметить, что все они принимают в пропс onPress функцию, которая будет выполняться по нажатию на них и все эти примитивы необходимо импортировать перед использованием:
import < YaMap, Marker, Circle, Polygon, Polyline >from 'react-native-yamap';
Итог
В конце хочу сказать, что выше приведенные примеры, это совсем базовая работая с Yandex Maps SDK. Библиотека позволяет простраивать маршруты, использовать геокодер, в ней также можно настраивать стили для карты. Мне на эту тему есть что ещё сказать, как про Yandex Maps SDK, так и про Mapbox Maps SDK. Интересно было бы почитать комментарии, услышать, интересно это кому-то или нет, стоит дальше рассказывать про этот инструментарий или что-нибудь другое.
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.
