# Plan: Group Course Display
*Started 2026-03-13 — Status: DRAFT*

## Goal

Display a group's planned course (from `THCGroup.track` waypoints) as a polyline on the
map, with three display modes controlled by new UI widgets:

| Mode | What shows |
|---|---|
| **None** | No course lines |
| **Selected** | Only the currently selected group's course |
| **All Groups** | Every group whose side is enabled in the per-side toggle |

---

## Source Data

`THCGroup.track: ^THCPathRec` is the head of a doubly-linked list of waypoints.
Each `THCPathRec` has `DestLon`, `DestLat` (same sliver units as `THCGroup.sLon/sLat`).
Walking `track → next → next …` (nil-terminated) gives the full ordered course.
The polyline origin is the group's current position (`sLon`, `sLat`).

Conversion from slivers to globe units uses the existing `HCLon2Deg` / `HCLat2Deg`
helpers (already in scope from `uHCUtils`), then `DecimalToGlobeUnits`.

---

## Rendering Approach

Use `TPolylinePresenter` + `TGeoDataObject.Chains[0]` — identical to the range-ring
pattern in `uRangeRings.pas`, which already works well.

- Course lines live on a new **`layTracks: TGlobeLayer`** (global, like `layArrows`).
- `layTracks.Enabled := False` (render-only, not hit-testable — same as `layArrows`).
- Line colour = `SideColor(group.side)`, drawn as a thin solid pen.
- Each group's course object always exists while the group exists; visibility is
  controlled by populating vs. zeroing `Chains[0]` (zero-point chain → nothing drawn).

---

## Data Model Changes

### `TGroupMapEntry` — two new fields

```pascal
TrackPresenterID: Integer;   // -1 until first use (create once, reuse)
TrackObjectID:   Integer;    // -1 until first use
```

The track presenter and object are created in `AddGroup` alongside the icon presenter.
They are torn down in `RemoveGroupEntry` / `ClearAllGroups`.

---

## UI

Two new controls on the form (placed with the existing `rgVisibility` / `cgRangeRings`
panel area):

### `rgCourseMode: TRadioGroup`
Items: `'None'`, `'Selected'`, `'All Groups'`
Default: `'None'` (ItemIndex = 0).
`OnClick` handler → `CourseModeChanged`.

### `cgCourse: TCheckGroup`
Items match sides: `'Blue'`, `'Red'`, `'Green'`, `'Unknown'`.
Default: all checked.
Visible / enabled only when `rgCourseMode.ItemIndex = 2` (All Groups).
`OnItemClick` handler → `CourseSideToggled`.

---

## New Globals / State

```pascal
layTracks: TGlobeLayer;          // new global alongside layArrows etc.
gSelectedGroupUUID: string;      // UUID of currently selected icon object; '' = none
```

`gSelectedGroupUUID` is updated in `glbMapObjectSelectChange` by scanning `FGroupMap`
for the entry whose `IconObjectID` matches `GlobeObject.ID`.
(Linear scan over typically <50 entries — acceptable.)

---

## Key New Routines

### `BuildTrackChain(pGroup: PHCGroup; pChain: TPointStore)`
*(free procedure, defined in `mainform.pas` or extracted to `uCourseLines.pas`)*

```
pChain.Count := 0
add current position: HCLon2Deg(pGroup.sLon), HCLat2Deg(pGroup.sLat)
p := pGroup^.track
while p <> nil do
  add HCLon2Deg(p^.DestLon), HCLat2Deg(p^.DestLat)
  p := PHCPathRec(p^.next)
```

If the chain ends up with < 2 points (no waypoints), set `Count := 0` — nothing to draw.

### `function TfrmMain.GroupCourseVisible(pEntry: PGroupMapEntry; pGroup: PHCGroup): Boolean`

```
case rgCourseMode.ItemIndex of
  0 {None}:      Result := False
  1 {Selected}:  Result := pEntry^.UUID = gSelectedGroupUUID
  2 {AllGroups}: Result := cgCourse.Checked[pGroup^.side]
end
```

### `CourseModeChanged` / `CourseSideToggled` (event handlers)

Both call `RefreshAllCourseTracks`, then `glbMap.Invalidate`.

