Custom source types

Расширяемая модель источников данных

Архитектура MapLibre GL JS строится вокруг унифицированной модели источников данных (sources), где каждый источник отвечает за получение, нормализацию и предоставление геоданных в формате, пригодном для рендеринга через WebGL. Стандартные типы источников — vector, raster, geojson, image, video — покрывают большинство сценариев, однако сложные приложения требуют интеграции нестандартных протоколов, форматов и потоков данных.

Custom source types предоставляют механизм расширения системы источников без модификации ядра рендерера. Это позволяет внедрять собственные реализации загрузки тайлов, динамической генерации геометрии, потоковой обработки данных и интеграции с внешними системами (IoT, real-time feeds, proprietary GIS backends).


Архитектурная модель custom source

Custom source в MapLibre GL JS представляет собой объект, который реализует набор жизненного цикла источника и контракт взаимодействия с тайловой системой.

Типичный custom source должен обеспечивать:

  • инициализацию при добавлении на карту
  • загрузку данных по тайловым координатам (z/x/y)
  • обновление при изменении состояния карты
  • освобождение ресурсов при удалении
  • синхронизацию с рендер-пайплайном

В отличие от geojson или vector источников, где логика загрузки фиксирована, custom source полностью определяет стратегию получения данных.


Регистрация пользовательского типа источника

В расширенных сценариях используется регистрация нового типа источника через API расширения стиля:

map.addSource('custom-data', {
  type: 'custom',
  data: {}
});

Однако сам по себе параметр type: 'custom' не определяет поведение. Реальная логика подключается через фабрику источника, которая регистрируется в стиле.

Внутренне MapLibre использует механизм наподобие:

maplibregl.Style.setSourceType('custom', CustomSourceImplementation);

Где CustomSourceImplementation — класс, реализующий контракт источника.


Контракт CustomSourceImplementation

Базовая структура пользовательского источника включает следующие методы:

class CustomSource {
  constructor(options) {
    this.options = options;
  }

  onAdd(map) {
    this.map = map;
    this.tileCache = new Map();
  }

  loadTile(tile, callback) {
    // загрузка или генерация данных для конкретного тайла
  }

  unloadTile(tile) {
    // освобождение памяти
  }

  reload() {
    // принудительное обновление всех тайлов
  }

  onRemove() {
    // очистка ресурсов
  }
}

Ключевым элементом является метод loadTile, который привязан к тайловой сетке Spherical Mercator (z/x/y). Именно он определяет, как данные попадают в рендеринг.


Тайловая модель и custom source

MapLibre использует тайловую пирамиду для оптимизации отображения. Custom source интегрируется в эту систему, получая координаты тайла:

  • z — уровень масштабирования
  • x, y — координаты тайла
  • географическая область тайла вычисляется автоматически

Пример обработки тайла:

loadTile(tile, callback) {
  const { z, x, y } = tile.tileID.canonical;

  fetch(`/tiles/${z}/${x}/${y}.json`)
    .then(res => res.json())
    .then(data => {
      tile.data = this.transform(data);
      callback(null);
    })
    .catch(err => callback(err));
}

Генерация геометрии на лету

Одно из ключевых применений custom source — динамическая генерация геометрии. В отличие от GeoJSON, данные не обязаны существовать заранее.

Сценарии:

  • генерация из математических моделей
  • агрегация больших датасетов на сервере
  • визуализация симуляций
  • потоковые данные (real-time tracking)

Пример генерации точек:

transform(raw) {
  return raw.items.map(item => ({
    type: 'Feature',
    geometry: {
      type: 'Point',
      coordinates: [item.lng, item.lat]
    },
    properties: {
      value: item.value
    }
  }));
}

Интеграция с Web Workers

При высоких нагрузках обработка данных в основном потоке становится узким местом. Custom source часто выносится в Web Worker.

Схема взаимодействия:

  • main thread инициирует загрузку тайла
  • worker выполняет трансформацию данных
  • результат возвращается через postMessage
  • тайл обновляется в рендерере

