Во многих картографических приложениях данные изменяются во времени. Перемещение транспорта, развитие погодных явлений, распространение пожаров, изменение состояния объектов инфраструктуры, исторические события и аналитические данные часто представлены в виде временных рядов. В таких случаях карта должна отображать состояние данных, соответствующее выбранному моменту времени.
Синхронизация с таймлайном представляет собой механизм, при котором положение ползунка времени, анимация воспроизведения или внешний источник временных данных управляют отображением объектов на карте.
В Mapbox GL JS подобная функциональность реализуется средствами JavaScript через:
Наиболее распространённый подход заключается в хранении временной метки внутри каждого объекта GeoJSON.
Пример набора данных:
const events = {
type: "FeatureCollection",
features: [
{
type: "Feature",
properties: {
timestamp: "2025-01-01T10:00:00Z",
name: "Событие A"
},
geometry: {
type: "Point",
coordinates: [37.62, 55.75]
}
},
{
type: "Feature",
properties: {
timestamp: "2025-01-01T12:00:00Z",
name: "Событие B"
},
geometry: {
type: "Point",
coordinates: [37.65, 55.77]
}
}
]
};
Каждый объект содержит собственную временную метку, которая затем используется для отображения или скрытия объекта в зависимости от текущей позиции таймлайна.
Чаще всего временная шкала реализуется через элемент
<input type="range">.
<input
id="timeline"
type="range"
min="0"
max="24"
step="1"
value="0"
/>
Получение текущего значения:
const timeline = document.getElementById("timeline");
timeline.addEventListener("input", (event) => {
const hour = Number(event.target.value);
console.log(hour);
});
После изменения положения ползунка запускается обновление данных карты.
Наиболее производительный способ синхронизации заключается в использовании фильтров Mapbox GL JS.
Добавление слоя:
map.addLayer({
id: "events",
type: "circle",
source: "events",
paint: {
"circle-radius": 6,
"circle-color": "#ff0000"
}
});
Изменение фильтра:
function updateTime(hour) {
map.setFilter("events", [
"<=",
["get", "hour"],
hour
]);
}
При перемещении таймлайна:
timeline.addEventListener("input", (e) => {
updateTime(Number(e.target.value));
});
Отображаться будут только объекты, удовлетворяющие условию.
Нередко требуется показывать не только текущий момент времени, но и всю историю до него.
Например:
map.setFilter("events", [
"all",
[">=", ["get", "hour"], 0],
["<=", ["get", "hour"], currentHour]
]);
В результате на карте постепенно появляется всё больше объектов по мере продвижения таймлайна.
Такой режим часто используется для:
Для показа объектов строго в выбранный момент времени используется точное сравнение:
map.setFilter("events", [
"==",
["get", "hour"],
currentHour
]);
На каждом шаге будут отображаться только данные выбранного периода.
Некоторые объекты существуют в течение определённого промежутка времени.
Пример данных:
{
type: "Feature",
properties: {
start: 8,
end: 14
},
geometry: {
type: "Point",
coordinates: [37.6, 55.7]
}
}
Фильтр активности объекта:
map.setFilter("events", [
"all",
["<=", ["get", "start"], currentTime],
[">=", ["get", "end"], currentTime]
]);
Объект будет отображаться только в период своей активности.
Иногда объём данных настолько велик, что фильтрация становится неэффективной. В этом случае обновляется сам источник данных.
Получение источника:
const source = map.getSource("events");
Формирование нового набора объектов:
function updateFeatures(time) {
const filtered = allFeatures.filter(feature => {
return feature.properties.hour <= time;
});
source.setData({
type: "FeatureCollection",
features: filtered
});
}
Обновление выполняется при каждом изменении временной шкалы.
Для автоматического воспроизведения временной шкалы применяется механизм кадров браузера.
let currentHour = 0;
Функция воспроизведения:
function play() {
currentHour++;
updateTime(currentHour);
if (currentHour < 24) {
requestAnimationFrame(play);
}
}
Запуск:
play();
Однако такой вариант слишком быстрый и обычно требует ограничения скорости.
Чаще используется комбинация requestAnimationFrame() и
временных интервалов.
let lastUpdate = 0;
let currentHour = 0;
function animate(timestamp) {
if (timestamp - lastUpdate > 500) {
currentHour++;
updateTime(currentHour);
lastUpdate = timestamp;
}
if (currentHour < 24) {
requestAnimationFrame(animate);
}
}
Запуск:
requestAnimationFrame(animate);
В данном случае новый шаг происходит каждые полсекунды.
Таймлайн обычно сопровождается текстовым отображением текущего времени.
HTML:
<span id="current-time"></span>
Jav * aScript:
const label =
document.getElementById("current-time");
Обновление:
function updateTime(hour) {
label.textContent = `${hour}:00`;
map.setFilter("events", [
"<=",
["get", "hour"],
hour
]);
}
Теперь положение ползунка и карта всегда отображают одинаковое состояние.
Временная шкала может контролировать сразу несколько наборов данных.
Например:
function updateTime(time) {
map.setFilter("fires", [
"<=",
["get", "hour"],
time
]);
map.setFilter("roads", [
"<=",
["get", "hour"],
time
]);
map.setFilter("stations", [
"<=",
["get", "hour"],
time
]);
}
Все слои будут синхронно реагировать на изменение времени.
Одной из наиболее востребованных задач является воспроизведение движения транспорта.
Исходные данные:
const route = [
[37.61, 55.75],
[37.62, 55.76],
[37.64, 55.77],
[37.66, 55.78]
];
Создание источника:
map.addSource("vehicle", {
type: "geojson",
data: {
type: "Feature",
geometry: {
type: "Point",
coordinates: route[0]
}
}
});
Обновление позиции:
function move(index) {
map.getSource("vehicle").setData({
type: "Feature",
geometry: {
type: "Point",
coordinates: route[index]
}
});
}
Состояние объекта становится напрямую связано с таймлайном.
Если данные содержат редкие измерения, движение может выглядеть рывками.
Для плавной анимации вычисляется промежуточное положение.
function interpolate(a, b, t) {
return [
a[0] + (b[0] - a[0]) * t,
a[1] + (b[1] - a[1]) * t
];
}
Использование:
const position =
interpolate(pointA, pointB, 0.5);
Будет получена координата, расположенная посередине между двумя точками.
Часто требуется показывать не весь маршрут, а только уже пройденную часть.
function updateRoute(index) {
const visibleCoordinates =
route.slice(0, index + 1);
map.getSource("path").setData({
type: "Feature",
geometry: {
type: "LineString",
coordinates: visibleCoordinates
}
});
}
Маршрут постепенно растёт по мере продвижения времени.
Для сложных сценариев полезен механизм состояния объектов.
Установка состояния:
map.setFeatureState(
{
source: "events",
id: 15
},
{
active: true
}
);
Использование в стиле:
"circle-color": [
"case",
["feature-state", "active"],
"#ff0000",
"#999999"
]
Теперь таймлайн может менять визуальное состояние объектов без полной перезагрузки данных.
Вместо скрытия объектов иногда удобнее плавно изменять их прозрачность.
map.setPaintProperty(
"events",
"circle-opacity",
[
"interpolate",
["linear"],
["get", "hour"],
currentHour - 2, 0,
currentHour, 1
]
);
Появление объектов становится визуально более плавным.
Создание состояния проигрывателя:
let playing = false;
Запуск:
function start() {
playing = true;
requestAnimationFrame(step);
}
Остановка:
function stop() {
playing = false;
}
Обработчик кадров:
function step() {
if (!playing) {
return;
}
currentHour++;
updateTime(currentHour);
requestAnimationFrame(step);
}
Такой подход позволяет реализовать полноценный медиаплеер для временных данных.
При обработке десятков тысяч объектов следует учитывать производительность.
Основные методы оптимизации:
setData;feature-state для визуальных изменений.Плохая практика:
source.setData(bigGeoJson);
на каждом кадре анимации.
Предпочтительный подход:
map.setFilter(...)
или
map.setFeatureState(...)
которые выполняются значительно быстрее.
Таймлайн может зависеть от времени воспроизведения видео.
video.addEventListener("timeupdate", () => {
const currentTime =
Math.floor(video.currentTime);
updateTime(currentTime);
});
Карта становится визуальным сопровождением видеоматериала.
Для отображения текущего состояния системы используется системное время.
setInterval(() => {
const now = new Date();
const hour = now.getHours();
updateTime(hour);
}, 1000);
Подход применяется в системах мониторинга транспорта, энергетики, телеметрии и диспетчеризации.
В крупных приложениях удобно выделять отдельный управляющий объект.
class TimelineController {
constructor(map) {
this.map = map;
this.currentTime = 0;
}
setTime(time) {
this.currentTime = time;
this.updateLayers();
}
updateLayers() {
this.map.setFilter(
"events",
[
"<=",
["get", "hour"],
this.currentTime
]
);
}
}
Использование:
const timelineController =
new TimelineController(map);
timelineController.setTime(12);
Такое разделение облегчает поддержку проекта, позволяет централизованно управлять всеми временными слоями и обеспечивает единый механизм синхронизации между интерфейсом пользователя, анимацией и отображением данных на карте.