### `RefreshAllCourseTracks`
Walks every `FGroupMap` entry and, for each, calls the same chain-update logic used
in `UpdateGroup`.  Needed so that mode/side changes take effect immediately without
waiting for the next `GameTime` tick.
Caller must hold the group pointer — this means we also need the live `PHCGroup` to
rebuild the chain.

**Problem**: `FGroupMap` stores `PGroupMapEntry` but not `PHCGroup`.

**Solution A** — Store `PHCGroup` in `TGroupMapEntry` (add `GroupPtr: PHCGroup`).
Set it in `AddGroup`, update in `UpdateGroup`.  Use only for immediate refresh; the
authoritative data always comes from the `GameTime` list.

**Solution B** — On mode/side change, just zero all chains and let the next GameTime
tick repopulate at no extra cost.  Simpler; the visual lag is at most one tick (typically
< 1 s at normal game speed).

**Decision: Solution B.**  It avoids storing a stale pointer.  If needed, user can
trigger a repaint by advancing game time.  Implementation note: if the game is paused,
call `glbMap.Invalidate` after zeroing; the zeroed chains mean no lines are drawn until
the tick fires.

---

## Integration into Existing Lifecycle

### `AddGroup`
After the arrow block, add:
```
create TPolylinePresenter on layTracks, colour = SideColor(pGroup^.side)
capture TrackPresenterID
create TGeoDataObject on layTracks
if GroupCourseVisible(lEntry, pGroup) then BuildTrackChain(pGroup, lDataObj.Chains[0])
capture TrackObjectID
store into lEntry
```

### `UpdateGroup`
After updating arrow, add:
```
lTrackObj := layTracks.ObjectByID(pEntry^.TrackObjectID)
if lTrackObj <> nil then
begin
  if GroupCourseVisible(pEntry, pGroup) then
    BuildTrackChain(pGroup, TGeoDataObject(lTrackObj).Chains[0])
  else
    TGeoDataObject(lTrackObj).Chains[0].Count := 0
end
```

### `RemoveGroupEntry`
Add teardown block (before icon teardown):
```
if pEntry^.TrackObjectID >= 0 then ...remove from layTracks...
if pEntry^.TrackPresenterID >= 0 then ...delete presenter + free...
```

### `ClearAllGroups`
```
layTracks.Clear
layTracks.Enabled := False   // TGlobeLayer.Clear resets Enabled
```

### `glbMapObjectSelectChange`
```
if GlobeObject <> nil then
begin
  // scan FGroupMap for matching IconObjectID
  gSelectedGroupUUID := ...
end
else
  gSelectedGroupUUID := ''
// if course mode = Selected, zero old chain + populate new (or just Invalidate
//   and let next tick handle it — Solution B)
glbMap.Invalidate
```

---

## `ClearGroups` compatibility note

`ClearGroups` calls `TPointPresenter(pLayer.Presenters[lC]).Image.Free` before clearing.
Track presenters are `TPolylinePresenter` (no Image), so `layTracks` must use its own
clear sequence — just `layTracks.Clear` (no image loop needed, same as `layArrows`).

---

## Implementation Order

1. Extend `TGroupMapEntry` with `TrackPresenterID`, `TrackObjectID`.
2. Add `layTracks` global; create in `FormCreate`; add to `ClearAllGroups`.
3. Add `BuildTrackChain` helper.
4. Add `GroupCourseVisible` method.
5. Add `gSelectedGroupUUID` global; wire `glbMapObjectSelectChange`.
6. Add `rgCourseMode` and `cgCourse` to the form (`.lfm`); add event handlers.
7. Integrate track create/update/remove into `AddGroup`, `UpdateGroup`, `RemoveGroupEntry`.
8. Test: None mode → no lines; Selected → one line follows selection; AllGroups + side
   toggles → correct subsets shown; ScenLoad clears all correctly.

---

## Resolved Design Decisions

- **Waypoint markers**: each waypoint node (including the final destination) is marked
  with a small dot. Implemented as a second `TGeoDataObject` on `layTracks` that holds
  the same set of waypoint `TPointLL` values; rendered via a `TCICG4PointPresenter` with
  `PointType = ppDot`, same colour as the line. Alternatively, store waypoints as a
  second chain on the same object if TGlobe4 allows mixed-presenter rendering — check
  at implementation time; separate object is the safe fallback.
- **Line style**: solid.
- **Track depth limit**: none.
