IndexedDB для GeoJSON

При работе с геопространственными данными в веб-клиентах ключевая проблема заключается в балансе между объёмом данных, скоростью отрисовки и ограничениями памяти. Браузерный слой хранения становится критическим компонентом, особенно при использовании Mapbox GL JS, где источником данных часто выступает GeoJSON.

GeoJSON в чистом виде плохо масштабируется при увеличении объёма объектов: полная перезагрузка источника через setData приводит к перерасчёту всех фич на стороне рендерера WebGL. В таких условиях IndexedDB используется как промежуточный слой долговременного хранения и частичной выборки данных.


Модель хранения GeoJSON в IndexedDB

IndexedDB представляет собой асинхронное объектное хранилище с поддержкой индексов и транзакций. Для GeoJSON оптимальной считается модель, в которой данные разделяются на:

  • отдельные фичи (Feature)
  • метаданные слоя
  • пространственные ключи (bbox, tile key, geohash)
  • версии объектов

Базовая структура объекта:

{
  id: "feature-123",
  geometry: { type: "Polygon", coordinates: [...] },
  properties: {
    type: "building",
    height: 42
  },
  bbox: [76.8, 43.2, 76.9, 43.3],
  updatedAt: 1710000000000
}

Такой подход позволяет избегать хранения единого монолитного GeoJSON-документа.


Создание базы данных и object store

Инициализация IndexedDB выполняется через indexedDB.open, при этом версия базы определяет схему хранения.

const request = indexedDB.open("geo_db", 1);

request.onupgradenee ded = (event) => {
  const db = event.target.result;

  const store = db.createObjectStore("features", {
    keyPath: "id"
  });

  store.createIndex("bbox", "bbox", { unique: false });
  store.createIndex("type", "properties.type", { unique: false });
  store.createIndex("updatedAt", "updatedAt", { unique: false });
};

Ключевой момент — индексация не решает геопространственные запросы напрямую, но позволяет быстро фильтровать набор перед передачей в Mapbox GL JS.


Стратегия пространственного разбиения данных

IndexedDB не поддерживает spatial index в нативном виде, поэтому применяется псевдогеопространственное разбиение:

1. Tile-based хранение

Каждый объект привязывается к тайлу:

tileKey = `${z}/${x}/${y}`;

И хранится как индекс:

store.createIndex("tile", "tileKey", { unique: false });

2. Geohash-индексация

Использование geohash позволяет группировать объекты по префиксу строки:

  • u4pruy
  • u4pru

Чем короче префикс, тем крупнее область.


Запись GeoJSON в IndexedDB

Запись выполняется через транзакции в режиме readwrite:

function saveFeature(db, feature) {
  const tx = db.transaction("features", "readwrite");
  const store = tx.objectStore("features");

  store.put(feature);

  return tx.complete;
}

При массовой загрузке важно использовать батчи:

  • уменьшение числа транзакций
  • контроль памяти
  • предотвращение блокировок UI

Чтение данных для Mapbox GL JS

Mapbox GL JS использует GeoJSON Source, который обновляется через setData.

Базовый поток:

  1. определение bounding box текущего viewport
  2. запрос объектов из IndexedDB
  3. сбор GeoJSON FeatureCollection
  4. передача в карту
function queryFeatures(db, bbox) {
  return new Promise((resolve) => {
    const tx = db.transaction("features", "readonly");
    const store = tx.objectStore("features");
    const index = store.index("bbox");

    const results = [];

    index.openCursor().onsucc ess = (event) => {
      const cursor = event.target.result;

      if (!cursor) {
        resolve(results);
        return;
      }

      const value = cursor.value;

      if (intersects(value.bbox, bbox)) {
        results.push(value);
      }

      cursor.continue();
    };
  });
}

Интеграция с источником GeoJSON

Mapbox GL JS ожидает структуру:

map.addSource("geo", {
  type: "geojson",
  data: {
    type: "FeatureCollection",
    features: []
  }
});

Обновление данных:

map.getSource("geo").setData(featureCollection);

Оптимизация частоты обновлений

Частая проблема — перерисовка слоя при каждом изменении viewport. Решается через:

Debounce запросов

function debounce(fn, delay) {
  let t;
  return (...args) => {
    clearTimeout(t);
    t = setTimeout(() => fn(...args), delay);
  };
}

Виртуализация запроса

  • обновление только при смене tile
  • сравнение bbox предыдущего состояния
  • игнор мелких сдвигов камеры

Обновление и версионирование данных

IndexedDB не имеет встроенного diff-механизма, поэтому применяется версия объекта:

{
  id: "feature-123",
  version: 5,
  geometry: {...}
}

Логика обновления:

  • если version > stored.version → overwrite
  • иначе игнор
  • при конфликте — merge properties

Удаление устаревших данных

Используется стратегия TTL:

store.createIndex("updatedAt", "updatedAt");

Очистка:

  • выборка старше N дней
  • пакетное удаление
  • выполнение в idle-time

Кэширование слоёв и чанков

Для больших датасетов применяется разбиение:

  • слой → регионы
  • регионы → тайлы
  • тайлы → features

Структура:

/tiles/14/4823/6160
/tiles/14/4823/6161

Каждый чанк хранится как отдельный объект IndexedDB.


Синхронизация с сервером

При наличии backend-источника GeoJSON применяется модель:

Pull-based синхронизация

  • клиент запрашивает изменения по bbox
  • сервер возвращает diff

Push-based обновления

  • WebSocket поток
  • инвалидация локальных объектов
  • повторная запись в IndexedDB

Ограничения IndexedDB в геопространственных задачах

Несмотря на гибкость, существуют системные ограничения:

  • отсутствие native spatial index
  • высокая стоимость cursor-итераций
  • блокировка транзакций при больших батчах
  • непредсказуемая производительность в Safari
  • необходимость ручного управления консистентностью

Минимизация нагрузки на WebGL слой Mapbox GL JS

Основная ошибка — передача слишком частых setData. Оптимальная стратегия:

  • хранить полные данные в IndexedDB
  • передавать только viewport subset
  • использовать кластеризацию при высокой плотности
  • избегать полной перерисовки слоя

Кластеризация перед рендерингом

Перед передачей данных в источник применяется агрегация:

  • grid clustering
  • spatial hashing
  • grouping by zoom level

Пример логики:

if (zoom < 10) {
  return cluster(features);
}

Использование Web Workers

Чтобы избежать блокировки UI:

  • запрос IndexedDB выполняется в worker
  • подготовка GeoJSON там же
  • передача через postMessage

Это особенно важно при объёмах > 50 000 объектов.


Инкрементальная загрузка данных

Вместо полной выборки:

  • загружаются только новые tiles
  • старые остаются в памяти
  • происходит частичное обновление source

Стратегия управления памятью

При длительной работе карты:

  • ограничение LRU-кэша тайлов
  • удаление невидимых bbox
  • очистка IndexedDB по приоритету актуальности
  • контроль дублирования объектов

Обработка геометрий перед сохранением

Перед записью в IndexedDB часто выполняется нормализация:

  • упрощение линий (Douglas-Peucker)
  • удаление лишних координат
  • приведение к WGS84
  • вычисление bbox

Подход к масштабируемым GeoJSON источникам

Сочетание IndexedDB и Mapbox GL JS позволяет реализовать архитектуру:

  • IndexedDB → долговременное хранилище
  • Worker → обработка геоданных
  • Mapbox Source → рендеринг WebGL
  • Tile index → выборка по viewport

Такая модель обеспечивает устойчивую работу при росте данных до сотен тысяч объектов без полной деградации интерактивности карты.