Baidu Maps — китайский картографический сервис с собственным API. Он активно используется внутри КНР и поддерживает стандартные сценарии: отображение карты, маркеры, полигоны, кластеры.
На первый взгляд интеграция выглядит как обычно. Но при работе с реальными данными появляются отличия. Используется система координат BD-09. Мир не повторяется по долготе. Поведение геометрии отличается от Google Maps и Яндекс Карт.
Привет, я Екатерина Плаксина, фронтенд-разработчик в Далее. В этой статье разберу каждую проблему отдельно и поделюсь своими решениями.
Проблема #1 (лайт) — другая система координат и смещение объектов
Google или Яндекс Карты используют геодезическую систему координат WGS-84. Это стандарт для GPS. Данные можно сразу передавать в карту, а маркеры и полигоны отображаются там, где ожидаешь.
В Baidu Maps ситуация другая. У китайского сервиса — проприетарная система координат BD-09, которая построена поверх GCJ-02. Если передать в карту «обычные» GPS-данные, то объекты системно смещаются. Это касается маркеров, полигонов и любой геометрии. Смещение от реального положения заметно на всех масштабах.
В данном случае решение простое — конвертировать все входные координаты из WGS-84 в BD-09 перед передачей в Baidu Maps. Для этого можно использовать библиотеку coordtransform.
Проблема #2 — разрыв полигонов на границе мира
В привычных для нас картах мир по долготе повторяется. Если пользователь уходит за 180°, интерфейс продолжает показывать «следующий» мир. Полигоны, которые пересекают 180 меридиан, отображаются корректно.
В Baidu Maps нет удобного нативного wrap-world поведения для таких полигонов. Есть четкая граница по долготе, и соседний «повтор мира» не появляется автоматически. Если полигон пересекает 180 меридиан, часть геометрии оказывается за краем текущего мира и обрезается. Даже если точки в реальности рядом, движок воспринимает их как удаленные.
Например, на шве 180° ставим одну метку на 179.9°, а вторую — на -179.9°. Логически между ними всего 0.2°, но для Baidu Maps разница равна 359.8°. Если соединить такие точки «как есть», то движок проецирует путь полигона в виде огромного сегмента через всю карту или просто клиппит все, что выходит за пределы мира.


