MapLibre GL JS: карты объектов, зоны и маршруты — 8 живых примеров
Создаём рабочую карту сервиса на MapLibre GL JS: 48 учебных объектов, восемь интерактивных сценариев, фильтры, кластеры, зоны, планы выездов и экспорт PNG/GeoJSON. Исходники и запуск на Windows, Linux и VPS.

В таблице CRM видно, сколько заявок назначено каждой бригаде. Но где находятся клиенты, какие объекты расположены рядом, почему одна территория перегружена и в каком порядке стоит запланировать выезды? Здесь полезна карта, связанная с рабочими данными: фильтрами, карточками, статусами и понятными действиями пользователя.
MapLibre GL JS позволяет построить такой интерфейс в браузере. В материале разберём настоящую карту на WebGL, восемь прикладных сценариев, источники GeoJSON, кластеры, зоны обслуживания, линии выездов, обновление объектов и экспорт. Ниже можно всё попробовать: мини-мастерская работает прямо на странице, а полный проект с исходниками, тестами и инструкциями для Windows и Linux доступен для скачивания.
Где карта приносит пользу веб-сервису
Карта нужна, когда пространственное положение помогает принять решение. В сервисном бизнесе диспетчер распределяет выезды между бригадами. В управлении недвижимостью сотрудник просматривает здания и открытые заявки. В торговой сети руководитель сравнивает магазины по территории. В системе обслуживания оборудования инженер выбирает ближайшие объекты для осмотра. Во всех этих случаях точка становится входом в рабочую карточку, а не декоративным значком.
Хорошая карта отвечает на конкретные вопросы: какие объекты подходят под фильтр, что известно о выбранном адресе, что изменилось после обновления данных и почему система показывает именно эти результаты. Для этого ей нужны легенда, текстовый список, понятные состояния и связь с остальным интерфейсом. Если пользователь вынужден угадывать значение цвета или искать исчезнувшую точку, сама графическая библиотека проблему не решит.
В нашей мастерской выбран небольшой сервисный район. У каждого объекта есть тип, статус заявки, бригада и число задач. Такой набор позволяет собрать законченную рабочую панель и увидеть, какие части потом заменяются настоящими данными вашей CRM.
Что делает MapLibre и что нужно добавить отдельно
MapLibre GL JS — библиотека отображения и взаимодействия с картой. Она рисует слои, применяет стили, обрабатывает масштабирование, перемещение камеры и события. Географические данные при этом приходят из источников, которые выбирает разработчик: небольшого GeoJSON, векторных или растровых тайлов, собственных сервисов.
Библиотека не выдаёт автоматически всемирную подложку, базу адресов, поиск организаций, дорожный граф и пробки. Для превращения адреса в координаты нужен геокодер. Для автомобильного маршрута — отдельный сервис или движок маршрутизации. Для закрытых клиентских данных — ваш сервер с проверкой доступа. Эти задачи можно объединить в одном продукте, но у них разные API, стоимость, лицензии и эксплуатационные ограничения.
В статье намеренно используется собственная учебная схема без внешней подложки. Поэтому первый запуск не зависит от ключа стороннего сервиса и квоты запросов. Можно изучить работу источников и слоёв, а затем осознанно подключить подходящую картографическую основу. Отсутствие тайлов здесь — свойство учебного проекта, а не ограничение MapLibre.
Мини-мастерская: восемь связанных задач
Выберите сценарий в верхней части мастерской и прочитайте короткое задание. Начните с карточки объекта №12, затем включите фильтры. Перейдите к кластерам и раскройте группу. В зонах сравните западную и восточную части района. В плане выездов переключите маршрут А/Б и сдвиньте указатель по линии. После изменения координат проверьте новое положение объекта и скачайте результат.
Все сценарии используют общий набор данных. Изменённый статус влияет на фильтрацию, перемещённая точка меняет линию плана и попадание в зону. Это помогает увидеть поведение целого интерфейса, а не восемь несвязанных картинок. Кнопка сброса восстанавливает исходные данные, а обновление страницы удаляет временные изменения.
На узком экране рабочие блоки перестраиваются в одну колонку. Для подробного изучения удобно открыть мастерскую отдельно. Внизу есть стартовый код выбранной задачи, копирование, скачивание примера и ссылка на полный ZIP. Выполняется настоящий MapLibre: здесь нет видеозаписи или имитации кластеризации поверх обычного изображения.
Учебные данные и границы демонстрации
В проекте 48 вымышленных объектов четырёх типов: офис, склад, магазин и сервис. Нарисованные улицы, река, адреса, бригады и заявки созданы специально для материала. Координаты используют обычную географическую систему, но схема не описывает реальную местность и не должна применяться для навигации.
Отдельно подготовлены три прямоугольные зоны и два плана выездов. Они нужны, чтобы проверить фильтры и связность данных. Здесь нет определения местоположения посетителя, загрузки клиентских документов, скрытой отправки координат или обращения к внешнему картографическому API. Все изменения остаются в памяти вкладки.
Для собственного проекта полезно сохранить это разделение: демонстрационные данные должны быть явно обозначены и не смешиваться с рабочими. При переходе к реальным объектам потребуется решить, кому разрешено видеть точные адреса, где хранить историю изменений и как удалять устаревшие сведения. Даже скрытый фильтром объект остаётся доступным посетителю, если полный GeoJSON уже загружен в браузер.
Версия 6.16.0, ESM и локальный worker
Материал использует MapLibre GL JS 6.16.0. В этой версии библиотека распространяется как ES modules. Старый пример с глобальной переменной из обычного script-тега нельзя переносить без проверки. В нашем проекте приложение подключается через type=”module”, а основной модуль и worker берутся из одного зафиксированного пакета.
Worker выполняет часть обработки данных отдельно от основного потока интерфейса. Его файл тоже нужно разместить на сервере. Если отдать вместо JavaScript страницу ошибки, забыть правильный MIME или смешать версии файлов, карта может не заработать даже при успешно загруженном основном модуле.
Сборка мастерской копирует официальный дистрибутив в public/vendor и согласованно меняет расширения .mjs на .js для совместимости с обычным хостингом. URL worker задан явно и содержит версию. Это техническое решение нашего пакета; в приложении на Vite или другом сборщике следует использовать схему подключения из документации для соответствующего инструмента.
import { Map, setWorkerUrl } from './vendor/maplibre-gl.js';
setWorkerUrl(new URL(
'./vendor/maplibre-gl-worker.js?v=6.16.0',
import.meta.url
).href);
Документация: подключение ESM и worker.
GeoJSON: координаты, свойства и стабильный id
Для небольшого набора объектов удобно начать с GeoJSON. Feature объединяет геометрию и свойства, а FeatureCollection собирает несколько объектов. У точки координаты записываются в порядке долгота, широта. Это частая причина ошибок: перестановка чисел переносит объект в другое место, при этом оба значения могут оставаться формально допустимыми.
Свойства содержат рабочие данные, которые нужны интерфейсу: название, тип, статус и идентификатор бригады. Не стоит автоматически выгружать в браузер всю строку клиентской базы. Оставьте только необходимое для разрешённого пользователю сценария. Секреты, внутренние заметки и лишние персональные сведения не становятся защищёнными от того, что карта их не показывает.
Стабильный id связывает точку с карточкой и обновлением. При повторной загрузке он должен обозначать тот же объект, а не текущий номер строки после сортировки. В мастерской используются числа от 1 до 48; в реальной системе это может быть отдельный публичный идентификатор с проверкой доступа на сервере.
const objects = {
type: 'FeatureCollection',
features: [{
type: 'Feature', id: 12,
geometry: { type: 'Point', coordinates: [37.61, 55.75] },
properties: { name: 'Сервис №12', type: 'service', status: 'new' }
}]
};
Источник данных и слой — разные части карты
Источник отвечает за данные, слой — за способ их отображения. Один источник можно использовать несколько раз: например, нарисовать обычные точки, отдельное кольцо выделения и подписи. При этом не нужно создавать три независимых копии карточек или поддерживать три разные базы объектов.
В мастерской источник objects хранит все точки, а отдельный источник clustered используется для кластеризации текущей выборки. У зон свой источник, у линии плана — свой. Такое разделение делает обновления понятнее: изменение заявки не требует пересоздавать карту и заново подключать каждый обработчик.
Порядок слоёв влияет на результат. Заливка зон находится под точками, линия выездов не закрывает карточки, а кольцо выбранного объекта рисуется поверх основных маркеров. Если слой существует, но ничего не видно, проверьте не только данные, но и visibility, фильтр, масштаб, прозрачность и положение слоя относительно остальных.
map.on('load', () => {
map.addSource('clients', { type: 'geojson', data: objects });
map.addLayer({
id: 'clients-circles', type: 'circle', source: 'clients',
paint: {
'circle-radius': 8, 'circle-color': '#09886e',
'circle-stroke-width': 2, 'circle-stroke-color': '#ffffff'
}
});
});
Документация: спецификация стиля, источников и слоёв.
Сценарий 1: карта объектов и рабочая карточка
Первая задача — открыть сведения об объекте. На карте пользователь нажимает точку, а в боковой панели видит название, учебный адрес, статус, бригаду и координаты. Тот же объект можно выбрать из обычного списка. Это важно на телефоне, при плотном расположении точек и при работе без мыши.
Выбор и фильтр — разные состояния. Если после выбора объекта поменять фильтр, карточка не должна внезапно превратиться в другую. В мастерской она сохраняется, но сообщает, что выбранный объект скрыт текущей выборкой. В рабочем продукте можно выбрать другую политику, например сбрасывать карточку, однако поведение должно быть явным.
Текст карточки формируется через textContent и DOM-элементы. Название из базы нельзя бездумно вставлять как HTML: данные могут содержать разметку или неожиданные символы. Для краткой всплывающей подсказки можно использовать Popup, но подробная карточка с действиями часто удобнее в отдельной панели, особенно на небольшом экране.
map.on('click', 'clients-circles', event => {
const feature = event.features?.[0];
if (!feature) return;
const title = document.getElementById('client-title');
title.textContent = String(feature.properties.name ?? 'Без названия');
// По feature.id найдите разрешённую карточку в состоянии приложения.
});
Документация: Popup и безопасное наполнение содержимого.
Сценарий 2: фильтры, поиск и согласованные счётчики
Фильтр полезен, когда карта отвечает на рабочий вопрос: показать только склады с новыми заявками или сервисные точки в работе. В мастерской условия типа, статуса и текста применяются одновременно. Результат используют карта, список, счётчик и экспорт выборки. Поэтому пользователь не получает четыре разных ответа на один запрос.
Поиск здесь локальный: он проверяет название и учебный адрес среди 48 объектов. Это не геокодирование произвольного адреса и не поиск по всем организациям города. При большом каталоге поиск обычно выполняет сервер, а клиент получает ограниченный результат. В интерфейсе тогда нужны состояния загрузки, отсутствия результатов и ошибки запроса.
Пустая выборка — нормальное состояние, а не сбой. Покажите, что условия не нашли объекты, и предложите очистить фильтры. Не возвращайте молча весь набор: пользователь может принять его за нужные результаты. В нашей мастерской кнопка вписывания также объясняет, почему пустой список нельзя показать одной рамкой.
map.setFilter('clients-circles', [
'all',
['==', ['get', 'type'], 'service'],
['==', ['get', 'status'], 'working']
]);
// Для очистки фильтра слоя:
map.setFilter('clients-circles', null);
Документация: фильтрация объектов слоя.
Цвет, размер и легенда через выражения
Стили MapLibre могут зависеть от свойств объекта и масштаба. Например, цвет показывает тип точки, а размер немного увеличивается при приближении. Это позволяет избежать множества почти одинаковых слоёв и держать визуальные правила рядом с данными.
Но цвет не должен быть единственным носителем смысла. В мастерской названия типов повторены в легенде и карточке, а статус доступен текстом в списке. Если в реальном сервисе красный означает просрочку, добавьте понятную подпись и фильтр. Иначе сотрудник с особенностями цветового восприятия или плохим экраном потеряет важную часть информации.
Также полезно ограничивать визуальное разнообразие. Когда одновременно используются тип, статус, приоритет и бригада, попытка закодировать всё цветом делает карту трудной для чтения. Выберите главный признак для текущей задачи, а остальное перенесите в карточку, подсказку или отдельный режим отображения.
const colorByType = [
'match', ['get', 'type'],
'office', '#2164aa', 'warehouse', '#8756ad',
'shop', '#d58b19', 'service', '#09886e', '#777777'
];
map.setPaintProperty('clients-circles', 'circle-color', colorByType);
map.setPaintProperty('clients-circles', 'circle-radius', [
'interpolate', ['linear'], ['zoom'], 10, 5, 14, 9
]);
Сценарий 3: настоящая кластеризация точек
Когда несколько объектов расположены близко, отдельные круги перекрываются. Кластеризация объединяет их в группы, размер которых зависит от масштаба. В нашем сценарии число внутри круга показывает количество объектов текущей выборки, а на более крупном масштабе группа распадается на меньшие группы или отдельные точки.
Важно фильтровать данные до расчёта кластеров, если число должно отражать именно найденные заявки. Простое скрытие части точек после кластеризации может оставить счётчик всех исходных объектов. Поэтому мастерская передаёт отфильтрованный GeoJSON в отдельный кластерный источник.
Кластер — способ отображения, а не постоянная бизнес-группа. Его идентификатор не следует хранить как номер бригады или района: при изменении данных и масштаба группировка пересчитывается. Для стабильных территорий нужны ваши полигоны или серверные сущности. Пользователю полезно дать два способа раскрытия: нажать круг на карте или воспользоваться обычной кнопкой.
map.addSource('clustered-clients', {
type: 'geojson', data: objects,
cluster: true, clusterRadius: 55, clusterMaxZoom: 13
});
map.addLayer({
id: 'client-clusters', type: 'circle', source: 'clustered-clients',
filter: ['has', 'point_count'],
paint: {
'circle-color': '#159275',
'circle-radius': ['step', ['get', 'point_count'], 19, 10, 25, 25, 31]
}
});
Документация: создание и оформление кластеров.
Раскрытие кластера и подписи без внешних шрифтов
После нажатия на группу можно запросить масштаб, на котором она раскроется, и переместить камеру. В текущем API getClusterExpansionZoom возвращает Promise. Пока ответ готовится, пользователь может сменить фильтр или сценарий, поэтому результат нужно применять только к актуальному состоянию интерфейса.
В мастерской число кластера рисует настоящий symbol-слой. Стиль не содержит URL glyphs, а text-font указывает локальные шрифты браузера. Таким образом, для этих коротких числовых подписей не загружаются внешние наборы глифов. Оформление текста может немного отличаться между системами из-за доступных шрифтов.
Для большой карты с многоязычными названиями решение выбирают отдельно: важны поддерживаемые письменности, лицензии шрифтов, размер данных и согласованность отображения. Не удаляйте glyphs из чужого стиля механически, ожидая идентичного результата. Наш случай прост: несколько чисел на собственных кластерах.
map.on('click', 'client-clusters', async event => {
const cluster = event.features?.[0];
if (!cluster) return;
const zoom = await map.getSource('clustered-clients')
.getClusterExpansionZoom(Number(cluster.properties.cluster_id));
map.easeTo({ center: cluster.geometry.coordinates, zoom });
});
Документация: подписи с локальными шрифтами.
Сценарий 4: зоны обслуживания и выборка внутри
Зона помогает связать географию с правилами работы. Например, одна бригада обслуживает западную часть территории, другая — центр. На карте можно показать границы и быстро отобрать попадающие внутрь объекты. В мастерской три полигона имеют разные заливки; выбранная зона становится дополнительным условием фильтра.
Геометрическая проверка выполняется отдельно от рисования. Пользователь видит результат благодаря fill-слою, но само попадание точки в полигон определяет функция модели. Это полезное разделение: бизнес-правило не должно зависеть от того, загружен ли WebGL и виден ли сейчас полигон в окне.
Учебная реализация поддерживает простые кольца без отверстий и включает границы. Она не предназначена для кадастровых расчётов, сложных мультиполигонов или территорий, пересекающих антимеридиан. Для серьёзных геопространственных задач выберите проверенную библиотеку либо пространственную базу и заранее определите, как трактуется точка на общей границе двух зон.
const zone = {
type: 'Feature', properties: { name: 'Запад' },
geometry: { type: 'Polygon', coordinates: [[
[37.553, 55.718], [37.602, 55.718],
[37.602, 55.782], [37.553, 55.782], [37.553, 55.718]
]] }
};
map.addSource('service-zone', { type: 'geojson', data: zone });
map.addLayer({
id: 'service-zone-fill', source: 'service-zone', type: 'fill',
paint: { 'fill-color': '#85b5e5', 'fill-opacity': 0.3 }
});
Документация: полигон из GeoJSON.
Сценарий 5: линия плана выездов и движение по ней
План выездов связывает несколько объектов в заданном порядке. В мастерской доступны два варианта: четыре и пять остановок. Линия проходит через координаты этих объектов, рядом перечислены их названия, а указатель можно перемещать вручную или запустить короткую анимацию.
Здесь принципиально важно правильное название результата. Линия соединяет точки напрямую и не учитывает дороги, въезды, развязки, ограничения движения или пробки. Показанные километры — сумма расстояний между соседними координатами, а не длина автомобильного маршрута. Анимация показывает учебный прогресс по сегментам, не движение реального сотрудника и не расчёт скорости.
Такой интерфейс уже полезен для обсуждения порядка обхода, визуализации связей и проверки входных данных. Для настоящего планирования транспорта нужно получить дорожную геометрию от отдельного движка и решить задачи оптимизации порядка, временных окон и вместимости. MapLibre затем сможет отобразить полученную линию, но сам её смысл определяет ваше приложение.
const plan = {
type: 'Feature', properties: { name: 'Учебный план' },
geometry: { type: 'LineString', coordinates: [
[37.57, 55.73], [37.61, 55.75], [37.66, 55.77]
] }
};
map.addSource('visit-plan', { type: 'geojson', data: plan });
map.addLayer({
id: 'visit-plan-line', type: 'line', source: 'visit-plan',
layout: { 'line-cap': 'round', 'line-join': 'round' },
paint: { 'line-color': '#e36c36', 'line-width': 5 }
});
Анимация без лишней нагрузки и неожиданных перемещений
Анимация должна помогать объяснить состояние. В нашей мастерской она начинается только по кнопке и останавливается при смене сценария. Обновления указателя выполняются последовательно: следующий шаг не создаёт бесконечную очередь setData, пока предыдущий ещё обрабатывается. Для одного маркера и короткого демонстрационного плана этого достаточно.
При включённом prefers-reduced-motion плавные перелёты камеры отключаются, а для линии остаётся пошаговый режим. Пользователь получает ту же информацию без обязательного движения. Даже без системной настройки полезно иметь кнопку остановки: человек может захотеть рассмотреть участок или сравнить положение со списком.
В реальной телеметрии нужно отдельно учитывать частоту сообщений, задержку сети, устаревшие координаты и время измерения. Красиво движущийся маркер не доказывает, что данные свежие. Показывайте отметку последнего обновления и не продолжайте имитировать движение после потери связи, если это может вводить диспетчера в заблуждение.
const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
map.flyTo({
center: [37.61, 55.75], zoom: 14,
duration: reduceMotion ? 0 : 600
});
// Для движения точки обновляйте её отдельный GeoJSON source:
await map.getSource('runner').setData({
type: 'Feature', properties: {},
geometry: { type: 'Point', coordinates: [37.615, 55.752] }
});
Сценарий 6: статус, координаты и обновление source
Откройте сценарий изменения данных, выберите объект и поменяйте статус. Затем задайте новые координаты в пределах учебного района. После применения обновляются модель, карточка, фильтры и настоящий GeoJSON source. Если объект участвует в плане выездов, меняется соответствующая вершина линии.
Поля координат дают альтернативу перетаскиванию точки. Они удобны для точного ввода, клавиатуры и проверки ошибок. Приложение ограничивает допустимую область именно этого стенда и объясняет диапазон. Это не пределы MapLibre: библиотека может работать с другими территориями, а реальный сервис должен проверять свои правила.
В версии 6.16.0 setData возвращает Promise, поэтому можно дождаться обработки обновления источника. Но такое ожидание не означает запись в базу данных. В мастерской сохранение серверу отсутствует: результат живёт в памяти и доступен через GeoJSON. Для CRM понадобятся отдельный API записи, проверка прав и обработка конфликта одновременных изменений.
const next = structuredClone(objects);
const object = next.features.find(feature => feature.id === 12);
if (!object) throw new Error('Объект не найден');
object.properties.status = 'working';
object.geometry.coordinates = [37.62, 55.75];
await map.getSource('clients').setData(next);
// После успешного ответа вашего API отдельно обновите серверную версию записи.
Документация: setData, updateData и работа с GeoJSONSource.
Как подключить настоящие данные с сервера
Первый практический шаг после мастерской — заменить генератор учебных объектов загрузкой разрешённой выборки. Сервер может быть написан на PHP, Node.js или другом языке. Для браузера важен контракт: GeoJSON, стабильные идентификаторы, допустимые координаты и свойства, которые действительно нужны интерфейсу.
На стороне сервера сначала проверяют пользователя и его организацию, затем формируют ответ. Нельзя отдавать все клиентские адреса и надеяться, что JavaScript скроет лишние. При загрузке по границам карты дополнительно ограничивайте размер ответа, диапазон координат и частоту запросов. Параметры запроса не должны превращаться в произвольный SQL или путь к файлу.
Клиенту нужны отмена устаревшего запроса и защита от обратного порядка ответов: медленная загрузка прошлого фильтра не должна заменить текущую выборку. Ниже показана базовая отмена fetch. Это заготовка интеграции, а не существующий endpoint мастерской; серверный обработчик необходимо реализовать под вашу модель доступа.
let activeRequest;
async function loadClients() {
activeRequest?.abort();
const request = new AbortController();
activeRequest = request;
const response = await fetch('/api/clients.geojson', {
credentials: 'same-origin', signal: request.signal
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
if (request !== activeRequest) return;
// Здесь проверьте схему, число объектов и координаты ответа.
await map.getSource('clients').setData(data);
}
Сценарий 7: fitBounds, flyTo и границы окна
Камера должна помогать пользователю, а не постоянно перехватывать управление. После поиска удобно вписать найденные точки кнопкой fitBounds. После выбора строки — приблизить конкретный объект через flyTo. Но автоматический перелёт при каждом приходе фонового обновления будет мешать сотруднику изучать другой участок.
В мастерской есть явные кнопки «Вписать выборку» и «Весь район». Для одного объекта применяется ограничение максимального масштаба, чтобы карта не приблизилась чрезмерно. Для пустой выборки операция не выполняется и появляется объяснение. Отступы оставляют пространство вокруг крайних точек, чтобы они не прятались под рамкой.
Счётчик «В окне» проверяет попадание координат в прямоугольник getBounds. Это полезная учебная метрика, но при повороте или наклоне карты географический прямоугольник не равен точному экранному полигону. Если бизнес-логика требует именно видимые элементы, необходимо отдельно определить семантику видимости и учитывать перекрытия, кластеры и фильтры.
map.fitBounds(
[[37.55, 55.715], [37.69, 55.785]],
{ padding: 35, maxZoom: 14, duration: 500 }
);
const currentBounds = map.getBounds();
const inWindow = objects.features.filter(feature =>
currentBounds.contains(feature.geometry.coordinates)
);
console.log('В географическом прямоугольнике:', inWindow.length);
Документация: границы и проверка координат.
Сценарий 8: PNG карты и экспорт GeoJSON
PNG нужен для отчёта, обсуждения плана или вставки в документ. В мастерской он создаётся из реального canvas после завершения отрисовки. К снимку добавляется подпись, что данные учебные. Сначала нажмите «Подготовить PNG карты», затем сохраните результат по появившейся ссылке. Ссылка содержит последний подготовленный вид, а не автоматически обновляемую карту.
В снимок не входят HTML-карточки, кнопки и боковой список: они находятся вне canvas. При необходимости полноценного отчёта собирайте отдельный документ, куда добавляются и изображение карты, и таблица выбранных объектов. Не называйте такой снимок доказательством актуальности данных без времени формирования и понятного источника.
GeoJSON сохраняет данные, а не пиксели. Можно скачать весь набор либо текущую выборку с изменёнными координатами и статусами. Такой файл удобно передать другому приложению или использовать для повторяемого теста. Но он может содержать чувствительные сведения, поэтому в рабочем сервисе экспорт должен подчиняться тем же правам, что и просмотр.
// Опция задаётся при создании карты, а не перед самым скачиванием.
const exportMap = new Map({
container: 'report-map',
style: { version: 8, sources: {}, layers: [
{ id: 'background', type: 'background', paint: { 'background-color': '#edf1e5' } }
] },
canvasContextAttributes: { preserveDrawingBuffer: true }
});
exportMap.once('idle', () => {
exportMap.getCanvas().toBlob(blob => {
if (!blob) return;
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'map.png';
link.textContent = 'Сохранить PNG';
document.body.append(link);
}, 'image/png');
});
Экспорт: память, CORS и момент готовности
Опция preserveDrawingBuffer позволяет читать результат WebGL после рисования, но имеет стоимость для производительности. В мастерской она включена ради учебного экспорта. В большом приложении стоит измерить влияние на целевых устройствах и решить, нужен ли постоянный режим или отдельная карта для отчётов.
Подготовка PNG ждёт idle с ограничением времени, останавливает демонстрационное движение и сообщает об ошибке, если карта не успела завершить работу. Длинная сторона изображения карты ограничена 1800 пикселями, затем добавляется полоса подписи. Это выбранный предел нашего интерфейса для удобного файла, а не максимальное разрешение библиотеки.
После подключения внешних изображений и источников учитывайте CORS и правила их использования. Успешный просмотр ресурса не всегда означает, что его можно включить в экспортируемый canvas. Наша схема состоит из собственных локальных данных, поэтому сторонний источник не участвует в снимке. Blob URL освобождается при замене подготовленного результата или закрытии страницы.
const blob = new Blob([JSON.stringify(objects, null, 2)], {
type: 'application/geo+json'
});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'clients.geojson';
link.textContent = 'Сохранить GeoJSON';
document.body.append(link);
// Когда ссылка больше не нужна:
// link.remove(); URL.revokeObjectURL(url);
Документация: опции canvas и создания карты.
Как добавить настоящую картографическую подложку
Для реальной местности обычно подключают готовый стиль провайдера или собственный сервер тайлов. Стиль может ссылаться сразу на несколько ресурсов: тайлы, спрайты, шрифты и изображения. Поэтому проверять нужно весь набор запросов, а не только URL начального JSON.
До внедрения уточните разрешённую нагрузку, атрибуцию, условия кеширования и использования изображений в отчётах. Открытая лицензия библиотеки не даёт автоматического права свободно использовать любые картографические данные. Аналогично бесплатный тариф сервиса может иметь лимит запросов или ограничения для конкретного сценария.
Если ключ предназначен для браузера, посетитель сможет его увидеть. Ограничивайте такой ключ средствами провайдера: доменами, квотами и доступными API. Секретный серверный ключ хранится на сервере. Для закрытой инфраструктуры можно организовать собственный источник, но тогда появятся задачи обновления данных, хранения, резервирования и мониторинга, которых нет в нашей автономной мастерской.
Большие наборы: когда GeoJSON становится недостаточно
Полный GeoJSON удобен для первого интерфейса, но объём растёт вместе с числом объектов и свойств. Даже если браузер показывает только несколько точек, большой JSON нужно скачать, разобрать и передать на обработку. Кластеризация помогает отображению, но сама по себе не убирает стоимость загрузки всего каталога.
Начните с измерений: размер ответа, время подготовки на сервере, задержка появления карты, расход памяти и частота обновлений. Удалите ненужные свойства и чрезмерную точность координат там, где она не влияет на задачу. Затем оцените загрузку по области, векторные тайлы, разделение статических и часто меняющихся данных.
Если обновляется одна движущаяся точка, нет необходимости каждый раз пересылать тысячи неизменных полигонов. Отдельные источники помогают локализовать работу. Для updateData важны стабильные уникальные id и соблюдение контракта diff; выбирайте этот путь после проверки конкретной версии API и ваших сценариев изменения, а не как механическую замену всех setData.
Документация: оптимизация больших наборов GeoJSON.
Мобильный интерфейс, клавиатура и доступность
На телефоне карта конкурирует со страницей за жесты. Поэтому в мастерской отключено масштабирование колёсиком, кнопки управления видны постоянно, а карточка и список остаются обычными HTML-блоками. Пользователь может изучить данные, даже если не хочет перемещать карту или не может точно попасть в маленькую точку.
Для интерактивных действий предусмотрены альтернативы: выбрать строку списка, приблизить объект кнопкой, раскрыть первый кластер, ввести координаты в поля и пройти линию шагами. Видимый фокус помогает понять, где находится клавиатура. Сообщения об изменениях и ошибках доступны как отдельные текстовые области.
При этом canvas-карта не превращается автоматически в полностью доступную таблицу. Для реального продукта проверьте сценарии со скринридером, клавиатурой и на физических устройствах своей аудитории. Не размещайте единственную критическую информацию только на цветной карте. Текстовый путь к тем же данным должен оставаться полноценной частью интерфейса.
Ошибки загрузки и потеря WebGL-контекста
На некоторых устройствах WebGL недоступен из-за настроек браузера, драйвера или ограничений среды. Во время работы контекст также может потеряться. Интерфейс должен сообщить, что случилось, и сохранить доступ к тем действиям, которым графика не нужна. В мастерской продолжают работать список, фильтрация и экспорт GeoJSON; подготовка PNG отключается.
Другой класс проблем связан с публикацией: неверный путь к worker, неправильный MIME JavaScript, запрет CSP, отсутствующий CSS или смешанные версии в кеше. Проверяйте вкладки Network и Console. Сообщение «файл загружен» не гарантирует, что worker запущен и данные обработаны.
При уходе со страницы анимация останавливается. Если браузер сохраняет страницу в back-forward cache, экземпляр карты не уничтожается безусловно: иначе после возврата пользователь увидит сломанный интерфейс. При окончательном закрытии освобождаются карта, наблюдатели и подготовленные Blob URL. Эти детали особенно заметны в приложении, где карту часто открывают и закрывают.
map.on('error', event => {
document.getElementById('map-error').textContent = event.error.message;
});
map.on('webglcontextlost', () => {
document.getElementById('export-png').disabled = true;
// Оставьте доступ к списку и текстовым данным.
});
window.addEventListener('pagehide', event => {
if (!event.persisted) map.remove();
});
Запуск проекта в Windows
В архиве находится готовая папка public и исходники сборки. Для разработки используйте Node.js 24. Распакуйте проект в отдельную рабочую папку и выполните команды ниже в PowerShell. npm ci устанавливает версии из lock-файла, а не подбирает новые зависимости по текущему состоянию реестра.
Параметр –ignore-scripts отключает install-скрипты зависимостей. Сборка использует зафиксированный esbuild и его платформенный пакет. После тестов npm start запускает локальный сервер на loopback: открыть мастерскую можно по адресу http://127.0.0.1:8152/. Остановка выполняется Ctrl+C.
Для простого просмотра уже собранного проекта достаточно node server.mjs. Открывать index.html двойным щелчком не следует: ESM и worker должны загружаться с нормального HTTP origin. Если порт занят, задайте PORT в текущем терминале. Это меняет только порт локального примера, а не настройки сайта или системных служб.
cd .\maplibre-project
node --version
npm.cmd ci --ignore-scripts
npm.cmd run build
npm.cmd test
npm.cmd start
# При занятом порте, после остановки сервера:
$env:PORT = '8153'
npm.cmd start
Запуск в Linux и статический вариант с PHP
На Linux порядок действий почти такой же: Node.js 24, установка из lock-файла, сборка, тесты и запуск локального сервера. Выполняйте команды обычным пользователем. Для разработки этой мастерской не требуется root, база данных или отдельный процесс обработки пользовательских данных.
Если нужен только просмотр готовой public и на машине уже установлен PHP, можно использовать его встроенный сервер. Он раздаст HTML, CSS, JavaScript и worker. В проекте нет PHP-обработчика записи: изменение статуса происходит в браузере, а экспорт создаёт локальный файл. Такой режим не превращает демонстрацию в серверную CRM.
Встроенные серверы Node и PHP здесь предназначены для локального запуска и проверки. На облачном или выделенном сервере удобнее раздавать подготовленную public через Caddy или Nginx с HTTPS. Тогда посетителю не нужен Node.js, а сборочная среда может находиться вообще на другом компьютере.
cd maplibre-project
npm ci --ignore-scripts
npm run build
npm test
npm start
# Альтернатива для просмотра готовой public, когда PHP уже установлен:
php -S 127.0.0.1:8152 -t public
Публикация на Linux VPS через Caddy
Для сайта достаточно перенести готовую public в отдельную папку релиза. Не раздавайте корень проекта: там находятся сборочные файлы и тесты, которые посетителю не нужны. В примере используется /srv/maplibre/current/public, где current указывает на проверенный релиз.
Ссылка ../maplibre-project.zip рассчитана на WordPress-мастерскую, где архив доступен на уровень выше public. При отдельном Caddy root public соседний ZIP автоматически не раздаётся. Замените href на /downloads/maplibre-project.zip и настройте отдельный маршрут выдачи архива. Другой простой вариант — положить подготовленный ZIP в public/downloads/maplibre-project.zip и указать ссылку downloads/maplibre-project.zip. После размещения проверьте скачивание именно кнопкой интерфейса.
Замените пример домена своим, настройте DNS, установите Caddy и проверьте доступность портов 80 и 443. При выполнении этих условий Caddy сможет получить сертификат и обслуживать HTTPS. Учебный архив не создаёт внешний домен и не выпускает сертификат за пользователя; эти шаги выполняются на вашем сервере.
В конфигурации разрешены только GET и HEAD, добавлены заголовки и журнал запросов. Полная CSP находится в deploy/Caddyfile.example: она учитывает локальный worker, динамические стили и Blob-ресурсы. Если карта встраивается на другой домен или использует внешние тайлы, политику нужно адаптировать под конкретные разрешённые источники.
maps.example.com {
root * /srv/maplibre/current/public
encode zstd gzip
@write not method GET HEAD
respond @write 405
header X-Content-Type-Options nosniff
header Referrer-Policy same-origin
log {
output file /var/log/caddy/maplibre-access.log
}
file_server
}
Подробности HTTPS, reverse proxy и эксплуатации разобраны в материале Caddy: HTTPS, PHP и Node.js. Для текущей карты reverse proxy не обязателен: готовые файлы обслуживаются напрямую.
Проверка после выкладки, обновление и откат
После публикации проверьте главную страницу, JavaScript, worker и CSS по конечным URL. Затем откройте карту в браузере, переключите фильтр, раскройте кластер, измените объект и подготовьте PNG. Ответ HTTP 200 не проверяет графическую отрисовку, поэтому сетевые и пользовательские проверки дополняют друг друга.
Обновление удобнее выполнять через отдельный каталог релиза. Соберите новые файлы, запустите тесты и аудит зависимостей, перенесите public и только потом переключите ссылку current. Предыдущий каталог сохраните до окончания проверки. Для отката достаточно вернуть ссылку на прошлый согласованный набор файлов.
Особенно важно не смешивать старый основной модуль с новым worker. В проекте версия worker включена в URL, но политика кеширования всего приложения всё равно должна быть продумана. Если используются долгие cache-control для статических файлов, добавьте версионирование имён или каталога релиза и проверьте обновление уже открытой страницы.
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
curl -I https://maps.example.com/
curl -I https://maps.example.com/vendor/maplibre-gl-worker.js
sudo journalctl -u caddy --since '10 minutes ago'
# В браузере дополнительно проверьте worker, кластеры и PNG.
Закрытая карта: права, секреты и журналы
Для внутренней панели недостаточно спрятать ссылку на мастерскую. Если GeoJSON лежит в публичной папке, его можно скачать напрямую. Защищайте весь путь получения данных и проверяйте права на сервере. В небольшом внутреннем инструменте может подойти аутентификация Caddy поверх HTTPS, а в CRM обычно требуется полноценная пользовательская сессия и разграничение организаций.
Пароли, серверные токены и закрытые ключи не должны попадать в JavaScript, public, исходный ZIP или журнал. Хеш для basic_auth генерируется на сервере и хранится в конфигурации с соответствующими правами. Браузерные ключи провайдера карт ограничиваются его средствами и рассматриваются как видимые посетителю.
Журналы полезны для поиска отсутствующих worker и ошибок API, но координаты клиентов и содержимое карточек не стоит собирать без необходимости. Для наблюдения часто достаточно технического кода ошибки, версии приложения и времени. При добавлении внешней аналитики отдельно оцените, какие данные карты могут попасть в события или запись сессии.
Как устроены исходники и что проверяют тесты
В src/model.js находятся учебные объекты, зоны, планы и функции работы с данными. src/style.js описывает настоящий Style Spec. src/app.js связывает карту с HTML, управляет состоянием и экспортом. Сборка копирует исходники в публичную папку source, поэтому можно изучить полный контроллер прямо из мастерской.
Тесты импортируют настоящую библиотеку для проверки LngLat, LngLatBounds и преобразования Mercator, а реальный валидатор Style Spec проверяет описание всех слоёв. Отдельно проверяются фильтры, границы зон, изменения координат, планы и HTTP-раздача worker с правильным MIME.
Проверки DOM-контроллера используют JSDOM и явно подменённый адаптер карты. Они помогают ловить рассогласование списка и источника, ошибки состояния и проблемы возврата через bfcache, но не проверяют GPU. Настоящие кластеры, визуальный результат и мобильную компоновку нужно проверять браузером. Такое разделение позволяет понимать, какие свойства подтверждены автоматически, а какие зависят от устройства.
Как использовать скачанный пример в своей странице
Кнопка скачивания стартового кода отдаёт ES module выбранного сценария. Он использует model.js и style.js проекта: это компактная отправная точка, а не самодостаточный файл со встроенной библиотекой. Полная инструкция есть в README; ниже показана сборка примера внутри распакованного проекта.
Сохраните скачанный файл как src/example.js, выполните команду esbuild и создайте страницу с контейнером map, подключением CSS MapLibre и модулем example.js. Контейнеру обязательно задайте высоту. Без неё карта может корректно инициализироваться, но занимать нулевую область, что выглядит как ошибка библиотеки.
После первого результата заменяйте данные постепенно: сначала собственные точки, затем карточку, фильтры и серверный API. Не переносите сразу всю учебную панель в рабочую CRM без проверки прав и жизненного цикла. Полный исходник полезен как образец связанного интерфейса, а конкретный продукт должен сохранять только нужные пользователю действия.
npm exec -- esbuild src/example.js --bundle --format=esm --external:./vendor/maplibre-gl.js --outfile=public/example.js
<!doctype html>
<html lang="ru">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Моя карта</title>
<link rel="stylesheet" href="vendor/maplibre-gl.css">
<div id="map" style="height:70vh;min-height:320px"></div>
<script type="module" src="example.js"></script>
</html>
Что развивать дальше и где сверяться с документацией
На основе мастерской можно сделать карту заявок, реестр филиалов, план осмотров оборудования, панель контроля территорий или визуальную часть логистического сервиса. Следующий шаг выбирайте по рабочему вопросу пользователя: кому назначить заявку, какие объекты требуют внимания, что изменилось за день или какие данные нужно включить в отчёт.
Для реальной эксплуатации добавляйте возможности по отдельности и проверяйте их смысл. Подложка должна иметь понятный источник, линия маршрута — соответствовать выбранному движку, статус — сохраняться через авторизованный API, а экспорт — отражать разрешённые данные. Это делает карту инструментом работы, которым можно пользоваться повторно, а не только эффектной демонстрацией.
Исходники собственной мастерской распространяются под MIT. MapLibre GL JS использует BSD-3-Clause; лицензии библиотек и транзитивных зависимостей включены в архив. При обновлении сверяйтесь с документацией конкретной версии, сохраняйте уведомления правообладателей и повторяйте проверку сценариев на своём наборе данных.


