CRUD-подход в Mapbox GL JS опирается не на прямое изменение объектов карты, а на управление источниками данных (sources), слоями (layers) и GeoJSON-структурами. Основной принцип заключается в том, что карта является визуализацией состояния данных, а любые операции изменения реализуются через пересборку или обновление этого состояния.
В основе большинства сценариев лежит GeoJSON-источник, подключаемый к карте:
map.addSource('points-source', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: []
}
});
Источник становится единой точкой управления набором объектов. Слой отвечает только за визуализацию:
map.addLayer({
id: 'points-layer',
type: 'circle',
source: 'points-source',
paint: {
'circle-radius': 6,
'circle-color': '#ff0000'
}
});
CRUD операции в таком подходе сводятся к манипуляции
data источника и состоянием объектов.
Создание данных в Mapbox GL JS реализуется через добавление GeoJSON-объектов в источник.
const initialData = {
type: 'FeatureCollection',
features: []
};
map.addSource('points-source', {
type: 'geojson',
data: initialData
});
Добавление нового элемента выполняется через обновление всего GeoJSON:
function addPoint(lng, lat, properties = {}) {
const source = map.getSource('points-source');
const currentData = source._data; // внутреннее состояние (неофициальный доступ)
const newFeature = {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [lng, lat]
},
properties
};
const upd ated = {
type: 'FeatureCollection',
features: [...currentData.features, newFeature]
};
source.setData(updated);
}
Важный момент заключается в том, что Mapbox GL JS не предоставляет
API уровня addFeature. Обновление всегда происходит через
setData.
Более устойчивый подход предполагает хранение данных вне Mapbox:
let state = {
type: 'FeatureCollection',
features: []
};
function addFeature(feature) {
state = {
...state,
features: [...state.features, feature]
};
map.getSource('points-source').setData(state);
}
Такой способ исключает зависимость от внутренних полей источника.
Чтение в Mapbox GL JS реализуется через несколько уровней API.
const features = map.queryRenderedFeatures({
layers: ['points-layer']
});
Этот метод возвращает только те объекты, которые попали в текущий viewport и прошли фильтрацию слоя.
const source = map.getSource('points-source');
const data = source._data;
Несмотря на распространённость, доступ к _data считается
неофициальным и может быть нестабильным.
const features = map.queryRenderedFeatures(
[x, y],
{
layers: ['points-layer']
}
);
Используется для обработки кликов:
map.on('click', (e) => {
const features = map.queryRenderedFeatures(e.point, {
layers: ['points-layer']
});
console.log(features);
});
const features = map.querySourceFeatures('points-source');
Этот метод работает только для векторных тайловых источников, а не для GeoJSON, что важно учитывать при проектировании архитектуры.
Обновление данных в Mapbox GL JS чаще всего реализуется через полную замену GeoJSON или через feature state.
function updateFeature(id, newCoordinates) {
const source = map.getSource('points-source');
const data = source._data;
const updatedFeatures = data.features.map(feature => {
if (feature.properties.id === id) {
return {
...feature,
geometry: {
...feature.geometry,
coordinates: newCoordinates
}
};
}
return feature;
});
source.setData({
type: 'FeatureCollection',
features: updatedFeatures
});
}
Этот подход прост, но может быть неэффективен при больших наборах данных.
Mapbox GL JS поддерживает механизм состояния объектов без пересборки GeoJSON:
map.setFeatureState(
{
source: 'points-source',
id: 1
},
{
selected: true
}
);
Использование требует наличия id у feature:
{
type: 'Feature',
id: 1,
geometry: {
type: 'Point',
coordinates: [30, 50]
},
properties: {}
}
В стиле слоя можно использовать состояние:
map.addLayer({
id: 'points-layer',
type: 'circle',
source: 'points-source',
paint: {
'circle-color': [
'case',
['boolean', ['feature-state', 'selected'], false],
'#00ff00',
'#ff0000'
]
}
});
Сброс состояния:
map.removeFeatureState({
source: 'points-source',
id: 1
});
Для оптимизации используется хранение индекса:
const featureIndex = new Map();
function updateFeature(id, updates) {
const feature = featureIndex.get(id);
const updated = {
...feature,
properties: {
...feature.properties,
...updates
}
};
featureIndex.se t(id, upd ated);
const data = {
type: 'FeatureCollection',
features: Array.from(featureIndex.values())
};
map.getSource('points-source').setData(data);
}
Удаление в Mapbox GL JS также сводится к пересборке GeoJSON.
function deleteFeature(id) {
const source = map.getSource('points-source');
const data = source._data;
const filtered = data.features.filter(
feature => feature.properties.id !== id
);
source.setData({
type: 'FeatureCollection',
features: filtered
});
}
Feature state не удаляет сам объект, но позволяет скрыть его визуально:
map.setFeatureState(
{ source: 'points-source', id: 2 },
{ hidden: true }
);
И в стиле слоя:
'circle-opacity': [
'case',
['boolean', ['feature-state', 'hidden'], false],
0,
1
]
if (map.getLayer('points-layer')) {
map.removeLayer('points-layer');
}
if (map.getSource('points-source')) {
map.removeSource('points-source');
}
Этот вариант используется при полной очистке контекста карты.
Данные выносятся из Mapbox и управляются отдельно:
class GeoStore {
constructor(map) {
this.map = map;
this.state = {
type: 'FeatureCollection',
features: []
};
}
sync() {
this.map.getSource('points-source').setData(this.state);
}
create(feature) {
this.state.features.push(feature);
this.sync();
}
update(id, changes) {
this.state.features = this.state.features.map(f =>
f.properties.id === id
? { ...f, properties: { ...f.properties, ...changes } }
: f
);
this.sync();
}
delete(id) {
this.state.features = this.state.features.filter(
f => f.properties.id !== id
);
this.sync();
}
}
При использовании фреймворков применяется реактивная модель:
useEffect(() => {
map.getSource('points-source').setData(data);
}, [data]);
Частое использование setData приводит к полной
переработке слоя. При больших объёмах данных это становится узким
местом.
Оптимизации включают:
feature-state вместо пересборки
GeoJSONmap.addSource('cluster-source', {
type: 'geojson',
data,
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 50
});
При тысячах объектов важно избегать полного пересоздания массива:
function patchUpdate(id, patch) {
const feature = featureIndex.get(id);
const updated = {
...feature,
...patch
};
featureIndex.se t(id, updated);
requestAnimationFrame(() => {
map.getSource('points-source').setData({
type: 'FeatureCollection',
features: Array.from(featureIndex.values())
});
});
}
Mapbox GL JS тесно связан с событиями:
map.on('click', 'points-layer', (e) => {
const feature = e.features[0];
setSelected(feature.properties.id);
});
Связка событий и состояния позволяет реализовать полноценный CRUD-интерфейс поверх карты.
В реальных приложениях часто комбинируются подходы:
map.setFeatureState(
{ source: 'points-source', id },
{ selected: true, hovered: false }
);
Такое разделение снижает количество перерисовок и повышает предсказуемость поведения карты.