В данной статье я опишу как можно перенести камеру так, чтобы поведение было идентичным с такими картографическими приложениями как 2ГИС. Приложения Яндекс.Карты и 2ГИС используют чуть упрощенный подход, здесь он тоже описан. Но об этом позже.
Pre-requirements
Flutter 3.35.6, Dart 3.9.2 иdgis_mobile_sdk_map: ^13.0.0
Однако все базовые расчеты можно легко перенести на любой другой язык программирования. Суть этой статьи больше в понимании, что нужно сделать для решения этой задачи, нежели готовый код.
Тем не менее готовый код можно будет посмотреть тут.
Поставновка задачи
При нажатии на карте в зону, которая закрывается элементами управления или любыми другими виджетами, мы хотим, чтобы а) маркер на карте появился (то есть карта "нажимается" в этих областях) и б) маркер переместился в "безопасную" зону, в которой с ним легче работать: добавлять текст, иконки, он виднее и так далее. Назовем эту зону "сценой" или scene, а все оставшееся видимое пространство "область просмотра" или viewPort.
Важное условие задачи -- это должно работать и при повороте карты (bearing != 0).

То есть нам необходимо переместить точку на сцену. Для этого нам необходимо:
Узнать, в какой зоне находится точка
Переместить камеру при необходимости
У точки может быть 3 состояния: а) внутри сцены (зеленый), б) вне сцены, но внутри вьюпорта (желтый) и в) вне вьюпорта (красный)
Если точка находится внутри сцены, мы ничего не делаем.
Если точка находится во вне вьюпорта -- перемещаем точку на центр экрана (в качестве точки центра камеры выбираем координату маркера)
Иначе двигаем точку внутрь сцены на минимально необходимое расстояние
Таким образом вот наш алгоритм, по которому мы пойдем:
Соберем все вводные: размер экрана; паддинги, которые формируют сцену; позицию камеры;
Найдем координаты видимой области; проверим, что точка внутри этой области; если точка во вне этой зоны - перемещаем камеру на маркер и заканчиваем
Иначе высчитаем координаты сцены; проверим, точка внутри сцены или нет; Если точка вне сцены, но внутри видимой области, переместим ее, при необходимости, по оси
Oyи по осиOxИначе ничего не делаем, точка уже на сцене
1. Вводные

Мы должны заранее посчитать количество пикселей, которые занимают "рабочие" элементы по краям. Передать это можно объектом класса EdgeInsets
Так же мы передаем размер экрана через MediaQuery и позицию камеры через sdkMap.camera.position.point
2. Видимая область
Получение видимой области в 2ГИС MSDK достаточно простое:
final area = camera.visibleArea
Однако видимая область отдается точками с координамтами соответствующими углам экрана. Но если bearing != 0,то точки располагаются не в естественном порядке. Поэтому мы с помощью кастомной структурки CustomRect и метода fromArea преобразовываем точки из visibleArea в человекочитаемые и понятные для интерпретации и работы точки.
Далее, раз у нас есть координаты точек, мы можем посчитать, находится ли точка внутри этого прямоугольника. Вкратце, мы используем векторное произведение для понимания, с какой стороны находится наш маркер от каждой из сторон прямоугольника. Назовем этот метод _isInRectZone
Если этот метод возвращает false (то есть точка вне зоны), то мы перемещаем камеру на маркер в позицию markerPoint
if (!isInViewZone) {
camera.moveToCameraPosition(
newCameraPosition.copyWith(
point: markerPoint,
zoom: Zoom(16),
),
);
return;
}
Расчеты `_isInRectZone`
Метод _isInRectZone проверяет, лежит ли маркер P внутри повернутого прямоугольника сцены P1P2P3P4 (точки задаются по часовой стрелке).
1) Для каждой стороны считаются векторы
• сторона:
• к точке:
2) Вычисляется 2D векторное произведение:
3) Точка внутри, если все произведения имеют одинаковый знак:
В коде это сверяется выражением:
(cross1.sign + cross2.sign + cross3.sign + cross4.sign).abs() == 4
Такой тест опирается только на знаки и корректно работает при любом наклоне/повороте камеры, если вершины прямоугольника переданы в одном порядке (в CustomRect порядок фиксирован).

