Тайлы
В этой главе мы рассмотрим
- понятие тайлов
- PostgreSQL + PostGIS
- Martin
- картографирование с помощью векторных тайлов
В рамках практической части создадим карту ойконимов (названий населённых пунктов) Московского региона по векторным тайлам, генерирующимся на лету приложением Martin из базы данных PostGIS. При желании посмотрите полный код и возможный результат.
Что такое тайлы
Заголовок раздела «Что такое тайлы»В прошлом упражнении мы сами разработали бэкенд для передачи пространственных данных из базы в браузер пользователю. Чаще для таких операций пользуются готовыми инструментами и устоявшимися подходами. Одним из таких подходов является применение тайлов.

Пространственные данные могут быть большими по объёму. Если пользователь хочет посмотреть веб-карту, передавать ему все данные сразу (а это могут быть гигабайты) не нужно. Это приведёт к длительной загрузке веб-страницы, избыточному трафику, медленной работе веб-карты или падению браузера.
Данные можно поделить на кусочки и передавать пользователю только нужные кусочки с нужной детальностью. Данные можно поделить на кусочки по-разному. Пространственные данные ожидаемо удобно делить на географические кусочки — тайлы. Нужные кусочки — те, что попадают на экран. Нужная детальность — та, что соответствует текущему масштабу карты.
Стандартная система тайлов делит планету на квадраты X/Y для каждого уровня зума Z. Каждый тайл имеет индекс Z/X/Y. По этому индексу и выполняются запросы тайлов. Так формируется API сервиса векторных тайлов. Когда запрос выполняется, вызывается серверная функция, формирующая тайл, или возвращается заранее рассчитанный (кэшированный) тайл.
Тайл привязан к глобальной системе координат одной точкой. Геометрии внутри тайла хранятся во внутренней системе координат тайла. Тайлы бывают векторными и растровыми. В векторных тайлах содержание одного тайла составляют точки, линии и полигоны, особым образом кодируются атрибуты. В растровых тайлах содержание одного тайла составляют пиксели.
Растровые тайлы можно использовать как для растровой модели данных (снимков, ЦМР, индексных изображений), так и для векторной, когда на тайлы будет нарезаться подготовленное изображение карты. Векторные тайлы оказываются удачным решением для векторных наборов данных.
Использование тайлов
Заголовок раздела «Использование тайлов»Познакомимся c возможностями практического применения тайлов. Для этого сделаем следующее.
- Загрузим наборы пространственных данных в базу пространственных данных.
- Подключим к ней сервер векторных тайлов.
- Получим векторные тайлы на клиентской стороне веб-приложения средствами картографической JavaScript-библиотеки.
База пространственных данных
Заголовок раздела «База пространственных данных»Используем сервер баз данных Postgres с расширением для пространственных данных PostGIS.
Установка
Заголовок раздела «Установка»Дистрибутив для сервера баз данных загрузим с сайта PostgreSQL. При установке следует обратить внимание на порт, который будет занимать сервер баз данных, и пароль для пользователя postgres.
Сервер баз данных запускается локально. Доступ к локальному серверу баз данных осуществляется по заданному при установке порту — обычно 5432. Если установщик предлагает другой порт, возможно, сервер баз данных уже установлен на компьютер и занимает этот порт.
После установки сервера баз данных Postgres можно установить расширение для работы с пространственными данными PostGIS. Дистрибутив доступен на сайте расширения.
На Windows наблюдаются проблемы с установкой PostGIS через StackBuilder. Рабочим вариантом является самостоятельная загрузка дистрибутива PostGIS с сайта OSGeo. Найдите папку с версией Postgres, которую установили. Например, для Postgres 17 нужна папка
pg17/, в папке находится дистрибутивpg17/postgis-bundle-pg17x64-setup-3.5.0-2.exe.
Создание базы данных
Заголовок раздела «Создание базы данных»Вместе с сервером базы данных устанавливается графический интерфейс для работы с базами данных PgAdmin 4.

При необходимости можно загрузить только дистрибутив PgAdmin и установить графическую оболочку отдельно от сервера баз данных.
Через этот интерфейс мы создадим базу данных на нашем локальном сервере.

Назовём базу данных oikonyms. Остальные параметры оставим по умолчанию.

И добавим к созданной базе расширение postgis.


База пространственных данных создана. Пора её наполнить!
Загрузка данных
Заголовок раздела «Загрузка данных»Грузить в базу будем данные об ойконимах Московского региона:
Здесь используем данные, которые Яндекс опубликовал в рамках интересного исследования 2021 года о населённых пунктах на карте России.
Сделаем это через QGIS.