Решения вроде привести долготы к диапазону [-180, 180] или [0, 360], а также округлять значения возле 180° — не устраняют проблему. Они не меняют главного: Baidu не wrap’ает мир. Путь полигона все равно пересекает разрыв на 180°, если не переписать геометрию.
И вот тут, в отличие от координат, придется подойти к вопросу творчески.
1. Используем Canvas Overlay вместо нативных полигонов.
Уходим от стандартных polygon overlays и рисуем полигоны самостоятельно в одном canvas. Я реализовала кастомный BMapGL.Overlay, который:
создает <canvas> в панели карты через labelPane,
отключает pointer-events, чтобы не ломать зум и перетаскивание,
перерисовывает сцену в draw() по событиям карты.
Один canvas-overlay оказывается значительно дешевле по производительности, чем тысячи отдельных polygon overlays. Кроме того, мы сами контролируем, что и когда рисовать, не размножая сущности карты.
2. Режем полигоны по антимеридиану (180°).
Ключ к корректности — не пытаться рисовать кольцо, которое пересекает 180°, а сначала превратить его в набор «обычных» частей. Для этого:
анализируем ребра кольца;
находим пересечения с антимеридианом;
режем кольца на несколько частей, каждая из которых лежит по одну сторону шва;
стабилизируем долготы вблизи 180°, чтобы убрать дрожание из-за floating point.
В результате вместо «ломающегося» кольца получаем набор частей, которые уже можно безопасно проецировать и рендерить.
3. Ручной wrap-world: дорисовываем «повторяющийся мир» сами.
Поскольку Baidu Maps не повторяет мир, мы повторяем его в рендере:
определяем, сколько «миров» нужно нарисовать по горизонтали, чтобы покрыть текущий viewport;
рисуем основной полигон и его копии, сдвигая их на k * worldWidthPx.
Диапазон k подбираем по геометрии. Самый надежный способ — вычислить ширину мира через текущий viewport карты:
Берем горизонтальную линию на уровне центра карты и ставим две точки — на левом и правом краях карты.
Через map.pixelToPoint получаем долготы этих точек и определяем их диапазон, который покрывает текущий viewport.
Используем пропорцию. Текущая ширина карты в пикселях соответствует диапазону долгот, который сейчас виден на экране. Полный оборот Земли на 360° по долготе будет занимать пропорционально большее количество пикселей.
Так можно вычислить worldWidthPx — ширину одного «мира» на текущем масштабе карты.
4. Упрощаем геометрию по зуму в пикселях.
Чтобы не рисовать тысячи вершин на каждом обновлении, упрощаем контуры в пикселях. Допуск зависит от зума:
на дальних зумах: больше tol → меньше точек → быстрее
на ближних: меньше tol → больше деталей
5. Перерисовываем без лагов: rAF-планирование + события карты.
В финале планируем перерисовку через requestAnimationFrame, чтобы не запускать тяжелый draw десятки раз в секунду. Если рендер уже запланирован, следующий не ставим.
Готово! Сразу после того, как мы:
разрезали геометрию по шву,
спроецировали ее в «размотанные» пиксели,
дорисовали копии «мира» вручную,
отрендерили всё в одном canvas с оптимизациями по зуму…
…у нас получилась стабильная отрисовка полигонов на 180 меридиане в движке, где мир не повторяется 🥲 Полигоны больше не обрезаются на границе, не рисуют линию через весь мир и не лагают при большом количестве точек.
Проблема #3 — маркеры перекрываются даже на среднем масштабе
По умолчанию карта на нашем проекте открывается в масштабе страны — с большим количеством маркеров. Часть из них расположена близко друг к другу. Усложняет ситуацию сам формат: это не просто точки или пины, а названия мест и городов.
Текстовые элементы занимают заметно больше пространства на экране, чем обычные иконки. Это ведет к ряду проблем:
на плотных данных маркеры накладываются друг на друга уже на средних зумах;
интерфейс превращается в ковер из надписей — пользователь перестает понимать, что где находится;
тяжело кликнуть на нужный маркер среди перекрывающихся элементов.
В данной ситуации кластеризацию нельзя оставлять на будущее, она обязательная часть UX. И делать ее нужно так, чтобы кластер появлялся, когда маркеры визуально начинают мешать друг другу, а не по абстрактному расстоянию в метрах.
1. Переводим координаты в пиксели:
Берем гео-точку lng/lat.
Получаем ее пиксельную позицию через map.pointToPixel.
Узнаем реальный размер маркера width/height через measureMarkerBox.
Строим rect в пикселях, который занимает этот маркер на экране.
2. Группируем маркеры, чьи rect пересекаются, и создаем кластер.
На каждом пересчете готовим список кандидатов:
pxCenter — центр маркера в пикселях;
rect — границы маркера в пикселях с учетом размера и anchor.
Затем clusterByMarkerRect строит группы. На практике это похоже на итерацию по маркерам и «приклеиванию» их к существующему кластеру, если rect маркера пересекается с кластерным rect. Если не пересекаются — создается новый кластер с расширением bbox при добавлении маркера.
После объединения маркеров нужно:
обновить px bounds — пересчитать общие границы кластера в пикселях с учетом всех добавленных rect;
вычислить центроид — определить точку, в которую будет установлен маркер кластера.
В итоге мы получаем один компактный элемент с числом, которое показывает количество близко расположенных мест. И когда пользователь приблизится до оптимального масштаба, то получит легко считываемые названия.
Проблема #4 — сложности с доступом вне Китая
Во время разработки мы столкнулись с нестабильной загрузкой Baidu Map API. Скрипт карты периодически не подгружался для пользователей вне Китая. Причины могут быть разными:
особенности маршрутизации трафика,
кроссбордерные ограничения,
сетевые фильтры.
В результате карта просто не инициализируется и сложно проверить, как она ведет себя при реальной загрузке.
Как вариант — использовать удаленные машины в регионах со стабильным доступом. В первую очередь это:
Китай,
Гонконг,
Сингапур,
Южная Корея,
Тайвань.
Здесь Baidu обычно предсказуемый и стабильный.
Baidu Maps — не единственное решение
В Китае есть и другие картографические сервисы. Например, AMap (Gaode Maps) от Alibaba Group и Tencent Maps. Они также используются внутри страны и подходят для локализованных продуктов.
При этом у китайских картографических сервисов есть общая особенность — использование локальной системы координат GCJ-02. Детали работы движка могут отличаться, но полностью избежать инфраструктурных ограничений вряд ли получится.
Так что, вполне возможно, что все перечисленные проблемы окажутся универсальными.
Но даже хорошо знакомые нам карты имеют свои ограничения и нюансы. Просто мы к ним уже привыкли. С какими особенностями картографических API приходилось сталкиваться вам и как вы их решали?
