Всем привет! Я Влад, сооснователь MapMagicкаталога готовых маршрутов и планировщика для пеших походов и велопутешествий.

У нас есть как веб-версия так и приложения для Android и iOS.

Сердце нашего сервиса — карты. Для них мы используем базу OpenStreetMap, это Википедия в мире карт, которые рисуются и поддерживаются глобальным сообществом.

Проблемы с внешними провайдерами

На старте мы использовали готовые решения. Для мобильного приложения — векторные карты от MapTiler, для веба — стандартные тайлы OpenStreetMap и OpenTopoMap для топографического стиля.

Кратко о разнице: векторные карты — это когда сервер отдаёт сырые данные (дороги, здания, реки) в виде геометрий — точек, линий и полигонов, а клиент рисует их на лету. Плюсы — меньше трафика, можно менять стиль, есть 3D. Растровые карты — это когда сервер отдаёт готовые картинки. Плюсы — проще, менее зависимо от возможностей клиента, быстрее работает при большом количестве объектов.

Всё работало стабильно довольно долгое время, но недавно перестал загружаться из России OpenTopoMap. Для наших пользователей это означало, что в веб-планировщике пропал рельеф — изолинии, перепады высот — всё то, что нужно для планирования маршрута в горах.

Также недавно мы выпустили приложения и нам нужны были офлайн-карты для них. Рассматривали Mapbox и MapTiler. Но отказались от них по трём причинам:

  • Тарификация — запросы тарифицируются по отдельным тайлам. При активном использовании это быстро вышло бы за все разумные бюджеты.

  • Зависимость — не хотелось зависеть от стороннего поставщика карт для снижения риска блокировок. С MapTiler мы уже сталкивались с ситуациями, когда тайлы переставали отдаваться в определённых странах.

  • Кастомизация — хотелось больше возможностей для настройки карты под наши нужды. Векторные тайлы дают гибкость, но провайдеры ограничивают то, что можно менять.

Что нам было нужно

Из этих проблем мы сформулировали чёткие требования к собственным картам:

Требование

Почему это важно

Топографический стиль

Наши пользователи — туристы и велосипедисты. Им нужен рельеф, изолинии, перепады высот

Единый стиль для всех платформ

Карта в мобильном приложении должна выглядеть так же, как в веб-планировщике

Офлайн-режим

В горах и лесах часто нет связи — карты должны скачиваться заранее

Независимость

Полный контроль над инфраструктурой, никаких зависимостей от поставщиков карт

Обновляемость

Возможность обновить данные при изменениях в OSM

Важное требование — дублирование топонимов латиницей. Наши пользователи путешествуют по Грузии, Индии, Японии. Местные названия на грузинском, хинди или японском не читаются, поэтому там, где это возможно, мы дублируем названия латиницей.

Архитектура решения

Чтобы удовлетворить эти требования, мы спроектировали собственный картографический пайплайн:

┌─────────────────────────────────────────────────────────────┐
│                    Источники данных                         │
├─────────────────────────────────────────────────────────────┤
│  • OpenStreetMap (.osm.pbf)                                 │
│  • DEM (цифровая модель рельефа)                            │
│  • Границы, береговые линии, городские агломерации          │
└─────────────────────────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│                   Генерация тайлов                          │
├─────────────────────────────────────────────────────────────┤
│  • Tilemaker — генератор векторных тайлов                   │
│  • Генерация изолиний из DEM                                │
│  • Конвертация в PMTiles                                    │
└─────────────────────────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│                     Хранение                                │
├─────────────────────────────────────────────────────────────┤
│  • Object Storage (S3)                                      │
│  • Формат: PMTiles                                          │
└─────────────────────────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│              Tile Server (Martin)                           │
├─────────────────────────────────────────────────────────────┤
│  • Отдача векторных тайлов                                  │
│  • Кеширование                                              │
└─────────────────────────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│         Рендеринг растровых тайлов                          │
├─────────────────────────────────────────────────────────────┤
│  • Рендеринг векторных тайлов в картинки (tileserver-gl)    │
│  • Балансировка и кеширование отрендеренных тайлов          │
└─────────────────────────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│                   Клиенты                                   │
├─────────────────────────────────────────────────────────────┤
│  • MapLibre GL JS (веб, векторные карты)                    │
│  • Leaflet (веб, растровые карты)                           │
│  • Mapbox SDK (мобильное)                                   │
└─────────────────────────────────────────────────────────────┘

Ключевые решения:

  • Данные — OpenStreetMap + DEM от mapterhorn

  • Формат — PMTiles (оптимизирован для S3, один файл на регион вместо тысячи мелких файлов)

  • Tileserver — Martin для векторных тайлов (быстрый, на Rust), tileserver-gl для растровых

  • Стиль — MapLibre Style Specification (работает и в вебе, и на мобильных устройствах)

Источники данных

Наш пайплайн использует несколько источников данных — комбинируем их для получения полных топографических карт.

OpenStreetMap

Основной источник данных — OpenStreetMap. Мы скачиваем .osm.pbf дампы с Geofabrik. Это формат Protocol Buffers, в котором хранятся данные OSM: дороги, здания, реки, границы, POI — вообще всё, что вносят участники проекта.

