Работа с MapLibre GL JS практически всегда предполагает взаимодействие с источниками данных формата GeoJSON. При этом статические файлы быстро перестают быть достаточными: координаты обновляются в реальном времени, объекты появляются и исчезают, а атрибуты требуют пересчёта прямо в интерфейсе. В таких сценариях ключевым становится динамическое создание и модификация GeoJSON-структур прямо в JavaScript.
GeoJSON как формат остаётся неизменным по спецификации, но его содержимое в приложении может формироваться полностью программно: из API, событий пользователя, телеметрии, потоковых данных или локальных вычислений.
MapLibre GL JS работает с источниками типа geojson, где
данные должны соответствовать стандарту FeatureCollection:
const geojson = {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: {
type: "Point",
coordinates: [66.9237, 48.0196]
},
properties: {
name: "Объект"
}
}
]
};
Ключевой момент: MapLibre не требует заранее фиксированного файла. Любой объект JavaScript, соответствующий спецификации GeoJSON, может быть передан как источник данных и обновлён в любой момент.
Динамическое формирование чаще всего начинается с пустой коллекции:
const dynamicGeojson = {
type: "FeatureCollection",
features: []
};
Далее структура наполняется в зависимости от входных данных.
const points = [
{ lon: 66.9, lat: 48.0, label: "A" },
{ lon: 66.95, lat: 48.02, label: "B" }
];
const geojson = {
type: "FeatureCollection",
features: points.map(p => ({
type: "Feature",
geometry: {
type: "Point",
coordinates: [p.lon, p.lat]
},
properties: {
label: p.label
}
}))
};
Такой подход позволяет строить слой полностью на основе данных, полученных из API или пользовательского ввода.
MapLibre использует принцип неизменяемости источника: вместо
частичного изменения массива требуется обновление всей GeoJSON-структуры
через setData.
map.addSource("dynamic-source", {
type: "geojson",
data: {
type: "FeatureCollection",
features: []
}
});
function addPoint(source, lon, lat) {
const data = source._data; // концептуально; в реальном коде хранят копию отдельно
data.features.push({
type: "Feature",
geometry: {
type: "Point",
coordinates: [lon, lat]
},
properties: {}
});
source.setData(data);
}
В корректной архитектуре данные хранятся вне MapLibre:
let state = {
type: "FeatureCollection",
features: []
};
function updateSource() {
map.getSource("dynamic-source").setData(state);
}
Один из наиболее распространённых сценариев — поступление координат в реальном времени.
const socket = new WebSocket("wss://example.com/stream");
socket.onmess age = (event) => {
const payload = JSON.parse(event.data);
state.features.push({
type: "Feature",
geometry: {
type: "Point",
coordinates: [payload.lon, payload.lat]
},
properties: {
timestamp: payload.time
}
});
map.getSource("dynamic-source").setData(state);
};
При высокой частоте обновлений важно контролировать производительность: не обновлять источник на каждое событие, а использовать буферизацию.
Частые вызовы setData приводят к перерасчёту слоя и
перерисовке карты. Оптимизация заключается в накоплении изменений:
let buffer = [];
function pushData(item) {
buffer.push(item);
}
setInterval(() => {
if (buffer.length === 0) return;
for (const item of buffer) {
state.features.push(item);
}
buffer = [];
map.getSource("dynamic-source").setData(state);
}, 200);
Такой подход снижает нагрузку и повышает плавность визуализации.
GeoJSON не предполагает мутацию объектов «по id» на уровне API, поэтому обновление выполняется через пересборку массива.
function updateFeature(id, newCoords) {
state.features = state.features.map(f => {
if (f.properties.id === id) {
return {
...f,
geometry: {
type: "Point",
coordinates: newCoords
}
};
}
return f;
});
map.getSource("dynamic-source").setData(state);
}
GeoJSON может быть результатом любых алгоритмов: интерполяции, кластеризации, геометрических преобразований.
function createCircle(lon, lat, radius, steps = 64) {
const coords = [];
for (let i = 0; i <= steps; i++) {
const angle = (i / steps) * 2 * Math.PI;
const dx = radius * Math.cos(angle);
const dy = radius * Math.sin(angle);
coords.push([lon + dx, lat + dy]);
}
return {
type: "Feature",
geometry: {
type: "Polygon",
coordinates: [coords]
},
properties: {}
};
}
Динамическая сборка часто включает агрегацию нескольких источников:
function buildGeojson(apiPoints, userPoints) {
return {
type: "FeatureCollection",
features: [
...apiPoints.map(p => ({
type: "Feature",
geometry: {
type: "Point",
coordinates: [p.lon, p.lat]
},
properties: { source: "api" }
})),
...userPoints.map(p => ({
type: "Feature",
geometry: {
type: "Point",
coordinates: [p.lon, p.lat]
},
properties: { source: "user" }
}))
]
};
}
Такой подход позволяет строить единый слой из разнородных данных без изменения конфигурации карты.
GeoJSON в MapLibre может содержать свойства, рассчитанные на лету:
function enrichFeature(feature) {
return {
...feature,
properties: {
...feature.properties,
magnitude: Math.sqrt(
feature.properties.x ** 2 +
feature.properties.y ** 2
)
}
};
}
Это позволяет использовать данные для стилизации:
paint: {
"circle-radius": ["interpolate", ["linear"], ["get", "magnitude"], 0, 5, 10, 20]
}
При динамическом создании GeoJSON критично учитывать размер структуры:
function trimState(maxItems) {
if (state.features.length > maxItems) {
state.features = state.features.slice(-maxItems);
}
}
Перед передачей в MapLibre данные часто проходят несколько этапов:
function pipeline(raw) {
return {
type: "FeatureCollection",
features: raw
.filter(r => r.valid)
.map(r => ({
type: "Feature",
geometry: {
type: "Point",
coordinates: [r.lon, r.lat]
},
properties: {
speed: r.speed
}
}))
};
}
При интенсивных обновлениях важно учитывать особенности рендеринга:
setData вызывает полное обновление источникаПрактика заключается в разделении данных:
Интерактивные сценарии часто формируют данные через клики:
map.on("click", (e) => {
state.features.push({
type: "Feature",
geometry: {
type: "Point",
coordinates: [e.lngLat.lng, e.lngLat.lat]
},
properties: {
created: Date.now()
}
});
map.getSource("dynamic-source").setData(state);
});
Вместо серверной подготовки GeoJSON можно выполнять расчёты прямо в браузере:
function filterByDistance(center, points, maxDist) {
return points.filter(p => {
const dx = p.lon - center[0];
const dy = p.lat - center[1];
return Math.sqrt(dx * dx + dy * dy) < maxDist;
});
}
В реальных приложениях используется разделение ответственности:
Такая схема делает динамическую работу с GeoJSON предсказуемой, масштабируемой и устойчивой к росту данных.