Работа с геометрией в интерактивных веб-картах строится вокруг
манипуляции вершинами (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 существует три основных подхода:
Стандартное решение для редактирования:
Позволяет контролировать каждую деталь:
GeoJSONSourceКомбинация 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);
После добавления контроллера включается режим, в котором вершины становятся интерактивными.
При редактировании происходит последовательность:
Каждое изменение приводит к пересозданию источника данных.
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);
}
Иногда требуется полный контроль над логикой. Тогда используется
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
}
});
});
Для реализации редактирования вручную используются события:
mousedownmousemovemouseupПример логики перетаскивания:
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)
});
При перемещении вершины требуется:
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 необходимо учитывать:
Для повышения производительности:
setDatarequestAnimationFrame при dragПример 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);
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 — удалениеКаждое состояние влияет на обработку событий мыши и рендеринг.
Редактирование вершин встраивается в рендер-пайплайн:
Любое изменение вершины фактически означает полное обновление source, которое триггерит перерасчёт слоя в WebGL-пайплайне Mapbox GL JS.