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

Векторные данные в Mapbox GL JS чаще всего представлены в формате GeoJSON, где каждый объект содержит геометрию и набор пользовательских атрибутов в поле properties. Эти свойства используются для стилизации, фильтрации, а также реализации интерактивного поведения карты. Однако архитектура библиотеки предполагает важное ограничение: геометрия и properties источника данных являются иммутабельными на уровне отдельных фич, что напрямую влияет на подход к их редактированию.

Любые изменения properties требуют либо перезаписи источника данных, либо использования механизма feature-state, который предназначен для динамического состояния объектов без модификации исходного GeoJSON.


Структура properties в GeoJSON-источнике

Каждая фича в GeoJSON имеет стандартную структуру:

{
  "type": "Feature",
  "geometry": {
    "type": "Point",
    "coordinates": [30.5, 50.5]
  },
  "properties": {
    "name": "Объект A",
    "status": "active",
    "value": 42
  }
}

В контексте Mapbox GL JS эти свойства используются:

  • в выражениях стилей (paint и layout)
  • в фильтрах слоёв
  • в popup и tooltip
  • в логике взаимодействия

Перезапись источника данных через setData

Наиболее прямой способ изменить properties — полная замена GeoJSON-источника.

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

source.setData({
  type: 'FeatureCollection',
  features: [
    {
      type: 'Feature',
      geometry: {
        type: 'Point',
        coordinates: [30.52, 50.45]
      },
      properties: {
        name: 'Обновлённый объект',
        status: 'inactive',
        value: 100
      }
    }
  ]
});

Ключевые особенности подхода:

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

Ограничение изменения отдельных properties

В Mapbox GL JS нельзя напрямую изменить property отдельной фичи внутри источника:

// такого метода не существует
source.updateFeatureProperty(featureId, 'status', 'active');

Это архитектурное ограничение связано с тем, что GeoJSON рассматривается как неизменяемый поток данных.


Feature-state как механизм динамических properties

Для решения задачи изменения состояния отдельных объектов используется API feature-state. Оно позволяет хранить временные свойства, не изменяя GeoJSON.

map.setFeatureState(
  {
    source: 'points-source',
    id: 123
  },
  {
    selected: true,
    hover: false,
    intensity: 0.8
  }
);

Для получения состояния используется:

const state = map.getFeatureState({
  source: 'points-source',
  id: 123
});

Очистка состояния:

map.removeFeatureState({
  source: 'points-source',
  id: 123
});

Использование feature-state в стилях

Главное преимущество feature-state проявляется в выражениях стилей:

map.addLayer({
  id: 'points-layer',
  type: 'circle',
  source: 'points-source',
  paint: {
    'circle-radius': [
      'case',
      ['boolean', ['feature-state', 'selected'], false],
      10,
      5
    ],
    'circle-color': [
      'case',
      ['boolean', ['feature-state', 'hover'], false],
      '#ff0000',
      '#3388ff'
    ]
  }
});

Здесь свойства selected и hover не являются частью GeoJSON, но влияют на визуализацию.


Сравнение setData и feature-state

Обновление через setData:

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

Обновление через feature-state:

  • не изменяет GeoJSON
  • работает только при наличии id у фич
  • оптимально для UI-состояний (hover, selection, focus)
  • обновляется мгновенно без пересоздания источника

Требование идентификаторов фич

Для использования feature-state каждая фича должна иметь уникальный идентификатор:

{
  "type": "Feature",
  "id": 42,
  "geometry": {
    "type": "Point",
    "coordinates": [30.5, 50.5]
  },
  "properties": {
    "name": "Объект"
  }
}

Без id работа feature-state невозможна, поскольку Mapbox GL JS не может сопоставить состояние с конкретной сущностью.


Изменение properties через пересборку массива данных

В типичных SPA-приложениях GeoJSON часто хранится во внешнем состоянии (например, Redux, Zustand, reactive store). В этом случае изменение properties реализуется через создание нового массива features.

const updatedFeatures = features.map(f => {
  if (f.properties.id === 10) {
    return {
      ...f,
      properties: {
        ...f.properties,
        status: 'updated'
      }
    };
  }
  return f;
});

map.getSource('points').setData({
  type: 'FeatureCollection',
  features: updatedFeatures
});

Этот подход сохраняет чистую функциональную модель данных, но требует повторной отправки всего набора в Mapbox.


Работа с queryRenderedFeatures и виртуальные свойства

При взаимодействии с картой часто используется метод:

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

Важно учитывать, что:

  • возвращаемые features содержат исходные properties
  • feature-state не включается в properties напрямую
  • состояние необходимо получать отдельно через getFeatureState

Это разделение часто требует объединения данных вручную:

const enriched = features.map(f => ({
  ...f,
  state: map.getFeatureState({
    source: 'points-source',
    id: f.id
  })
}));

Использование properties в фильтрах

Properties активно применяются в фильтрах слоёв:

map.addLayer({
  id: 'filtered-points',
  type: 'circle',
  source: 'points-source',
  filter: ['==', ['get', 'status'], 'active']
});

После изменения GeoJSON через setData фильтры автоматически пересчитываются.

Feature-state в фильтрах напрямую не используется, поэтому для динамических фильтров требуется либо обновление источника, либо использование выражений через paint/layout.


Производительные стратегии обновления properties

При работе с большим количеством объектов критично выбирать правильную стратегию:

  • статические данные → setData
  • частые UI-изменения → feature-state
  • комбинированные сценарии → разделение слоя данных и слоя состояния

Практический паттерн:

  • GeoJSON хранит только неизменяемые атрибуты
  • feature-state хранит UI-логику (hover, selection, animation)
  • отдельные слои отвечают за разные уровни визуализации

Ограничения и нюансы архитектуры

  • feature-state не сохраняется при перезагрузке карты
  • при удалении слоя состояние теряется
  • невозможно использовать feature-state без id
  • setData может вызывать перерасчёт кластеров и фильтров
  • большое количество setData снижает FPS при анимации

Объединение подходов в интерактивных сценариях

В сложных интерфейсах часто комбинируются оба механизма:

  • базовые свойства (категория, тип, координаты) → GeoJSON properties
  • временные состояния (hover, selection, progress) → feature-state
  • визуальная логика → expressions в слоях

Пример архитектуры взаимодействия:

// базовое обновление данных
map.getSource('objects').setData(updatedGeoJSON);

// динамическое состояние
map.setFeatureState(
  { source: 'objects', id: selectedId },
  { selected: true }
);

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