Добавим слои на карту.

Выполним подключение к базе данных.

Назовём подключение oikonyms-database. Наша база размещена на локальном сервере localhost, занимает порт 5432, а называется oikonyms. Используем базовую аутентификацию (Authentication - Basic), где укажем логин и пароль.

Увидим нашу базу данных в списке подключений. Словом public обозначена схема базы данных, которую мы будем заполнять. Она создаётся для базы данных по умолчанию.

Перенесём слои с карты в базу данных. Это можно сделать прямо из списка слоёв.

Чтобы проверить, что данные успешно загружены в базу, выполним следующие действия:
- Обновим подключение
oikonyms-database, нажав на него правой кнопкой мыши и выбрав в контекстном меню опцию “Обновить” - В схеме данных
publicнайдём загруженный набор пространственных данных - Перенесём его на карту и удостоверимся, что геометрия и атрибуты отображаются корректно
Векторные тайлы
Заголовок раздела «Векторные тайлы»Мы не будем обращаться к базе данных напрямую. Мы добавим прослойку, которая будет формировать векторные тайлы из пространственных данных, хранящихся в базе.
Сервер векторных тайлов
Заголовок раздела «Сервер векторных тайлов»Для подготовки векторных тайлов используем сервер векторных тайлов Martin. Загрузим это приложение со страницы релиза.
Распакуем архив и запустим сервер векторных тайлов, указав подключение к локальной базе данных, куда мы загрузили ойконимы. Чтобы это сделать, надо запустить терминал в папке, где распакован исполняемый файл Martin, например, martin.exe. И выполнить команду, которая в общем виде будет выглядеть так.
martin postgresql://postgres:password@localhost:5432/oikonymsА в Windows приобретёт такой вид.

После успешного выполнения увидим это.

На последней строчке увидим, что каталог слоёв доступен по адресу http://0.0.0.0:3000/catalog, то есть по IP-адресам компьютера. Мы воспользуемся нашим любимым localhost (другие обычно и недоступны).
Перейдём по адресу localhost:3000/catalog, чтобы увидеть доступные наборы векторных тайлов. Их должно быть два по количеству пространственных таблиц в базе данных.
По адресам localhost:3000/grid и localhost:3000/oikonyms доступны описания наборов векторных тайлов в формате TileJSON. Наиболее существенным в нём является указание адреса, по которому доступны векторные тайлы — localhost:3000/grid/{z}/{x}/{y}
/catalog,/grid,/grid/{z}/{x}/{y}— это всё эндпоинты API, которые для нас автоматически формирует Martin. Он же выполняет нужные серверные функции, за счёт которых мы получаем ответы, обращаясь к этим эндпоинтам. И ничего не пришлось писать самим, как в прошлом упражнении!
При обращении {z}/{x}/{y} заменяются на индекс запрашиваемого тайла, например, 0/0/0 для запроса тайла на весь мир при уровне зума 0 или 1/1/0 для тайла в верхнем правом углу при уровне зума 1. Некоторые тайлы могут быть пустыми.
Когда тайл запрашивается, сервер векторных тайлов выполняет серверную функцию, которая вырезает соответствующий кусочек из исходного набора пространственных данных. Сервер векторных тайлов Martin обеспечивает подготовку тайла за счёт запроса к базе данных, то есть вырезанием и кодированием атрибутов и геометрии объектов, попадающих в тайл, занимается PostGIS.
Векторные тайлы на карте
Заголовок раздела «Векторные тайлы на карте»Остаётся принять эти векторные тайлы на карте. Заготовку для карты формируем как обычно.
Добавляем источник пространственных данных и картографический слой. При добавлении источника пространственных данных указываем тип vector, а не geojson, как в прошлых картах.
При добавлении слоя указываем source-layer — векторный тайл может содержать несколько слоёв. В нашем случае слой только один, посмотреть на него можно в TileJSON-описании localhost:3000/grid, где идентификаторы слоёв указываются в обязательном списке vector_layers.

map.addSource("grid", { type: "vector", url: "http://localhost:3000/grid",})map.addLayer({ id: "grid-layer", source: "grid", "source-layer": "grid", type: "fill", paint: {}})При добавлении источника вместо url TileJSON-файла мы можем указать tiles — список адресов, по которому можно выполнять запросы к тайлам. Если мы указываем url, MapLibre самостоятельно находит этот список в TileJSON.

