События draw

В MapLibre GL JS отсутствует встроенный инструмент рисования геометрий, однако функциональность редактирования и создания объектов на карте реализуется через совместимые расширения, прежде всего через API-подобные плагины уровня Mapbox GL Draw. В результате события категории draw становятся частью событийной модели карты и позволяют отслеживать полный жизненный цикл геометрий: создание, изменение, удаление и взаимодействие с режимами редактирования.

Эти события интегрируются в стандартную систему событий карты и подписываются через map.on(...), где map — экземпляр MapLibre GL JS.


Архитектура событий draw

События draw строятся вокруг внутреннего состояния коллекции GeoJSON-объектов. Каждый объект проходит несколько стадий:

  • создание (feature добавлена пользователем)
  • изменение (геометрия или свойства обновлены)
  • удаление (объект исключён из коллекции)
  • выбор (изменение активного объекта)
  • смена режима (например, переход к рисованию линии или полигона)

Эта модель позволяет синхронизировать пользовательские действия с внешними системами: хранилищами данных, редакторами, аналитикой и серверными API.


Подключение обработчиков draw-событий

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

map.on('draw.create', (e) => {
    console.log('Создан объект:', e.features);
});

map.on('draw.update', (e) => {
    console.log('Обновлён объект:', e.features);
});

map.on('draw.delete', (e) => {
    console.log('Удалён объект:', e.features);
});

Каждое событие передаёт объект события, содержащий массив features, представленный в формате GeoJSON.


Структура объекта события

Общий формат события draw:

{
    type: 'draw.create' | 'draw.update' | 'draw.delete',
    features: [
        {
            id: 'string | number',
            type: 'Feature',
            geometry: {
                type: 'Point' | 'LineString' | 'Polygon',
                coordinates: [...]
            },
            properties: {
                ... пользовательские данные
            }
        }
    ]
}

Идентификатор id используется для связи между состояниями одного объекта в разных событиях.


draw.create — создание объектов

Событие draw.create возникает при завершении создания геометрии пользователем. Это ключевая точка фиксации новых данных.

Типичный сценарий:

  • пользователь завершает рисование полигона
  • объект добавляется во внутренний GeoJSON-слой
  • событие эмитится с финальной геометрией

Пример обработки:

map.on('draw.create', (e) => {
    const feature = e.features[0];

    fetch('/api/features', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(feature)
    });
});

Особенность события заключается в том, что оно срабатывает только после завершения интерактивного действия, а не в процессе рисования.


draw.update — изменение геометрии

Событие draw.update возникает при модификации уже существующих объектов:

  • перемещение вершин
  • изменение формы полигона
  • редактирование линии
  • изменение координат точки
map.on('draw.update', (e) => {
    e.features.forEach(feature => {
        console.log('Обновление:', feature.id);
    });
});

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


draw.delete — удаление объектов

Событие draw.delete фиксирует удаление одного или нескольких объектов из слоя редактирования.

map.on('draw.delete', (e) => {
    const removedIds = e.features.map(f => f.id);
    console.log('Удалены объекты:', removedIds);
});

Удаление происходит без сохранения геометрии в текущем состоянии, поэтому для восстановления требуется внешнее хранилище.


draw.selectionchange — изменение выделения

Событие draw.selectionchange отражает изменение активного набора объектов, находящихся в состоянии выделения.

map.on('draw.selectionchange', (e) => {
    console.log('Выделенные объекты:', e.features);
});

Это событие критично для интерфейсов редактирования, где действия зависят от текущего выбора:

  • включение панелей свойств
  • отображение инструментов трансформации
  • подсветка активных объектов

draw.modechange — смена режима

Инструмент рисования работает в различных режимах:

  • simple_select
  • draw_point
  • draw_polygon
  • draw_line_string
  • direct_select

Событие draw.modechange фиксирует переход между ними.

map.on('draw.modechange', (e) => {
    console.log('Новый режим:', e.mode);
});

Это позволяет адаптировать интерфейс под текущую операцию пользователя, включая:

  • переключение тулбаров
  • блокировку интерфейсных элементов
  • изменение подсказок

draw.render — процесс отрисовки

Событие draw.render возникает при каждой перерисовке состояния draw-слоя.

Особенности:

  • может вызываться очень часто
  • не связан напрямую с изменением данных
  • используется для синхронных UI-обновлений
map.on('draw.render', () => {
    console.log('Перерисовка draw слоя');
});

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


Синхронизация состояния с внешним хранилищем

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

Типовой подход:

const store = new Map();

map.on('draw.create', sync);
map.on('draw.update', sync);
map.on('draw.delete', sync);

function sync(e) {
    e.features.forEach(f => {
        store.set(f.id, f);
    });
}

При удалении объектов требуется явное удаление из хранилища.


Работа с GeoJSON источниками

Несмотря на наличие внутреннего состояния draw, часто требуется синхронизация с внешним GeoJSON source:

map.getSource('features-source').setData({
    type: 'FeatureCollection',
    features: Array.from(store.values())
});

Такая схема позволяет:

  • использовать кастомные стили
  • комбинировать draw-данные с другими слоями
  • сохранять единый источник правды

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

События draw.update и draw.render могут вызываться часто, особенно при сложных полигонах.

Ключевые оптимизации:

  • дебаунс обновлений
  • батчинг изменений
  • разделение UI и data sync
  • минимизация операций внутри обработчиков

Пример debounce:

let timeout;

map.on('draw.update', (e) => {
    clearTimeout(timeout);
    timeout = setTimeout(() => {
        persist(e.features);
    }, 200);
});

Частые ошибки при работе с draw-событиями

1. Двойная синхронизация Одновременное обновление состояния и через draw.update, и через render приводит к дублированию логики.

2. Игнорирование массива features Даже при изменении одного объекта события часто передают массив.

3. Отсутствие контроля режима Логика обработки без учёта modechange приводит к неконсистентному UI.

4. Хранение состояния только в памяти Без внешнего persistence данные теряются при перезагрузке карты.


Поведение событий в сложных сценариях

При комбинированных операциях (например, редактирование нескольких объектов):

  • selectionchange может предшествовать update
  • update может вызываться несколько раз за один жест
  • render вызывается независимо от логики данных

Это требует разделения:

  • событий UI уровня
  • событий модели данных
  • событий рендера

Взаимодействие с анимацией карты

Так как MapLibre GL JS имеет собственный render loop, события draw вплетаются в общий цикл отрисовки карты. Это означает:

  • обновления геометрии синхронны с рендером
  • UI может обновляться чаще, чем фактические изменения данных
  • тяжёлые операции внутри обработчиков влияют на плавность карты

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