# TlogRouteExtractor

**Task**: AZ-836_tlog_route_extractor
**Name**: TlogRouteExtractor: extract active flight segment + coarsen tlog GPS to ≤N waypoints (AZ-835 C1)
**Description**: First building block of Epic AZ-835. Pure, testable function that consumes a `.tlog` binary and returns a `RouteSpec` (≤ N waypoints + suggested per-waypoint coverage radius) suitable for posting to satellite-provider's `POST /api/satellite/route` endpoint (consumed by AZ-835 C2 / AZ-838).
**Complexity**: 3 SP
**Dependencies**: AZ-697 (`load_tlog_ground_truth` — done); AZ-279 (WGS converter — done); AZ-835 (parent Epic)
**Component**: `src/gps_denied_onboard/replay_input/tlog_route.py` (new module under `replay_input/`)
**Tracker**: AZ-836 (https://denyspopov.atlassian.net/browse/AZ-836)
**Parent Epic**: AZ-835

Jira AZ-836 is the authoritative spec; this file is the in-workspace mirror.

## Public surface

```python
from dataclasses import dataclass
from pathlib import Path

@dataclass(frozen=True, slots=True)
class RouteSpec:
    waypoints: tuple[tuple[float, float], ...]   # (lat, lon), 1..max_waypoints
    suggested_region_size_meters: float           # per-waypoint coverage radius
    source_tlog: Path                             # provenance
    source_segment: tuple[int, int]               # (start_idx, end_idx) into tlog GPS rows
    total_distance_meters: float                  # along-track distance of active segment

class RouteExtractionError(ReplayInputAdapterError): ...

def extract_route_from_tlog(
    tlog: Path,
    *,
    max_waypoints: int = 10,
    min_takeoff_speed_m_s: float = 2.0,
    min_takeoff_altitude_agl_m: float = 5.0,
    douglas_peucker_tolerance_m: float | None = None,  # auto-computed if None
    region_size_meters: float = 500.0,
) -> RouteSpec: ...
```

Reuses `replay_input.tlog_ground_truth.load_tlog_ground_truth()` for GPS extraction — no MAVLink re-parsing.

## Active-segment detection

Trim leading + trailing rows where horizontal speed < `min_takeoff_speed_m_s` AND altitude AGL < `min_takeoff_altitude_agl_m`. Both thresholds configurable. If trimmed segment has < 2 fixes, raise `RouteExtractionError` with the explicit threshold values — no silent fallback to the full tlog.

## Coarsening

Douglas-Peucker in WGS84 with great-circle distance metric. Use the existing `helpers.wgs_converter` or `helpers.gps_compare` meter conversion — do NOT reimplement (check both first; pick whichever has the right primitive).

When `douglas_peucker_tolerance_m is None`, auto-compute by binary-search over the tolerance until `len(result) <= max_waypoints`. Halt at convergence (delta < 1 m) or 32 iterations.

## Validation

- `max_waypoints >= 1` (raise `ValueError`).
- `region_size_meters > 0` (raise `ValueError`).
- At least 1 fix from `GLOBAL_POSITION_INT` (preferred) or `GPS_RAW_INT` (fallback); if neither, `RouteExtractionError` referencing missing message types (mirrors AZ-697).
- Missing tlog file → `RouteExtractionError` (not bare `FileNotFoundError`) so callers can catch one error class.

## Acceptance criteria

| # | Criterion |
|---|-----------|
| AC-1 | Real Derkachi tlog → RouteSpec with `len(waypoints) <= 10`; every waypoint inside lat 50.0808..50.0832, lon 36.1070..36.1134 |
| AC-2 | Active-segment trim filters pre-takeoff stationary frames (synthetic 5+ stationary leading fixes → `source_segment[0] > 0`) |
| AC-3 | `max_waypoints=2` → exactly 2 waypoints |
| AC-4 | `max_waypoints=100` on N<100 tlog → N waypoints (no coarsening below natural fix count) |
| AC-5 | Missing tlog → `RouteExtractionError` with path; not `FileNotFoundError` |
| AC-6 | Tlog with no GPS → `RouteExtractionError` naming missing message types |
| AC-7 | `RouteSpec` is `frozen=True`, `slots=True`, all provenance fields populated |
| AC-8 | Auto-tolerance binary-search converges within 32 iters on a 200-fix synthetic trajectory |
| AC-9 | No I/O beyond tlog read; logging at DEBUG only |
| AC-10 | Unit tests cover: Derkachi happy path, small/large max_waypoints, missing tlog, missing GPS, custom DP tolerance, custom region size, synthetic stationary-leading trim |

## Out of scope

- Posting to satellite-provider (AZ-838 / C2)
- Route visualization on a map (future, AZ-700-style)
- Multi-tlog aggregation
- Live-stream tlog ingestion

## References

- Parent Epic: AZ-835 — https://denyspopov.atlassian.net/browse/AZ-835
- Reference tlog: `_docs/00_problem/input_data/flight_derkachi/derkachi.tlog`
- Reuse: `src/gps_denied_onboard/replay_input/tlog_ground_truth.py` (AZ-697), `src/gps_denied_onboard/helpers/gps_compare.py`