map.addSource("oikonyms", { type: "vector", tiles: ["http://localhost:3000/oikonyms/{z}/{x}/{y}"],})map.addLayer({ id: "oikonyms-layer", source: "oikonyms", "source-layer": "oikonyms", type: "circle", paint: {}})Веб-карта
Заголовок раздела «Веб-карта»Подключили источники данных. Теперь пора заняться визуализацией.
Оформление слоёв
Заголовок раздела «Оформление слоёв»Слой с сеткой шестиугольников раскрасим интерполяцией цвета по полю численности населения в ячейке.
Обратите внимание, поле
sum_popвнутри карты является текстом, хотя в базе и векторном тайле указан числовой формат. Такое бывает. Проверить объект внутри карты всегда можно функциейmap.on("click", "grid-layer", (e) => console.log(e.features))
map.addLayer({ id: "grid-layer", source: "grid", "source-layer": "grid", type: "fill", paint: { "fill-color": [ "interpolate", ["linear"], ["to-number", ["get", "sum_pop"]], 0, "#440154", 100, "#39568c", 1000, "#1f968b", 10000, "#fde725" ] }})Слой с точками населённых пунктов покажем кружками с обводкой.
map.addLayer({ id: "oikonyms-layer", source: "oikonyms", "source-layer": "oikonyms", type: "circle", paint: { "circle-color": "#1a9641", "circle-radius": 6, "circle-stroke-width": 1, "circle-stroke-color": "#FFF", "circle-opacity": 0.8 }})Мультимасштабное содержание
Заголовок раздела «Мультимасштабное содержание»На картах можно менять уровень приближения. Картограф должен озаботиться тем, чтобы содержание на каждом уровне было визуально понятным и приятным.
Точки ойконимов в мелком масштабе накладываются друг на друга и закрывают шестиугольники. Не будем показывать их до 9-го уровня зума.
map.addLayer({ id: "oikonyms-layer", source: "oikonyms", "source-layer": "oikonyms", type: "circle", paint: { "circle-color": "#1a9641", "circle-radius": 6, "circle-stroke-width": 1, "circle-stroke-color": "#FFF", "circle-opacity": 0.8 }, minzoom: 9})Также ограничим возможности максимального отдаления и приближения веб-карты. Другими словами, установим минимальный и максимальных масштаб карты. При этом наибольшую степень приближения зафиксируем как максимальный зум maxZoom, а наибольшую степень отдаления как максимальный охват карты maxBounds.
const map = new maplibregl.Map({ container: "map", style: "https://raw.githubusercontent.com/gtitov/basemaps/refs/heads/master/voyager-nolabels.json", center: [37, 55], zoom: 6, maxZoom: 11, maxBounds: [[25, 50], [50, 60]], hash: true,})Подлёт при клике
Заголовок раздела «Подлёт при клике»Подскажем пользователю, что содержание карты является мультимасштабным.
При клике на ячейку сетки подлетим к точке клика на 10 уровень зума, при котором отображаются ойконимы внутри ячейки. Чтобы подчеркнуть доступное интерактивное действие, используем изменение курсора.
map.on("click", "grid-layer", (e) => { map.flyTo({ center: e.lngLat, zoom: 10 })})
map.on("mouseenter", "grid-layer", () => { map.getCanvas().style.cursor = "pointer"})
map.on("mouseleave", "grid-layer", () => { map.getCanvas().style.cursor = ""})Подсветка при наведении
Заголовок раздела «Подсветка при наведении»Изменение курсора при наведении на объект намекает на доступное интерактивное действие. Но ещё лучше его наличие подчеркнёт подсветка объекта при наведении курсора. Воспользуемся вариантом подсветки объекта с помощью его обводки.
Сначала зафиксируем объект, на котором находится курсор.
Назначим поле id из свойств слоя идентификатором объектов слоя.
map.addSource("grid", { type: "vector", url: "http://localhost:3000/grid", promoteId: "id"})Присвоим объекту состояние hover: true, когда курсор попадает на объект, и hover: false, когда курсор переходит на другой объект или покидает слой.
let hoveredFeatureId = null;
map.on("mousemove", "grid-layer", (e) => { if (hoveredFeatureId !== null) { // последнему назначенному объекту // присваиваем состояние `false` map.setFeatureState( { source: "grid", sourceLayer: "grid", id: hoveredFeatureId }, { hover: false } ) } // назначаем текущий объект hoveredFeatureId = e.features[0].id // текущему объекту присваиваем состояние `true` map.setFeatureState( { source: "grid", sourceLayer: "grid", id: hoveredFeatureId }, { hover: true } )})
// когда курсор покидает слойmap.on("mouseleave", "grid-layer", () => { // последнему назначенному объекту // присваиваем состояние `false` map.setFeatureState( { source: "grid", sourceLayer: "grid", id: hoveredFeatureId }, { hover: false } )})На основе состояния объекта делаем обводку.
Объекты, которые курсор не трогал, не будут иметь состояния
hoverвообще. Для таких объектов через выражениеbooleanмы присваиваем значениеfalse, чтобы они корректно обработались в выраженииcase.
map.addLayer({ id: "grid-layer", source: "grid", "source-layer": "grid", type: "fill", paint: { "fill-color": [ "interpolate", ["linear"], ["to-number", ["get", "sum_pop"]], 0, "#440154", 100, "#39568c", 1000, "#1f968b", 10000, "#fde725" ], "fill-outline-color": [ "case", ["boolean", ["feature-state", "hover"], false], "cyan", "transparent" ] }})Изменение курсора и подсветка ячейки ясно указывают пользователю, что на ячейку можно кликнуть и что-то произойдёт. В нашем случае, мы подлетим к карте до уровня видимости ойконимов.
Попап при наведении
Заголовок раздела «Попап при наведении»При наведении на пунсоны ойконимов выведем их названия в попап.
const popup = new maplibregl.Popup({ closeButton: false, closeOnClick: false});
map.on("mouseenter", "oikonyms-layer", (e) => { popup .setLngLat(e.features[0].geometry.coordinates) .setHTML(e.features[0].properties.name) .addTo(map);});
map.on("mouseleave", "oikonyms-layer", () => { popup.remove();});Интерактивная фильтрация
Заголовок раздела «Интерактивная фильтрация»При использовании векторных тайлов можно организовать интерактивную фильтрацию.
Для этого добавим элемент, в который пользователь будет вводить граничное значение для численности населения.
<body> <div id="map"></div> <input type="number" id="filter" value="12000000"/> <label for="filter">Фильтр по населению</label> <script src="main.js"></script></body>#map { position: absolute; top: 0; bottom: 0; left: 0; right: 0; z-index: -1;}И запрограммируем реакцию карты на ввод значений.
document.getElementById("filter").addEventListener("input", (e) => { filterValue = parseInt(e.target.value) map.setFilter("grid-layer", ["<", ["to-number", ["get", "sum_pop"]], filterValue])})Что мы получили
Заголовок раздела «Что мы получили»Получилась просто отличная карта!
При желании посмотрите полный код и возможный результат.
Повторим основные моменты работы веб-карты, использующей векторные тайлы:
- Пользователь открывает веб-карту при определённом зуме в определённом охвате.
- Картографическая библиотека определяет, какие тайлы попадают в охват при заданном зуме.
- Картографическая библиотека запрашивает векторные тайлы, выполняя запрос к API сервера векторных тайлов.
- Сервер векторных тайлов получает запрос и выполняет серверную функцию, отвечающую за формирование тайла. Входным параметром для этой функции является индекс тайла Z/X/Y.
- Серверная функция выполняет запрос к базе данных. Этот запрос вырезает из слоя пространственных данных объекты, попадающие в тайл, кодирует их атрибуты и геометрию.
- Тайлы приходят в клиентскую часть картографического веб-приложения, где картографическая библиотека извлекает нужный слой из тайлов, сшивает объекты, попадающие в несколько тайлов, отрисовывает пространственные данные как картографический слой.
- С данными полученного слоя мы можем спокойно работать: показывать попап с атрибутами при клике на объект, менять курсор при наведении на слой, интерактивно фильтровать данные.
Такая карта применима в разнообразных задачах. Фокусом использования векторных тайлов является визуализация больших объёмов пространственных данных. На этом каркасе построены популярные картографические сервисы, подсистемы визуализации пространственных данных в аналитических платформах. Векторные тайлы — это проверенный, функциональный инструмент, но для его успешного применения надо понимать, из каких частей состоит этот механизм и за что отвечает каждая из этих частей.
Упражнения
Заголовок раздела «Упражнения»- Сделайте так, чтобы при наведении курсора на точку из слоя ойконимов она становилась немного больше
- Добавьте на карту инструмент фильтрации населённых пунктов по первой букве названия
- Добавьте обводку в 3 пикселя ячейке, на которую был выполнен клик. Сделайте так, чтобы обводка исчезала при зуме меньше 9.
Контрольные вопросы
Заголовок раздела «Контрольные вопросы»- Какое программное обеспечение предоставляет API для запроса тайлов в нашей веб-карте?
- За какой параметр отвечает индекс
Zв схеме векторных тайловZ/X/Y? - Из чего формируется геометрическая составляющая векторного тайла?
- Какой тип источника применяется для использования векторных тайлов в MapLibre GL JS?
- За что отвечает параметр
source-layerв коде функций MapLibre GL JS?