Mapbox GL JS построена вокруг реактивной модели данных: карта не
перерисовывается целиком при изменении информации, а пересчитывает
только те слои, которые зависят от обновлённого источника данных.
Центральным механизмом работы с динамическими пространственными данными
выступает GeoJSON source, поддерживающий оперативное обновление через
setData.
GeoJSON-источник в Mapbox GL JS представляет собой связующее звено между пользовательскими данными и рендерингом WebGL. Любое изменение объекта GeoJSON немедленно влияет на визуализацию всех слоёв, связанных с этим источником, при условии корректного вызова обновления.
Внутри Mapbox GL JS данные разделяются на два уровня:
GeoJSON source может быть:
Динамическое поведение обеспечивается методом:
map.getSource('points').setData(geojson);
Каждый вызов setData инициирует:
Структура источника создаётся один раз:
map.addSource('vehicles', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: []
}
});
Слой, использующий данные:
map.addLayer({
id: 'vehicles-layer',
type: 'circle',
source: 'vehicles',
paint: {
'circle-radius': 6,
'circle-color': '#ff5500'
}
});
Динамическое обновление:
function updateVehicles(newData) {
const source = map.getSource('vehicles');
source.setData(newData);
}
На практике GeoJSON редко обновляется вручную. Чаще используется поток данных:
const socket = new WebSocket('wss://example.com/vehicles');
socket.onmess age = (event) => {
const data = JSON.parse(event.data);
const geojson = {
type: 'FeatureCollection',
features: data.map(item => ({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [item.lng, item.lat]
},
properties: {
id: item.id,
speed: item.speed
}
}))
};
map.getSource('vehicles').setData(geojson);
};
Ключевой момент — каждый пакет данных должен формировать полный GeoJSON, а не дельту.
Метод setData заменяет весь источник целиком. Это
означает:
При больших наборах данных (10k–100k объектов) это становится узким местом.
Частые вызовы setData приводят к избыточным
перерисовкам. Решение — агрегация данных:
let buffer = [];
let scheduled = false;
function pushUpdate(item) {
buffer.push(item);
if (!scheduled) {
scheduled = true;
requestAnimationFrame(() => {
flushBuffer();
scheduled = false;
});
}
}
function flushBuffer() {
const source = map.getSource('vehicles');
const geojson = {
type: 'FeatureCollection',
features: buffer.map(toFeature)
};
buffer = [];
source.setData(geojson);
}
Вместо полного пересоздания GeoJSON можно хранить состояние отдельно:
const state = new Map();
function updateItem(item) {
state.set(item.id, item);
const features = Array.from(state.values()).map(toFeature);
map.getSource('vehicles').setData({
type: 'FeatureCollection',
features
});
}
Подход полезен при небольших и средних объёмах данных, но требует контроля памяти.
При росте числа объектов важны следующие приёмы:
Mapbox GL JS поддерживает serverless clustering:
map.addSource('points', {
type: 'geojson',
data: geojson,
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 50
});
Кластеризация снижает нагрузку на GPU и уменьшает количество отрисовываемых объектов.
Слои позволяют ограничивать отображение:
map.setFilter('vehicles-layer', [
'>',
['get', 'speed'],
10
]);
Фильтрация уменьшает количество визуализируемых элементов без изменения источника.
Разделение GeoJSON на несколько источников уменьшает стоимость пересчёта:
Хотя GeoJSON не поддерживает частичный update, Mapbox GL JS предоставляет механизм состояния объектов:
map.setFeatureState(
{ source: 'vehicles', id: 'vehicle-1' },
{ speed: 80 }
);
И слой может использовать выражения:
'circle-color': [
'case',
['>', ['feature-state', 'speed'], 60],
'#ff0000',
'#00ff00'
]
Преимущество:
Ограничение:
id у featuresФормирование структуры данных критично для производительности:
function toFeature(item) {
return {
type: 'Feature',
id: item.id,
geometry: {
type: 'Point',
coordinates: [item.longitude, item.latitude]
},
properties: {
speed: item.speed,
heading: item.heading
}
};
}
Рекомендации:
idДля плавного движения объектов используется интерполяция:
function animate(from, to, t) {
return [
from[0] + (to[0] - from[0]) * t,
from[1] + (to[1] - from[1]) * t
];
}
И цикл обновления:
function tick() {
const features = state.map(item => ({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: animate(item.prev, item.next, item.progress)
},
properties: item.props
}));
map.getSource('vehicles').setData({
type: 'FeatureCollection',
features
});
requestAnimationFrame(tick);
}
При потоковых данных возможны ситуации:
Решение — версионирование:
socket.onmess age = (event) => {
const payload = JSON.parse(event.data);
if (payload.version < currentVersion) return;
currentVersion = payload.version;
map.getSource('vehicles').setData(payload.geojson);
};
Если getSource вызывается до загрузки стиля:
map.on('load', () => {
const source = map.getSource('vehicles');
});
Или проверка:
if (map.getSource('vehicles')) {
map.getSource('vehicles').setData(data);
}
Важно учитывать асинхронность загрузки стиля карты.
Каждый вызов setData вызывает:
Факторы нагрузки:
Оптимизация достигается не только снижением данных, но и уменьшением частоты обновлений.
В гибридных системах:
Пример архитектуры:
Это снижает нагрузку на сервер и клиент одновременно.
let timeout;
function update(data) {
clearTimeout(timeout);
timeout = setTimeout(() => {
map.getSource('layer').setData(data);
}, 100);
}
let pending = false;
function schedule(data) {
if (pending) return;
pending = true;
requestAnimationFrame(() => {
map.getSource('layer').setData(data);
pending = false;
});
}
Иногда данные требуют нормализации:
function sanitize(features) {
return features.filter(f =>
Array.isArray(f.geometry.coordinates)
);
}
Типичные области применения:
Во всех случаях ключевой механизм остаётся одинаковым: пересборка
GeoJSON через setData и минимизация частоты вызовов.