Реплей задачи соревнований: пилоты летят над 3D-рельефом по своим GPS-трекам
Типичная соревновательная задача: 38 пилотов летят над 3D-рельефом по своим трекам, а 324 тысячи GPS-точек приходят в браузер файлом на 1,6 МБ.
Задача
sla.events — наша платформа для соревнований по парапланерному и дельтапланерному спорту: календарь, результаты, рейтинги. После каждой задачи пилоты загружают на неё свои GPS-треки. Всё прекрасно, но сайту очень не хватало реплея треков. Задача эта настолько сложная, что существует всего несколько популярных сервисов реплея. И все они основаны на одной и той же библиотеке Cesium. Для разработчиков она имеет низкий порог вхождения, и картинка «на выходе» очень детальная и красивая. Обратная сторона медали — очень большие объёмы передаваемых данных и высокая требовательность к железу. Использовать такой реплей в полевых условиях очень сложно, часто невозможно. Нужен был альтернативный реплей: может быть, не такой эффектный, как на Cesium, но быстрый и нетребовательный к железу.
Решение
Мы подобрали уникальный технологический стек: он лёгкий, быстрый, но даёт картинку хорошего качества даже на слабых устройствах. Несколько десятков треков одного таска можно смотреть одновременно на одной карте с общей шкалой времени. Ограничением становится не ширина интернет-канала и возможности устройства, а комфорт пользователя: просматривать больше 10−15 треков одновременно достаточно сложно даже на больших экранах. Цилиндры и оптимальный маршрут отображаются на карте так же, как в самой задаче. Карту можно наклонить и включить трёхмерный рельеф. Тогда пилоты летят на своей настоящей высоте, а от трека к земле опускается полупрозрачная «шторка» — видно, и насколько пилот высоко над склоном, и где он набирал высоту.
2D и 3D




Один и тот же момент задачи: на плоской карте и на рельефе
Как трек попадает в браузер
На сервере. Пилоты загружают треки в IGC, GPX или KML — всё, что не IGC, сервер переводит в IGC. Для повтора трек обрезается окном задачи, из него выбрасывается время, когда пилот стоял на месте, а точки кодируются дельтами: первая — целиком, каждая следующая — разницей с предыдущей.
На проводе. Ответ уходит в двоичном формате MessagePack. В задаче с 38 пилотами 324 тысячи точек занимают 1,6 МБ вместо 3,9 МБ в JSON. Геометрию задачи сервер берёт из файла FSComp или XCTrack и сам считает оптимальный маршрут по правилам FAI.
Передача треков
{
title: 'МС. Параплан-маршрутный полет Peregrine Falkon. nonFAI, Задача 3',
tracks: [
{ number: 30, name: '…', pts: [
[4283512, 4721035, 612, 1782976516], // первая точка: широта×1e5, долгота×1e5, высота, время
[3, -2, 1, 1], // дальше — только разница с предыдущей
[4, -1, 2, 1],
…
] },
… // ещё 37 пилотов
],
geo: { … }, // цилиндры, линии старта и финиша, оптимальный маршрут
marks: [{ text: 'Stop', ts: … }],
} Структура ответа API повтора; координаты и время в примере условные.
three.js внутри MapLibre
Карта — MapLibre GL с рельефом из открытых данных. Треки в 3D рисует three.js, но не отдельным холстом поверх карты, а внутри неё: каждый трек — собственный слой MapLibre, и все слои делят с картой один WebGL-контекст и одну камеру. Поэтому при вращении и наклоне треки не отстают от рельефа ни на кадр.
Маркер пилота держит постоянный размер на экране при любом масштабе. А сам three.js — это ещё 189 КБ, и браузер скачивает его, только когда посетитель включает 3D.
Ссылка — это набор пилотов
В адресе повтора после # стоят номера пилотов, которых видно на карте. Пилот может отправить ссылку на свой полёт или на сравнение с соперником, и она откроется с теми же пилотами. Если поменять номера прямо в адресе, приложение догрузит только недостающие треки.
Дальше — таймлайн со скоростью от 1x до 8x, перемотка с клавиатуры и камера, которая следует за выбранным пилотом.
Один движок — три приложения
Карта, рельеф, проигрыватель треков и виджеты вынесены в отдельный движок на TypeScript. На нём работают сам повтор, карты задач и полётных мест на sla.events и task.er — планировщик задач для организаторов, который работает и без интернета. Исправление в движке доходит до всех трёх.


