Рисование на карте в MapLibre GL JS строится вокруг двух фундаментальных подходов: декларативного (через слои и источники данных) и императивного (через обработку событий и ручное управление отрисовкой). Оба подхода часто комбинируются, особенно при создании интерактивных редакторов геометрии, систем разметки и пользовательских инструментов картографирования.
Любая отрисовка на карте начинается с геометрии, представленной в формате GeoJSON. Этот формат позволяет описывать точки, линии и полигоны, которые затем интерпретируются движком рендеринга.
Основные типы геометрий:
MapLibre GL JS использует GeoJSON как стандартный способ передачи пользовательских данных в карту через источники.
Для начала работы с рисованием создаётся источник данных, который будет динамически обновляться:
map.on('load', () => {
map.addSource('draw-source', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: []
}
});
});
Этот источник изначально пуст, но в процессе взаимодействия пользователя он будет изменяться, что и создаёт эффект рисования.
Чтобы данные стали видимыми, необходимо добавить слой, связанный с источником.
Для точек:
map.addLayer({
id: 'points-layer',
type: 'circle',
source: 'draw-source',
paint: {
'circle-radius': 6,
'circle-color': '#ff0000'
},
filter: ['==', '$type', 'Point']
});
Для линий:
map.addLayer({
id: 'lines-layer',
type: 'line',
source: 'draw-source',
paint: {
'line-color': '#0000ff',
'line-width': 3
},
filter: ['==', '$type', 'LineString']
});
Для полигонов:
map.addLayer({
id: 'polygons-layer',
type: 'fill',
source: 'draw-source',
paint: {
'fill-color': '#00ff00',
'fill-opacity': 0.4
},
filter: ['==', '$type', 'Polygon']
});
Разделение слоёв по типам геометрии позволяет гибко управлять стилями и логикой отображения.
Базовый механизм рисования реализуется через обработчики событий карты.
map.on('click', (e) => {
const point = {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [e.lngLat.lng, e.lngLat.lat]
},
properties: {}
};
const source = map.getSource('draw-source');
const data = source._data;
data.features.push(point);
source.setData(data);
});
Такой подход позволяет динамически расширять набор объектов прямо по клику пользователя.
Для построения линий требуется хранить массив координат и обновлять LineString:
const lineCoordinates = [];
map.on('click', (e) => {
lineCoordinates.push([e.lngLat.lng, e.lngLat.lat]);
const lineFeature = {
type: 'Feature',
geometry: {
type: 'LineString',
coordinates: lineCoordinates
}
};
const source = map.getSource('draw-source');
source.setData({
type: 'FeatureCollection',
features: [lineFeature]
});
});
При каждом клике линия перерисовывается, расширяясь новыми сегментами.
Полигон требует замкнутого массива координат, где первая и последняя точки совпадают.
const polygonCoordinates = [];
map.on('click', (e) => {
polygonCoordinates.push([e.lngLat.lng, e.lngLat.lat]);
const closed = [...polygonCoordinates];
if (closed.length > 2) {
closed.push(closed[0]);
}
const polygonFeature = {
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [closed]
}
};
map.getSource('draw-source').setData({
type: 'FeatureCollection',
features: [polygonFeature]
});
});
Такой механизм используется в простых редакторах областей интереса и инструментах выделения зон.
При усложнении логики рисования важно отделять состояние интерфейса от данных карты:
Пример структуры состояния:
const state = {
mode: 'line',
active: [],
features: []
};
Это позволяет реализовать переключение инструментов без конфликтов между обработчиками событий.
Интерактивное рисование часто требует отображения «резиновой» линии или полигона до фиксации точки:
map.on('mousemove', (e) => {
if (state.mode !== 'line' || state.active.length === 0) return;
const preview = {
type: 'Feature',
geometry: {
type: 'LineString',
coordinates: [...state.active, [e.lngLat.lng, e.lngLat.lat]]
}
};
map.getSource('draw-source').setData({
type: 'FeatureCollection',
features: [...state.features, preview]
});
});
Это создаёт эффект динамической прорисовки геометрии в реальном времени.
Для построения полноценного инструмента рисования используются ключевые события:
Комбинация событий позволяет имитировать поведение профессиональных GIS-редакторов.
Для взаимодействия с уже нарисованными объектами используется выборка объектов под курсором:
map.on('click', (e) => {
const features = map.queryRenderedFeatures(e.point, {
layers: ['points-layer']
});
if (features.length) {
console.log('Выбран объект:', features[0]);
}
});
Это основа для редактирования и удаления геометрии.
Удаление выполняется через фильтрацию GeoJSON:
function removeFeature(index) {
const source = map.getSource('draw-source');
source._data.features.splice(index, 1);
source.setData(source._data);
}
Редактирование координат требует изменения конкретного объекта и повторной установки данных.
Для сложных сценариев применяется HTML canvas поверх карты:
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
map.getCanvasContainer().appendChild(canvas);
Далее координаты переводятся из географических в пиксельные:
const pixel = map.project([lng, lat]);
Это позволяет рисовать произвольные графические элементы, не ограниченные GeoJSON-моделью.
Ключевой аспект рисования — постоянная синхронизация координат:
Это обеспечивает корректное отображение при масштабировании и перемещении карты.
Частое вызовы setData могут приводить к нагрузке на рендеринг. Для оптимизации применяются подходы:
Пример оптимизированного обновления:
let pending = false;
function updateSource(data) {
if (pending) return;
pending = true;
requestAnimationFrame(() => {
map.getSource('draw-source').setData(data);
pending = false;
});
}
Сложные инструменты рисования почти всегда комбинируют:
Такой подход позволяет создавать полноценные редакторы картографических данных, системы планирования маршрутов и инструменты геоаналитики.