Цифровая модель рельефа

Для топографических карт критически важен рельеф — изолинии, hillshade, пере��ады высот. В OSM этой информации нет, поэтому используем открытые источники DEM (Digital Elevation Model).

Мы используем данные mapterhorn — открытый источник тайлов с высоким разрешением рельефа в формате terrarium: каждый пиксель WebP изображения кодирует высоту в метрах.

Альтернативные источники, которые мы рассматривали:

  • GEDTM30 — глобальная DEM с разрешением 30м

  • viewfinderpanoramas.org — SRTM и NED данные

Однако, с этими источниками возникло много проблем, и наилучший результат удалось получить именно с использованием mapterhorn.

Дополнительные данные

Одного OSM и рельефа недостаточно для хорошей топографической карты. Мы используем дополнительные источники:

Границы — нужны нам, чтобы не дублировать латиницей названия, находящиеся в пределах России. Используем открытый набор geoBoundaries: geoBoundaries-RUS-ADM0.geojson.

Береговые линии — нужны для корректной отрисовки границ суши и воды. Берём из OpenStreetMap data.

Городские агломерации — нужны для корректной отрисовки городов на низких зумах. Берём из Natural Earth.

Приоритизация вершин — используем запросы к Overpass API для приоритизации отображения горных вершин. На низких масштабах карты не все вершины нужно показывать — выбираем самые значимые в регионе.

Генерация тайлов с Tilemaker

Для превращения всех этих данных в векторные тайлы мы используем собственный доработанный форк Tilemaker — быстрый и гибкий генератор тайлов на C++.

Почему Tilemaker?

  • Скорость — обрабатывает planet.osm за несколько часов

  • Гибкость — Lua скрипты для кастомной логики обработки

  • Прост в доработке — довольно несложно доработать под свои задачи, что мы и сделали

Конфигурация

Tilemaker требует два файла конфигурации (базовые примеры есть в репозитории tilemaker):

config.json — описание слоёв и их свойств (упрощенный пример):

{
  "layers": [
    { "name": "water", "minzoom": 0, "maxzoom": 14 },
    { "name": "roads", "minzoom": 0, "maxzoom": 14 },
    { "name": "buildings", "minzoom": 13, "maxzoom": 14 }
  ]
}

process.lua — правила обработки OSM объектов. Здесь вся логика: какие теги сохранять, как классифицировать дороги, что делать с топонимами. Lua даёт полную гибкость — можно написать любую логику обработки.

Изолинии

Без изолиний них невозможно понять рельеф: где подъём, где спуск, насколько круто.

Изначально предпринимались попытки заранее создать тайлы изолиний с помощью программы gdal_contour по открытым данным viewfinderpanoramas с использованием руководства от OpenTopoMap. Однако, качество результата не устраивало нас, а генерация изолиний с шагом в 5 и 10 м для всей планеты занимала огромное количество времени и места в хранилище.

Мы вновь обратили внимание на mapterhorn, который уже использовали для отрисовки рельефа-отмывки. Для рендеринга изолиний на карте создатели mapterhorn предлагали использовать клиентский плагин maplibre-contour. Мы адаптировали этот плагин для работы на сервере. Таким образом, генерацию изолиний мы делаем на лету — не храним их заранее, а создаём по запросу. Это даёт гибкость: можно менять параметры (шаг, единицы измерения) без перегенерации всего набора данных.

  • При запросе тайла изолиний сервер загружает соответствующие DEM тайлы с mapterhorn

  • Декодирует RGB значения в высоты

  • Алгоритмом marching squares генерирует изолинии

  • Кодирует в векторный тайл (MVT)

  • Кеширует результат

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

Зум

Шаг изолиний

11

100м

12

50м

13

20м

14

10м

16

В итоге получился такой результат

Эльбрус, детальный вид
Эльбрус, детальный вид

Дублирование топонимов латиницей

Как я писал выше, важное требование для нас — дублирование латиницей топонимов в Грузии, Индии, Японии.

Реализуем это в process.lua:

function SetNameAttributes()
	local name = Find("name")
	local name_ru = Find("name:ru")
	local name_en = Find("name:en")

	if name_en == "" or (name_en ~= "" and name:find(name_en, 1, true)) or (name_ru ~= "" and name:find(name_ru, 1, true)) then
		Attribute("name", name)
		return;
	end	

	if Intersects("ru_boundaries?") then 
		Attribute("name", name); 
		return;
	end

	Attribute("name", name.."\n"..name_en)
end

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

Запуск генерации

Базовая команда генерации:

tilemaker input.osm.pbf output.mbtiles \
  --config config.json \
  --process process.lua

Что получилось

Эльбрус, общий вид
Эльбрус, общий вид
Вид лесных троп и дорог
Вид лесных троп и дорог

Теперь у нас есть:

  • Топографические карты с рельефом, которые работают везде

  • Единый стиль для веба и мобильных приложений

  • Офлайн-карты в приложениях

  • Полный контроль над данными и инфраструктурой

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

Посмотреть получившуюся карту можно в вебе и в приложениях.


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