Редактирование вершин

Работа с геометрией в интерактивных веб-картах строится вокруг манипуляции вершинами (vertices) объектов типа LineString и Polygon. В Mapbox GL JS базовый рендеринг геометрии не включает встроенный редактор вершин, поэтому редактирование реализуется через дополнительные слои, источники данных и вспомогательные библиотеки, наиболее распространённая из которых — Mapbox GL Draw.


Геометрическая модель и структура вершин

В основе всех редактируемых объектов лежит формат GeoJSON. Каждая геометрия описывается набором координат:

  • Point — одна вершина
  • LineString — последовательность вершин
  • Polygon — массив контуров, где первый — внешний, остальные — отверстия

Структура Polygon:

{
  "type": "Feature",
  "geometry": {
    "type": "Polygon",
    "coordinates": [
      [
        [lng, lat],
        [lng, lat],
        [lng, lat],
        [lng, lat]
      ]
    ]
  }
}

Каждая координата — это редактируемая вершина, которая может быть перемещена пользователем при включённом режиме редактирования.


Подходы к редактированию вершин

В Mapbox GL JS существует три основных подхода:

Использование Mapbox GL Draw

Стандартное решение для редактирования:

  • добавление точек, линий и полигонов
  • перетаскивание вершин
  • удаление вершин
  • объединение и разбиение геометрий

Ручная реализация через source + layer

Позволяет контролировать каждую деталь:

  • хранение GeoJSON в GeoJSONSource
  • обработка событий мыши
  • обновление координат вручную

Гибридный подход

Комбинация Draw для базового редактирования и кастомных логик поверх него.


Подключение Mapbox GL Draw

import mapboxgl from "mapbox-gl";
import MapboxDraw from "@mapbox/mapbox-gl-draw";

mapboxgl.accessToken = "TOKEN";

const map = new mapboxgl.Map({
  container: "map",
  style: "mapbox://styles/mapbox/streets-v12",
  center: [37.618423, 55.751244],
  zoom: 10
});

const draw = new MapboxDraw({
  displayControlsDefault: false,
  controls: {
    polygon: true,
    line_string: true,
    trash: true
  }
});

map.addControl(draw);

После добавления контроллера включается режим, в котором вершины становятся интерактивными.


Жизненный цикл редактируемой вершины

При редактировании происходит последовательность:

  1. Создание объекта (feature)
  2. Генерация вершин
  3. Отображение якорей (handles)
  4. Перетаскивание вершины
  5. Пересчёт геометрии
  6. Обновление GeoJSON

Каждое изменение приводит к пересозданию источника данных.


События редактирования

Mapbox Draw предоставляет набор событий:

map.on("draw.create", updateFeatures);
map.on("draw.update", updateFeatures);
map.on("draw.delete", updateFeatures);

Функция обработки:

function updateFeatures(e) {
  const features = draw.getAll();
  console.log(features);
}

Ручное редактирование вершин без Draw

Иногда требуется полный контроль над логикой. Тогда используется GeoJSONSource.

Добавление источника

map.on("load", () => {
  map.addSource("polygon-source", {
    type: "geojson",
    data: {
      type: "Feature",
      geometry: {
        type: "Polygon",
        coordinates: [
          [
            [37.6, 55.75],
            [37.62, 55.75],
            [37.62, 55.76],
            [37.6, 55.76],
            [37.6, 55.75]
          ]
        ]
      }
    }
  });

  map.addLayer({
    id: "polygon-layer",
    type: "fill",
    source: "polygon-source",
    paint: {
      "fill-color": "#088",
      "fill-opacity": 0.4
    }
  });
});

Обработка вершин через события мыши

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

  • mousedown
  • mousemove
  • mouseup

Пример логики перетаскивания:

let isDragging = false;
let selectedVertexIndex = null;

map.on("mousedown", "vertex-layer", (e) => {
  isDragging = true;
  selectedVertexIndex = e.features[0].properties.index;
});