3. Сцена
Теперь посчитаем координаты сцены sceneRect. Для этого мы возьмем сцену в координатах [point1, point2, point3, point 4], возьмем размер экрана в пикселях, возьмем паддинг в пикселях и преобразуем это в проценты.
final paddingInPercents = EdgeInsets.only(
left: paddingInPixels.left / sizeOfScreen.width,
right: paddingInPixels.right / sizeOfScreen.width,
top: paddingInPixels.top / sizeOfScreen.height,
bottom: paddingInPixels.bottom / sizeOfScreen.height,
);
Затем проценты умножим на координаты, получим координаты новой области. Сами расчеты чуть сложнее, я их делал через вектора, но суть понятна: переводим пиксели в координаты.
Чуть духоты: мы можем использовать формулы для уточнения расстояния между двумя координатами, учитывая радиус Земли. Но для больших зумов это пренебрежимо мало, а на больших зумах можно либо не ставить точку, либо перемещать ее всегда в центр экрана. Однако для большей точности на малых зумах это можно было бы использовать.
Как только мы получили sceneRect, помещаем его в _isInRectZone. Если точка вне нашей сцены, то двигаем точку с помощью метода _movePoint
При перемещении точки я придумал такой алгоритм: мы узнаем, находится точка слева или справа от прямой, образованной стороной прямоугольника "сцены". С какой стороны находится точка мы узнаем по векторному произведению.
Если нужно двигать, мы узнаем расстояние от точки до прямой, умножаем это на нормальный вектор и прибовляем к результирующему вектору перемещения.
if (!isInSceneZone) {
final transferVector = _movePoint(markerPoint, sceneRect);
final newLat = cameraPositionPoint.latitude.value - transferVector.y;
final newLon = cameraPositionPoint.longitude.value - transferVector.x;
cameraPositionPoint = cameraPositionPoint.copyWith(
latitude: Latitude(newLat),
longitude: Longitude(newLon),
);
}
Расчеты
Метод _movePoint сдвигает маркер P внутрь сцены, если он выходит за любую из сторон прямоугольника P1P2P3P4 (точки идут по часовой стрелке). Для каждой стороны AB с соседней вершиной C вычисляется вектор смещения, затем все смещения суммируются.
1) Базовые векторы для стороны и соседней стороны:
,
— единичная нормаль вдоль следующей стороны.
2) Проверка положения точки относительно стороны:
Если (точка «внутри» относительно хода по часовой стрелке), сдвиг не нужен.
3) Если, расстояние от точки до линии
AB:
4) Вклад смещения для стороны:
5) Итоговый перенос:
по всем четырём сторонам; новая позиция камеры смещается на
-t (долготу и широту уменьшаем на t.x, t.y).
В коде предварительно все координаты умножаются на константу-фактор, чтобы повысить числовую устойчивость, но итоговая формула соответствует описанным шагам.

4. Точка там, где надо
В этом случае мы ничего не делаем.
Яндекс.Карты и Google Maps
Если нужно поведение как у других картографических приложений, то на пункте 3 мы не используем _movePoint, а просто делаем то же, что и в пункте 2 и перемещаем камеру маркер. Выглядеть это может как-то так:
if (!isInSceneZone) {
camera.moveToCameraPosition(
// Заметьте, что в данном случае мы оставляем Zoom тем же, чтобы не было рывков камеры
newCameraPosition.copyWith(
point: markerPoint,
),
);
return;
Заключение
Спасибо за то, что прочитали этот пост. Надеюсь, он будет вам полезен (или вашей LLM'ке). Буду благодарен за конструктивную критику.
Все материалы вы можете найти тут. Однако есть пара моментов, если вы захотите запутить проект. Нужно будет получить ключ dgissdk.key и положить его в папку assets в корне приложения + удалить vendorConfig:snapshot.data! из инициализатора sdkContext ??= sdk.DGis.initialize(vendorConfig: snapshot.data!);, если он вам не нужен. Файл vendor_config.json так же лежит в папке assets в виде json'а. Тут, думаю, разберетесь.
Документация по 2GIS SDK на Flutter тут