CRUD операции

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 источника и состоянием объектов.


Create: создание объектов и источников

Создание данных в 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);
}

Такой способ исключает зависимость от внутренних полей источника.


Read: чтение данных и выборка объектов

Чтение в Mapbox GL JS реализуется через несколько уровней API.

Чтение отрисованных объектов

const features = map.queryRenderedFeatures({
  layers: ['points-layer']
});

Этот метод возвращает только те объекты, которые попали в текущий viewport и прошли фильтрацию слоя.

Чтение объектов источника

const source = map.getSource('points-source');
const data = source._data;

Несмотря на распространённость, доступ к _data считается неофициальным и может быть нестабильным.

Query по координатам

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

Query по исходным данным

const features = map.querySourceFeatures('points-source');

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


Update: обновление объектов

Обновление данных в 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
  });
}

Этот подход прост, но может быть неэффективен при больших наборах данных.


Feature State как оптимизированное обновление

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

Delete: удаление объектов

Удаление в 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

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

Этот вариант используется при полной очистке контекста карты.


Архитектурные паттерны CRUD в Mapbox GL JS

Централизованное состояние

Данные выносятся из 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]);

Производительность CRUD операций

Частое использование setData приводит к полной переработке слоя. При больших объёмах данных это становится узким местом.

Оптимизации включают:

  • использование feature-state вместо пересборки GeoJSON
  • сегментацию данных по нескольким источникам
  • кластеризацию точек:
map.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())
    });
  });
}

Работа с событиями и CRUD

Mapbox GL JS тесно связан с событиями:

map.on('click', 'points-layer', (e) => {
  const feature = e.features[0];

  setSelected(feature.properties.id);
});

Связка событий и состояния позволяет реализовать полноценный CRUD-интерфейс поверх карты.


Смешанные стратегии управления данными

В реальных приложениях часто комбинируются подходы:

  • GeoJSON как основной источник
  • feature-state для UI-состояний
  • внешнее хранилище как источник истины
  • Mapbox как слой визуализации
map.setFeatureState(
  { source: 'points-source', id },
  { selected: true, hovered: false }
);

Такое разделение снижает количество перерисовок и повышает предсказуемость поведения карты.