В MapLibre GL JS геоданные в формате GeoJSON подключаются через
источник типа geojson. Такой источник работает как
реактивная обёртка над набором пространственных объектов, которые
визуализируются слоями карты. Обновление данных в этом источнике
реализуется через полную замену содержимого, а не точечное изменение
отдельных объектов, что определяет архитектуру динамических карт.
Инициализация источника выполняется при загрузке карты. Источник становится частью внутреннего хранилища стиля и доступен по идентификатору.
map.on('load', () => {
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': '#1978ff'
}
});
});
В этом контексте data представляет собой стандартный
объект GeoJSON. MapLibre хранит его внутри Web Worker для обеспечения
производительности при большом количестве объектов.
Обновление GeoJSON осуществляется через метод setData,
вызываемый у источника.
const source = map.getSource('points-source');
source.setData({
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [30.5, 50.5]
},
properties: {
id: 1
}
}
]
});
Каждый вызов setData полностью заменяет предыдущее
состояние источника. Внутренняя модель не выполняет дифференциальное
обновление объектов, поэтому даже небольшое изменение требует передачи
полного GeoJSON.
Модель обновления через замену имеет ряд последствий:
idFeatureCollectionПри работе с большими наборами данных важно учитывать стоимость повторной отправки всей структуры.
Типичный подход заключается в хранении данных во внешнем состоянии приложения и генерации нового GeoJSON при каждом обновлении.
let features = [];
function updateFeaturePosition(id, newCoordinates) {
features = features.map(f => {
if (f.properties.id === id) {
return {
...f,
geometry: {
type: 'Point',
coordinates: newCoordinates
}
};
}
return f;
});
map.getSource('points-source').setData({
type: 'FeatureCollection',
features
});
}
Такая схема отражает иммутабельный стиль обновления данных, при котором исходный массив не модифицируется напрямую.
При поступлении данных в реальном времени каждое сообщение может изменять положение объектов или добавлять новые элементы.
socket.onmess age = (event) => {
const update = JSON.parse(event.data);
features = features.map(f => {
if (f.properties.id === update.id) {
return {
...f,
geometry: {
type: 'Point',
coordinates: update.coordinates
}
};
}
return f;
});
map.getSource('points-source').setData({
type: 'FeatureCollection',
features
});
};
При высокой частоте сообщений возникает необходимость агрегации обновлений.
async function refreshData() {
const response = await fetch('/api/points');
const geojson = await response.json();
map.getSource('points-source').setData(geojson);
}
setInterval(refreshData, 5000);
Этот подход прост, но создаёт регулярные пересоздания данных независимо от реальных изменений.
При поступлении множества событий за короткий интервал выполняется
накопление изменений с последующим единичным вызовом
setData.
let pending = false;
function scheduleUpdate() {
if (pending) return;
pending = true;
requestAnimationFrame(() => {
map.getSource('points-source').setData({
type: 'FeatureCollection',
features
});
pending = false;
});
}
Такой подход синхронизирует обновления с циклом рендеринга браузера.
Снижение объёма данных уменьшает нагрузку на сериализацию:
Для Point наиболее частая операция — изменение
координат. В MapLibre нет механизма частичного обновления, поэтому
пересобирается вся коллекция.
Для LineString и Polygon обновление требует
полной замены массива координат:
feature.geometry.coordinates = newCoordinates;
map.getSource('points-source').setData({
type: 'FeatureCollection',
features
});
При сложных геометриях стоимость пересборки возрастает.
При включённой кластеризации источник выполняет агрегацию точек в
worker. Обновление через setData автоматически приводит к
пересчёту кластеров.
map.addSource('clustered', {
type: 'geojson',
data: geojsonData,
cluster: true,
clusterRadius: 40
});
Любое изменение данных вызывает:
При больших наборах данных это становится наиболее затратной операцией.
При объёмах в десятки тысяч объектов прямое использование
setData может приводить к просадкам производительности.
Используются следующие стратегии:
Данные делятся по категориям или регионам:
Разделение по тайлам или регионам позволяет обновлять только часть данных.
Частая ошибка — создание нового GeoJSON с полным копированием всех объектов при каждом обновлении. Это приводит к лишней нагрузке на GC и сериализацию.
Более эффективный подход заключается в переиспользовании структуры и изменении только необходимых частей:
function updateInPlace(id, coords) {
const feature = featureMap.get(id);
feature.geometry.coordinates = coords;
map.getSource('points-source').setData({
type: 'FeatureCollection',
features: Array.from(featureMap.values())
});
}
Хотя подход сохраняет ссылочную структуру, сам вызов
setData остаётся полной заменой.
В архитектурах на основе состояния (Redux, MobX, Zustand) GeoJSON часто является производным представлением.
store.subscribe(() => {
const state = store.getState();
const geojson = {
type: 'FeatureCollection',
features: state.points.map(p => ({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: p.coordinates
},
properties: p.meta
}))
};
map.getSource('points-source').setData(geojson);
});
Такой подход упрощает управление, но требует контроля частоты обновлений.
Высокочастотные обновления приводят к блокировке UI и росту времени перерисовки.
GeoJSON должен строго соответствовать спецификации:
typeFeature и
FeatureCollectionОшибки в структуре могут приводить к молчаливому игнорированию обновления.
Создание новых объектов вместо изменения существующих увеличивает нагрузку на сборщик мусора.
На практике используется единый слой абстракции:
setDatafunction renderSource(state) {
map.getSource('points-source').setData(toGeoJSON(state));
}
Такой слой обеспечивает предсказуемость обновлений и отделяет визуализацию от логики данных.