Пример упрощённого worker-подхода:

// worker.js
self.onmess age = (e) => {
  const { z, x, y } = e.data;

  const result = generateTile(z, x, y);

  self.postMessage({
    tileId: `${z}-${x}-${y}`,
    data: result
  });
};

Кэширование тайлов

Custom source обычно требует собственной стратегии кэширования:

  • LRU-кэш для ограниченной памяти
  • persistent cache через IndexedDB
  • сетевой кэш (HTTP caching)
  • мемоизация вычислений

Пример простого кэша:

class TileCache {
  constructor(limit = 200) {
    this.limit = limit;
    this.cache = new Map();
  }

  get(key) {
    const value = this.cache.get(key);
    if (value) {
      this.cache.delete(key);
      this.cache.set(key, value);
    }
    return value;
  }

  set(key, value) {
    if (this.cache.size >= this.limit) {
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }
    this.cache.set(key, value);
  }
}

Асинхронная модель обновления

Custom source обязан корректно работать с асинхронностью MapLibre рендерера. Изменения данных должны инициировать перерасчёт тайлов.

Типовой механизм:

updateData(newData) {
  this.data = newData;
  this.tileCache.clear();
  this.map.triggerRepaint();
}

triggerRepaint() обеспечивает перерисовку без изменения камеры, что важно для потоковых источников.


Поддержка потоковых данных

Custom source часто используется для streaming-данных:

  • GPS-трекинг транспорта
  • телеметрия датчиков
  • финансовые котировки
  • игровые симуляции

Архитектура включает буферизацию:

pushStreamPoint(point) {
  this.buffer.push(point);

  if (this.buffer.length > 1000) {
    this.flushBuffer();
  }
}

Географическая нормализация данных

Так как MapLibre работает в Web Mercator (EPSG:3857), custom source часто включает преобразование координат:

  • WGS84 → Mercator
  • локальные системы координат → глобальные
  • нормализация высотных данных

Пример:

function lngLatToMercator(lng, lat) {
  const x = lng * 20037508.34 / 180;
  let y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180);
  y = y * 20037508.34 / 180;
  return [x, y];
}

Ошибки и устойчивость

Custom source требует строгой обработки ошибок:

  • тайлы могут приходить в произвольном порядке
  • сеть может возвращать частичные данные
  • рендер может запрашивать устаревшие тайлы

Типовая стратегия:

  • игнор устаревших tileID
  • отмена fetch через AbortController
  • fallback на кэшированные данные
const controller = new AbortController();

fetch(url, { signal: controller.signal })
  .catch(err => {
    if (err.name === 'AbortError') return;
    console.error(err);
  });

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

Custom source напрямую влияет на:

  • время загрузки тайлов
  • CPU usage при трансформации
  • память (кэш геометрии)
  • частоту repaint

Критические оптимизации:

  • минимизация JSON
  • бинарные форматы (ArrayBuffer)
  • батчинг обновлений
  • предвычисление индексов

Сценарии применения

Custom source используется в системах, где стандартные источники недостаточны:

  • GIS-платформы с проприетарными форматами
  • визуализация больших потоков IoT-данных
  • 3D-симуляции поверх карты
  • генеративные карты (procedural mapping)
  • real-time транспортные системы

Взаимодействие со стилем и слоями

Custom source интегрируется в слойную модель MapLibre так же, как стандартные источники:

map.addLayer({
  id: 'custom-layer',
  type: 'circle',
  source: 'custom-data',
  paint: {
    'circle-radius': 6,
    'circle-color': '#ff5500'
  }
});

Рендерер не различает тип источника на уровне слоя — важна только структура возвращаемых данных (features).


Совместимость и эволюция API

Custom source API эволюционирует между версиями MapLibre GL JS. В зависимости от версии могут отличаться:

  • механизм регистрации типов
  • сигнатуры методов
  • поведение кэширования тайлов
  • интеграция с worker pipeline

Поэтому реализация обычно изолируется в адаптерном слое, отделяющем бизнес-логику от API MapLibre.