map.on("mousemove", (e) => {
  if (!isDragging) return;

  const source = map.getSource("polygon-source");
  const data = source._data;

  data.geometry.coordinates[0][selectedVertexIndex] = [
    e.lngLat.lng,
    e.lngLat.lat
  ];

  source.setData(data);
});

map.on("mouseup", () => {
  isDragging = false;
  selectedVertexIndex = null;
});

Представление вершин как отдельного слоя

Для интерактивного редактирования вершины часто выносятся в отдельный circle layer:

map.addLayer({
  id: "vertex-layer",
  type: "circle",
  source: "polygon-source",
  paint: {
    "circle-radius": 5,
    "circle-color": "#ff0000"
  }
});

Для полигонов требуется преобразовать координаты в набор точек.


Преобразование координат в вершины

Для отображения вершин отдельно:

function getVertices(feature) {
  return feature.geometry.coordinates[0].map((coord, index) => ({
    type: "Feature",
    geometry: {
      type: "Point",
      coordinates: coord
    },
    properties: {
      index
    }
  }));
}

Эти точки добавляются в отдельный источник:

map.getSource("vertices").setData({
  type: "FeatureCollection",
  features: getVertices(polygonFeature)
});

Обновление геометрии при изменении вершин

При перемещении вершины требуется:

  1. Обновить массив координат
  2. Пересчитать замыкание полигона
  3. Перезаписать источник данных
function updatePolygon(vertices) {
  const coords = vertices.map(v => v.geometry.coordinates);

  coords.push(coords[0]); // замыкание

  map.getSource("polygon-source").setData({
    type: "Feature",
    geometry: {
      type: "Polygon",
      coordinates: [coords]
    }
  });
}

Вставка и удаление вершин

Редактирование включает не только перемещение, но и изменение структуры:

Добавление вершины

Вставка между двумя точками:

coords.splice(insertIndex, 0, newCoordinate);

Удаление вершины

coords.splice(removeIndex, 1);

Важно сохранять минимум 3 вершины для корректного полигона.


Ограничения редактирования

При реализации редактирования в Mapbox GL JS необходимо учитывать:

  • нельзя допускать самопересечения (если это не требуется)
  • геометрия должна оставаться валидной GeoJSON
  • обновление источника может быть дорогостоящим при частых изменениях
  • события мыши имеют задержку при высокой нагрузке

Оптимизация работы с вершинами

Для повышения производительности:

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

Пример throttling:

let pending = false;

function scheduleUpdate(data) {
  if (pending) return;
  pending = true;

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

Кастомная логика привязки вершин

Часто требуется привязка к сетке или другим объектам:

function snapToGrid(lngLat, step = 0.001) {
  return [
    Math.round(lngLat.lng / step) * step,
    Math.round(lngLat.lat / step) * step
  ];
}

Применение при перемещении:

const snapped = snapToGrid(e.lngLat);

Использование feature-state для визуальной обратной связи

Mapbox GL JS поддерживает feature-state, что позволяет изменять стиль вершин без пересоздания источника:

map.setFeatureState(
  { source: "vertices", id: vertexId },
  { active: true }
);

И в стиле слоя:

"circle-color": [
  "case",
  ["boolean", ["feature-state", "active"], false],
  "#00ff00",
  "#ff0000"
]

Режимы редактирования и состояния

Типичная модель состояния:

  • idle — ожидание действий
  • selected — объект выбран
  • dragging — перемещение вершины
  • inserting — добавление новой вершины
  • deleting — удаление

Каждое состояние влияет на обработку событий мыши и рендеринг.


Связь с архитектурой Mapbox GL JS

Редактирование вершин встраивается в рендер-пайплайн:

  • Source хранит GeoJSON
  • Layer визуализирует геометрию
  • Event layer обрабатывает взаимодействие
  • Paint properties задают визуальную обратную связь

Любое изменение вершины фактически означает полное обновление source, которое триггерит перерасчёт слоя в WebGL-пайплайне Mapbox GL JS.