Рисование на карте

Рисование на карте в MapLibre GL JS строится вокруг двух фундаментальных подходов: декларативного (через слои и источники данных) и императивного (через обработку событий и ручное управление отрисовкой). Оба подхода часто комбинируются, особенно при создании интерактивных редакторов геометрии, систем разметки и пользовательских инструментов картографирования.

Геометрия как основа рисования

Любая отрисовка на карте начинается с геометрии, представленной в формате GeoJSON. Этот формат позволяет описывать точки, линии и полигоны, которые затем интерпретируются движком рендеринга.

Основные типы геометрий:

  • Point — отдельная точка
  • LineString — линия из набора координат
  • Polygon — замкнутая область

MapLibre GL JS использует GeoJSON как стандартный способ передачи пользовательских данных в карту через источники.

Добавление источника данных 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]
  });
});

Это создаёт эффект динамической прорисовки геометрии в реальном времени.

Использование событий MapLibre GL JS

Для построения полноценного инструмента рисования используются ключевые события:

  • click — добавление точек
  • mousemove — предпросмотр
  • dblclick — завершение фигуры
  • contextmenu — отмена действия
  • keydown — управление режимами

Комбинация событий позволяет имитировать поведение профессиональных GIS-редакторов.

Работа с queryRenderedFeatures

Для взаимодействия с уже нарисованными объектами используется выборка объектов под курсором:

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);
}

Редактирование координат требует изменения конкретного объекта и повторной установки данных.

Использование canvas overlay для кастомного рисования

Для сложных сценариев применяется HTML canvas поверх карты:

const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');

map.getCanvasContainer().appendChild(canvas);

Далее координаты переводятся из географических в пиксельные:

const pixel = map.project([lng, lat]);

Это позволяет рисовать произвольные графические элементы, не ограниченные GeoJSON-моделью.

Преобразование координат и синхронизация с картой

Ключевой аспект рисования — постоянная синхронизация координат:

  • lngLat → географические координаты
  • project → экранные координаты
  • unproject → обратное преобразование

Это обеспечивает корректное отображение при масштабировании и перемещении карты.

Производительность при частых обновлениях

Частое вызовы setData могут приводить к нагрузке на рендеринг. Для оптимизации применяются подходы:

  • батчинг обновлений
  • requestAnimationFrame для синхронизации
  • минимизация числа features
  • разделение слоёв по частоте обновления

Пример оптимизированного обновления:

let pending = false;

function updateSource(data) {
  if (pending) return;

  pending = true;

  requestAnimationFrame(() => {
    map.getSource('draw-source').setData(data);
    pending = false;
  });
}

Комбинирование декларативного и императивного подходов

Сложные инструменты рисования почти всегда комбинируют:

  • GeoJSON слои для хранения данных
  • события мыши для интерактивности
  • canvas для кастомной графики
  • фильтры слоёв для управления отображением

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