# ADS-B Instrument Architecture

## Цель

У нас есть один приборный движок, который должен работать в двух режимах:

- `live`: реальный телефонный режим, где свои координаты приходят из GPS, а цели - из live API.
- `replay`: режим воспроизведения, где свои координаты приходят из загруженного IGC-трека, а цели - из архивных ADS-B снимков.

Важный принцип: визуализация прибора, кнопки, режимы, меню, CPA-математика и alert-логика живут в одном файле `mobile-field.js`. Replay-страница не должна копировать прибор, она только подменяет источники данных.

## Файлы

- `mobile-field.html` - live-оболочка прибора, `body[data-runtime="live"]`.
- `replay-field.html` - replay-оболочка прибора, `body[data-runtime="replay"]`.
- `mobile-field.css` - общий стиль прибора, кнопок, таблиц, плюс небольшой блок `.replay-panel`.
- `mobile-field.js` - общий движок прибора.
- `archive-map.html`, `archive-map.css`, `archive-map.js` - обычная веб-карта для просмотра всех архивных ADS-B треков без приборного форм-фактора.
- `tools/adsb_recorder.py` - серверный ADS-B логгер.
- `deploy/adsb-recorder.service` - systemd unit для логгера на VPS.

## Контракт данных прибора

`mobile-field.js` ожидает два состояния:

- `state.own`: наше положение и вектор движения.
- `state.aircraft`: массив целей в формате, близком к Airplanes.live API.

После этого одинаково работают:

- `analyzeAircraft()` - расчет относительного положения, CPA и alert state.
- `analyses()` - сортировка и ограничение целей.
- `drawInstrument()` - отрисовка радара, таблицы, режимов внимания/тревоги.
- кнопки `FULL`, `+`, `+/-`, `SET`, `-`.

## Live Runtime

В `live` режиме:

- `startGps()` запускает browser geolocation.
- `fetchTraffic()` раз в цикл запрашивает `https://api.airplanes.live/v2/point/{lat}/{lon}/{radius}`.
- цели получают `_receivedAtMs`, чтобы можно было кратко прогнозировать их движение между API-снимками.

## Replay Runtime

В `replay` режиме:

- GPS не запускается.
- пользователь загружает IGC-файл.
- `parseIgc()` читает дату `HFDTE` и все `B`-точки.
- таймлайн задает `state.replay.currentEpochMs`.
- `interpolateTrackPoint()` восстанавливает нашу позицию, высоту, курс, скорость и вертикальную скорость на выбранный момент.
- `loadReplayArchive()` читает `archive/manifest.json` и почасовые `*.ndjson.gz` файлы за дату трека.
- `nearestReplaySnapshot()` выбирает ближайший ADS-B снимок в пределах 8 секунд.
- найденные цели передаются в `state.aircraft`, после чего используется тот же приборный движок.

## Формат архива

Архив лежит на VPS:

```text
/opt/adsb-viewer/archive/YYYY-MM-DD/HH.ndjson.gz
/opt/adsb-viewer/archive/manifest.json
```

Одна строка `NDJSON` - один снимок:

```json
{
  "schema": "adsb-snapshot-v1",
  "capturedAt": "2026-07-04T21:12:45.768657+03:00",
  "epochMs": 1783188765768,
  "source": "airplanes.live",
  "center": { "lat": 37.076111, "lon": 29.344722 },
  "radiusNm": 27,
  "total": 13,
  "ac": []
}
```

Для replay важно сохранять исходный массив `ac`, потому что это позволяет позже улучшать алгоритм без потери старых данных.

Внутри `ac` мы не выкидываем метаданные борта: сохраняются `hex`, `flight`, `r`, `t`, `desc`, `category`, скорости/высоты, флаги и любые дополнительные поля, которые возвращает Airplanes.live. Это нужно, чтобы позже можно было переиграть архив с новой логикой классификации или отображения.

## VPS Recorder

Сервис:

```text
adsb-recorder.service
```

Параметры по умолчанию:

- центр: Çameli `37.076111, 29.344722`
- радиус: `27 NM` примерно `50 км`, диаметр области около `100 км`
- окно записи: `09:00-19:00 Europe/Istanbul`
- интервал: `1.1 c`
- хранение: `30 дней`

Проверки:

```bash
systemctl status adsb-recorder.service
journalctl -u adsb-recorder.service -f
find /opt/adsb-viewer/archive -maxdepth 2 -type f
```

## Правило будущих правок

Если меняется внешний вид, кнопки, режимы, меню, scale, alert или CPA, править нужно общий `mobile-field.js` и `mobile-field.css`. Не делать отдельную копию логики для replay.

Если меняется только источник данных replay, править функции `parseIgc`, `loadReplayArchive`, `applyReplayState`.

Если меняется только источник live-данных, править `startGps`, `fetchTraffic`, `updateOwnFromPosition`.

## Archive Map

`archive-map.html` - отдельный аналитический интерфейс, не имитирующий прибор. Он читает тот же архив:

- `archive/manifest.json`
- `archive/YYYY-MM-DD/HH.ndjson.gz`

Страница строит треки всех целей по `hex`, показывает текущие позиции на выбранном времени, список активных целей и позволяет проигрывать день по таймлайну. Это не копия приборной логики: страница нужна для визуальной проверки качества архива и анализа движения всех бортов.

В режиме `Коридоры` страница локально строит дополнительный контрастный слой по всем загруженным трекам: если в зоне прошел хотя бы один самолет, участок подсвечивается как коридор. Период выбирается как день, неделя или месяц относительно выбранной даты. Ширина коридора рассчитывается от текущего масштаба карты примерно как 1 км на местности, поэтому при изменении зума толщина слоя пересчитывается. Диапазон высот фильтрует слой снизу и сверху с шагом 250 м: точки вне выбранного диапазона не попадают в коридоры, а трек разрывается. Градиент высоты можно включать/выключать; по умолчанию зоны такие: красный ниже 3000 м, желтый 3000-4500 м, зеленый 4500-6000 м, синий выше 6000 м. Пороги можно менять бегунками.

Карта классифицирует типы бортов эвристически по `category`, `t`, `desc`, `flight`, `operator`, `dbFlags` и похожим полям:

- `passenger-heavy` - большие пассажирские самолеты/лайнеры.
- `passenger-small` - малые и средние пассажирские реактивные самолеты.
- `business-jet` - бизнес-джеты/турбоджеты.
- `turboprop` - турбопропы, ATR/Dash/Saab/PC-12/King Air и похожие.
- `light-prop` - легкая поршневая авиация, Cessna/Piper/Cirrus/DA и похожие.
- `cargo` - грузовые/почтовые борта, если это видно по типу, описанию, оператору или позывному.
- `helicopter` - вертолеты/rotorcraft.
- `ultralight` - сверхлегкие, планеры, мотодельтапланы, UAV, если это видно из данных.
- `military` - военные борта, если API дал флаги или это видно по типу/описанию/позывному.

Военный, грузовой и сверхлегкий типы не являются гарантированными ADS-B полями: если источник не пометил борт или метаданные скудные, классификация может остаться `light-prop` или одной из обычных гражданских категорий.
