Синхронизация с таймлайном

Во многих картографических приложениях данные изменяются во времени. Перемещение транспорта, развитие погодных явлений, распространение пожаров, изменение состояния объектов инфраструктуры, исторические события и аналитические данные часто представлены в виде временных рядов. В таких случаях карта должна отображать состояние данных, соответствующее выбранному моменту времени.

Синхронизация с таймлайном представляет собой механизм, при котором положение ползунка времени, анимация воспроизведения или внешний источник временных данных управляют отображением объектов на карте.

В 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]
]);

Объект будет отображаться только в период своей активности.


Синхронизация через изменение GeoJSON

Иногда объём данных настолько велик, что фильтрация становится неэффективной. В этом случае обновляется сам источник данных.

Получение источника:

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
        }
    });
}

Маршрут постепенно растёт по мере продвижения времени.


Использование feature-state

Для сложных сценариев полезен механизм состояния объектов.

Установка состояния:

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);
}

Такой подход позволяет реализовать полноценный медиаплеер для временных данных.


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

При обработке десятков тысяч объектов следует учитывать производительность.

Основные методы оптимизации:

  • использовать фильтры вместо полного обновления GeoJSON;
  • минимизировать вызовы 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);

Такое разделение облегчает поддержку проекта, позволяет централизованно управлять всеми временными слоями и обеспечивает единый механизм синхронизации между интерфейсом пользователя, анимацией и отображением данных на карте.