Introduction:

This is a project for a museum to demonstrate a simulation of a 1950's to 1960's
vintage marine radar and air traffic control radar

The project will be implemented on a Geekom A8 Max
with AMD AI chip R9-8945HS with 32 GB ram

# Project: C++ OpenGL Radar Simulation
**Environment:** Ubuntu Linux (Remote SSH from Windows)
**Tech Stack:** C++20, OpenGL 3.3 Core, GLFW, GLAD, FreeType, GDAL (libgdal-dev)


The operating system is Linux (Ubuntu)
Details:

Distributor ID: Ubuntu
Description:    Ubuntu 25.10
Release:        25.10
Codename:       questing

The compiler is cpp (Ubuntu 15.2.0-4ubuntu4) 15.2.0

We plan to use the cmake for building.

Please add MIT license header to each file
Please add Author: Mark Allyn to each file

Here is the directory structure with files already installed:
All directories are in the new-radar top level directory. The 
entire directory list is /home/maallyn/new-radar on the Geekom.

./shaders
./shaders/CLAUDE.md
./glad
./glad/src
./glad/src/glad.c
./include
./include/glad
./include/glad/glad.h
./include/CLAUDE.md
./include/KHR
./include/KHR/khrplatform.h
./new-claude
./README.md
./CMakeLists.txt
./build
./build/CLAUDE.md
./CLAUDE.md
./.new-claude.swp
./LICENSE
./src
./src/CLAUDE.md
./map
./map/lidar_processed
./map/charts_enc
./map/charts_enc/US5WA45M.000
./map/charts_enc/n48_w123_1arc_v3.tif
./map/lidar_raw
./map/lidar_raw/wa2022_nooksack_dem_J1364940.zip
./map/lidar_raw/wa2016_west_dem_J1364939.zip


==================================================================

GENERAL STUFF

==================================================================

Please note that all on-screen text shall be white and fully
illuminated and is not subject to phosphor persistence or decay.

Exceptions:

Graticule text: should be incandescent for the bearing marks. 
Graticule text for all a-scope should be incandescent, not white
and not phosphor as they are dependent on glass graticules with etched
lines and text.

PPI Scope Range Ring Markers Text on the PPI scope range rings shall be blue
fade to yellow green as on p7 phosphor. Which is the same for ppi targets.

Please note that direction as stated here are True directions. 000 is True North

Maximum Range is 6 miles for marine type radar
Maximum Range is 20 miles for air traffic control radar.
Maximum Range is 100 miles for chain home
Maximum Range 10 miles for precision approach radar; graticule is
  incandescent showing the azimuth path and elevation path as described
  below in PAR description. Those graticules are etched glass for minimal
  parallax

The proposed location of the marine radar antenna is in the middle of Bellingham
Bay on a 100 foot platform. (This should be mentioned as fictitious in the description)
Location is 48.74361448950435 latitude, -122.56466911663048 longitude

The proposed location of the air traffic control radar is the Bellingham
airport (BLI) control tower. Latitude: 48° 47' 33.7" N ; Longitude: 122° 32' 15.1" W

The proposed location for the chain home would be at the original location on
the UK coast facing the European Continent
  
The following types of scope will be used; (note that these are not all
going to show at once. They will be selectable using a push button (a letter on
the keyboard until I get physical buttons that are connected to a gpio pin.
The selection key should be s (short for scope)

Please note that all keyboard based controls need to be described in each
scope's left hand text panel. These are different for each scope. Note that
the s for selecting a scope should be in each scope's description and what
would the next scope be.

Also note that when the radar exhibit starts, the very first option will
be on the screen. Then the screen will advance through the scopes by two means;
the pressing of the s key by the user, or automatically at every 120 seconds. You will
need to emphasize in the first description that you can advance without waiting
for the automatic advancing by pressing the s key. You can reverse by hitting the S key
(upper case s) This should be articulated for the
description window for each scope. When the main exhibit descriptor screen comes up, it's 
important to highlight the feature that the user can press the s key or the S key any time to 'hurry up'
the scope advancement.

Also ensure that the timeout clock will reset when the user changes to a new scope, or presses
any key or operates any control on the panel. This
should be articulated in the descriptive text


   1. Exhibit introduction - a text block describing the exhibit and the
      basics on how to use it and what you are seeing. This should be text only. Top would
      be in all caps, "WELCOME TO MUSEUM VINTAGE RADAR EXHIBIT"

   2. Marine A-Scope - (horizontal axis is range; vertical axis is amplitude of
      return pulse; bearing will be set via a bearing control; current implementation
      would be two keys on the keyboard; one key to go clockwise on bearing and another
      key would be to go counterclockwise. The A scope phosphor is P1, which is green.
      The c key for clockwise on a scope and v for counterclockwise.
      The step rate for this control, before the knob is implemented would be one or two
      degrees per key press, but if the key is held down, it would increase slowly due to
      how long the key is depressed

      The A scope graticule is manually swapped out at each maximum range value 
      by the operator during the period. Here we will have to fake it out. And that
      graticule needs to have an incandescent color. That graticule will have three horizontal
      graticule lines for estimating return pulse strength. The range lines (vertical lines)
      must match the interim and final ranges as selected by the max range selection
      To change maximum range, use key u for up and d for down. Possible settings are 2,4,6
      miles; this must be noted clearly on the description text.

      Max is 2; one interim range at 1
      Max is 4; one interim range at 2
      Max is 6; one interim range at 4

      In addition to the blips for targets, there would be a floor of noise (signal received by
      rain and waves. This needs to be shown.

      Graticule swap simulation: In the period, changing maximum range required the operator
      to physically slide the glass graticule panel upward and out from in front of the CRT,
      then slide the replacement graticule (calibrated for the new range) downward into position.
      This must be simulated when the operator presses u or d to change range.

      The graticule swap animation uses four states:
        NORMAL      - graticule in place, scope operating normally
        SLIDING_OUT - old graticule translates upward off screen (~0.5 seconds)
        BARE_CRT    - no graticule visible; CRT trace and noise floor still running
        SLIDING_IN  - new graticule (correct lines for new range) slides down into position (~0.5 seconds)
      After SLIDING_IN completes, state returns to NORMAL with the new range active.
      The u and d keys are ignored during the swap animation (operator's hands are busy).
      The graticule remains incandescent color throughout — it is edge-lit glass.

  3. Chain Home A Scope There is a second use of the a-scope. 
      That is for the early world war 2 chain home
      radar. This operated very differently. You have a large array of high power transmitters
      'floodlighting' the target area (in World War 2, that would be the English Channel. 
      Since we don't care about land reflections with the original chain home setup was
      facing the English Channel, we can tell visitors that this radar is set at the
      English Channel (do this explanation on the explanation side panel for this
      radar mode. And for simulating operator using this radar, there would be two controls,
      one for the 'nulling the signal at the correct direction; simulating the behavior 
      of the goniometer and the other for using the goniometer for elevation. For museum
      accuracy, we need to simulate the sharp 'null' when the goniometer is at the direction
      of the signal. This concept needs to be covered in the description text thoroughly
      as this is a bit advanced. I need your advice on how to do this for children and those
      who never heard of chain home. 

      The goniometer vertical and horizontal switch could be key [ for toggling. The goniometer tuning
      would be 9 and 0 to avoid using the shift key. The tuning keys should have one unit
      for single  press, but a slow build of of speed if key is held down. This has to stay
      slow due to the sudden appearance of the null.

      Targets for Chain Home would all have to be simulated as there will be no ais
      nor ads-b. Simulations would show several aircraft approaching the radar in many
      different directions and ranges. The museum visitor for exercise could try to sort
      out the targets by range and bearing and elevation by the nulling procedure noted
      above as well as the distance of the pulse from the origin.

      The graticule is etched glass (side lit with incandescent lights) with 10 mile
      markers for range (horizontal axis). There are no vertical markers; the signal 
      strength value is not important. The only vertical value that is important is the
      nulling of the signal based on bearing and elevation from the manipulation of the
      goniometer.

      The refresh rates for chain home were slow in order to avoid aliasing with targets
      far away, the pulse repetition frequency (PRF) is about 25 times per second. This
      rate is 1/2 of the standard 50 Hz for British power.

      The operator did have a switch to switch from the 25 pulses per second PRF to 12.5
      pulses per second PRF so that they could help eliminate the range ambiguity
      problem, where a target far away could appear to be right on site since that echo
      would return at the precise time for the next pulse to go out at 25 PRF. This needs
      to be explained in the explainer window for the chain home. Mention that mountains or planes
      in the continent could have that kind of range. Furthermore, the operator can reduce
      the PRF in order to reduce confusion caused by other radio transmissions such as press-to-talk
      communications transmissions. 

      Let's assign key . for toggling between 25 and 12.5 PRF. There is no range selection. 
      Note on description; this is to reduce  use of the shift key.

      Because of the slow repetition rate, the phosphor used was P2 (long-persistence green)
      so that the targets will still glow between the sweeps and not cause flickering. P2 is
      a single-layer green phosphor with longer decay than P1, appropriate for an A-scope at
      low PRF. Unlike P7, it does not produce a blue flash or a seconds-long smearing tail.

      Another unique feature would be a response to the drifting problem in early electronics. 
      The scope electronics would use a crystal calibrator that puts tiny pips or spikes at
      known intervals (10 miles). The operator would use a knob, or control, to stretch or
      shrink the electronic trace so that the 10 mile pips align perfectly with the 10 mile
      marks on the edge lit glass graticule.

      Let's assign key n for shrink and m for stretch. (may be ambiguous, but I am running
      out of keys. Note in the descriptor.
     

   4. Marine PPI Scope - 
      marine scopes have the following items in common:
           Targets, range rings, and range ring text levels shall be treated the same for
           presentation. All are P7 phosphor. Immediate strike by the electron beam is blue.
           persistence is green/yellow. Targets, range rings, and range ring labels shall all
           persist and fade out together. They should be faded out by the time the sweep
           returns to that location.

           IMPORTANT — range rings are beam-painted, not a static overlay:
           Range rings and their labels are written by the rotating sweep beam on
           each pass, exactly like target echoes. They are NOT rendered as a fixed
           overlay on top of the scope. This means they are fully subject to P7
           persistence and decay just as targets are.

           When the operator changes max range (u/d keys), the new ring geometry
           takes effect only for the sector the sweep is currently painting. The
           rest of the scope still holds the old ring positions in the phosphor,
           glowing and fading normally. As the sweep continues rotating, each
           sector it reaches is repainted with the new ring geometry, replacing the
           old. One full sweep rotation after the change, the transition is complete
           and only the new rings remain. This is physically correct — the phosphor
           holds whatever the beam last wrote. No special transition animation is
           needed; the behavior emerges naturally from the phosphor model.

           The maximum range settings are 6 miles for the marine radar scope 
           Rings should be 2,4, and 6 miles for marine.
           The max range settings for marine ppi will be u for up and d for
           down. If  you are in the marine ppi, you change only the max range for the marine
           ppi. The possible max range values for
           the marine radar are 2,4,6 
           miles.

           Marine:
           Max is 2; one interim range at 1, final ring at 2
           Max is 4; one interim range at 2, final ring at 4
           Max is 6; one interim range at 4, final ring at 6

           Note on range. If cursor range is beyone max, clamp it to the max.

           Bear in mind that the max range setting is independent for both radars. 

           The bearing graticule (lit incandescent) There should
           be an inner circle with tickmarks for each degree, starting at 0 (north) and going
           clockwise to the last tick, which is 359. Outside the inner ring shall be text
           labels for every 15 degrees. Outside the text labels, there will be
           an outer ring. Both inner and outer rings, along with ticks, and the bearing
           labels are to be incandescent color.

           The sweep time shall be 4 seconds for the marine scope 

           The sweep direction is clockwise, which means that the antenna
           dish rotates clockwise.

           The scope has a cursor for range and bearing. The cursor consists of a
           section of a ring ( 10 degrees) and a cross line for bearing. 
           The cursor should be yellow (it
           a plastic overlay in the period time. Two controls control the  cursor; range and
           bearing. Both were physical crank controls. For now, both we need to use key pairs
           on the keyboard. A white text indication of range and bearing should be put under 
           the scope. In the real day, it was a machanical readout. The key sequence would be
           r for bearing to the right and l for bearing for the left; and t for higher range
           and y for smaller range. These controls should have slow movement for single stroke; but
           gradual for for holding key down.

   5. Police Patrol Boat PPI

      This scope shows the radar display aboard a simulated Bellingham Police
      Department patrol vessel making its routine waterfront patrol. Unlike all
      other scopes in this exhibit the radar origin is not fixed — it moves with
      the boat, making this a genuinely different operational experience.

      THE PATROL VESSEL AND ITS RADAR
      The vessel carries a professional-grade open-array radar: 6 kW peak power,
      24-inch open array, ~1.9° horizontal beamwidth, mounted 3–4 m above the
      waterline on a flybridge or radar arch. This is a working law-enforcement
      vessel, not a recreational boat; the equipment reflects that. The narrower
      beamwidth (vs a recreational radome's 4–6°) gives better azimuth resolution
      and less sea clutter per range cell — important when searching for small
      targets such as kayakers and stand-up paddleboards near the ferry lane.

      MAX RANGE: 2 miles. Range steps: 0.5 / 1 / 2 miles.
      The patrol mission is close-in situational awareness. The tight range steps
      let the officer zoom into the marina entrance, Whatcom Waterway, or the
      ferry lane without switching to a different scope.

      RADAR HORIZON: ~4.3 nm at 3.5 m antenna height. Range is mission-limited
      (2 miles) rather than horizon-limited.

      PATROL ROUTE (v1 — open water only)
      The simulated vessel follows a continuous back-and-forth patrol of the
      working waterfront at variable speed. Entry into Squalicum Marina and
      Whatcom Waterway is deferred to a future version; v1 stays in open water
      where the radar geometry is straightforward.

      Speed by zone:
        Open waterfront / ferry lane:   10 knots
        Near docks and breakwater:      4 knots
      Route (loaded from data/patrol_route.json at startup):
        Whatcom Waterway entrance → ferry terminal → Boulevard Park →
        Taylor Dock → Community Boating Center → reverse and repeat

      SMALL TARGET SCENARIO — FERRY LANE
      The Bellingham terminal serves Alaska Marine Highway ferries up to 400 ft.
      The simulator places a scripted stand-up paddleboarder on a slow drift
      across the ferry departure lane, plus random kayakers near the harbor mouth.
      These are marginal radar targets: low RCS, no metal, minimal freeboard.
      At 10 kt approach in light chop the paddleboarder may show as a faint
      intermittent blip or vanish into sea clutter entirely. Visitors can try the
      wave clutter filter (keys 5/6) and observe how suppressing clutter can also
      suppress the very target they are looking for. This is the central teaching
      moment of this scope: radar does not see everything.

      DISPLAY ORIENTATION
      Default is North-up (000° at top). The k/j keys rotate the display offset.
      Matching the offset to the boat's heading puts the bow at the top — Head-up
      mode. The left panel labels the current mode and shows the heading marker,
      a white dashed line from scope center toward the bow, drawn after all
      phosphor content so it never decays.

      TERRAIN AND BREAKWATER CLUTTER
      The concrete Squalicum Harbor outer breakwater is a strong radar return and
      a significant shadow-caster. Everything behind the breakwater from the
      patrol boat's point of view is shadowed — no return. The marina interior is
      not visible from open water. This is realistic and visible on the scope.
      Coastline, piers, and the ferry terminal structure also appear as clutter.
      Shadow masks are pre-computed for patrol route waypoints by
      terrain_preprocess and selected at runtime by nearest-waypoint lookup.

      Left panel status (below description text):
        Zone:          [plain text, e.g. "Ferry lane — open waterfront"]
        Boat pos:      XX.XXXX°N  XXX.XXXX°W
        Boat heading:  XXX°T
        Boat speed:    X.X kt
        Display mode:  North-up  (or Head-up)
        Cursor range:  X.X nm
        Cursor brg:    XXX°T
        Max range:     X.X mi

   6. Air Traffic PPI Scope - 
           Targets, range rings, and range ring text levels 
           All are P7 phosphor. Immediate strike by the electron beam is blue.
           persistence is green/yellow. Targets, range rings, and range ring labels shall all
           persist and fade out together. They should be faded out by the time the sweep
           to that location. 

           Rings should be 5,10,15,20 for the air traffic control radar. 
           The max range settings for air ppi will be u for up and d for
           down. Use of these controls affect only the scope  you are in. No other scopes are
           affected. 
           The ranges for air traffic control radar are 5,10,15,20
           miles.

           Air Traffic Control:
           Max is 5; one interim range; two total; rings at 2.5; final ring at 5
           Max is 10; four interim ranges, five total; 2,4,6,8; final ring at 10
           Max is 15; three interim ranges four total; 4,8,12; final ring at 15
           Max is 20, three interim ranges four total; 5,10,15; final ring at 20

           Note on range. If cursor range is beyone max, clamp it to the max.

           Bear in mind that the max range setting is independent for both radars. 

           The bearing graticule (lit incandescent) for the scopes are the same. There should
           be an inner circle with tickmarks for each degree, starting at 0 (north) and going
           clockwise to the last tick, which is 359. Outside the inner ring shall be text
           labels for every 15 degrees. Outside the text labels, there will be
           an outer ring. Both inner and outer rings, along with ticks, and the bearing
           labels are to be incandescent color.

           The sweep time shall be 5 seconds for the
           air traffic scope.

           The sweep direction on the scope is clockwise, which means that the antenna
           dish rotates clockwise.

           The scope has cursor for range and bearing. The cursor consists of a
           section of a ring ( 10 degrees) and a cross line for bearing. 
           The cursor should be yellow (it
           a plastic overlay in the period time. Two controls control the  cursor; range and
           bearing. Both were physical crank controls. For now, both we need to use key pairs
           on the keyboard. A white text indication of range and bearing should be put under 
           the scope. In the real day, it was a machanical readout. The key sequence would be
           r for bearing to the right and l for bearing for the left; and t for higher range
           and y for smaller range.
           These controls should have slow movement for single stroke; but
           gradual for for holding key down.

   7. Precision approach (PAR for short)
      PAR was developed in WWII and matured in the 1950s. With a fixed 10 mile range, it was
      controller who talked the pilot down verbally over radio, which means that the pilot
      does not have to rely on any equipment on the plane itself to help with landing.
      The display shows the full 10-mile approach path, but the controller's active guidance window
      is roughly the last 5 miles, intensifying from about 2 miles out to touchdown.
      This needs to be carefully explained on the explainer screen.
      Let's locate this at the south end of Runway 16/34 landing at BLI and let's have the
      active runway 34 (northbound landing)

      Locate at the end of Runway 16/34
      at Bellingham Airport (BLI). Two vertically stacked scopes share the
      right panel. Top scope: azimuth (lateral deviation vs. range from
      touchdown). Bottom scope: elevation (vertical deviation vs. range).
      Have the azimuth scope to about 1/3 larger than the elevation scope
      Both use P7 phosphor; graticules are incandescent etched glass.
      Range: 10 miles maximum, fixed (no range change control).
      Non-linear scale: inner 5 miles occupies 70% of horizontal width.
      All targets are simulated. No cursor or bearing controls; PAR
      has no bearing selection — it always points toward the runway.
      Sweep rate: approximately 30 Hz alternating between azimuth and
      elevation planes so that each will scan 1/15 th of a second.

THREADS

These are the threads of processes:

1. Display initiation and operation (anything that 'touches' the display and the shaders) Thread 1
2. Software that receives data for targets. Thread 2 (this is the traffic cop that polls the
   raspberry pis. and the Simulator. This needs mutex access to shared data with thread 1. It will
   also need mutex access to shared data with thread 4 (the simulator) 
3. Knob panel - thread 3 - uses a mutex to write shared state variables that thread 1 reads 
   to send to the shaders.
4. Simulator, thread 4. It is polled by the traffic cop

Threads 2,3, need mutex access to shared data that is read by thread 1.
Thread 2 needs mutex access for shared data with thread 4, the simulator

SUMMARY OF Controls:
● ┌─────┬─────────────────────────────────────┬───────┬──────────┬──────────────┬────────────┬──────────┬─────────┬─────┐
  │ Key │              Function               │ Intro │ Marine A │ Chain Home A │ Marine PPI │ Boat PPI │ ATC PPI │ PAR │
  ├─────┼─────────────────────────────────────┼───────┼──────────┼──────────────┼────────────┼──────────┼─────────┼─────┤
  │ s   │ Advance to next scope               │   ✓   │    ✓     │      ✓       │     ✓      │    ✓     │    ✓    │  ✓  │
  ├─────┼─────────────────────────────────────┼───────┼──────────┼──────────────┼────────────┼──────────┼─────────┼─────┤
  │ S   │ back to previous scope              │   ✓   │    ✓     │      ✓       │     ✓      │    ✓     │    ✓    │  ✓  │
  ├─────┼─────────────────────────────────────┼───────┼──────────┼──────────────┼────────────┼──────────┼─────────┼─────┤
  │ c   │ Bearing clockwise                   │       │    ✓     │              │            │          │         │     │
  ├─────┼─────────────────────────────────────┼───────┼──────────┼──────────────┼────────────┼──────────┼─────────┼─────┤
  │ v   │ Bearing counterclockwise            │       │    ✓     │              │            │          │         │     │
  ├─────┼─────────────────────────────────────┼───────┼──────────┼──────────────┼────────────┼──────────┼─────────┼─────┤
  │ u   │ Max range up                        │       │    ✓     │              │     ✓      │    ✓     │    ✓    │     │
  ├─────┼─────────────────────────────────────┼───────┼──────────┼──────────────┼────────────┼──────────┼─────────┼─────┤
  │ d   │ Max range down                      │       │    ✓     │              │     ✓      │    ✓     │    ✓    │     │
  ├─────┼─────────────────────────────────────┼───────┼──────────┼──────────────┼────────────┼──────────┼─────────┼─────┤
  │ r   │ Cursor bearing right                │       │          │              │     ✓      │    ✓     │    ✓    │     │
  ├─────┼─────────────────────────────────────┼───────┼──────────┼──────────────┼────────────┼──────────┼─────────┼─────┤
  │ l   │ Cursor bearing left                 │       │          │              │     ✓      │    ✓     │    ✓    │     │
  ├─────┼─────────────────────────────────────┼───────┼──────────┼──────────────┼────────────┼──────────┼─────────┼─────┤
  │ t   │ Cursor range increase               │       │          │              │     ✓      │    ✓     │    ✓    │     │
  ├─────┼─────────────────────────────────────┼───────┼──────────┼──────────────┼────────────┼──────────┼─────────┼─────┤
  │ y   │ Cursor range decrease               │       │          │              │     ✓      │    ✓     │    ✓    │     │
  ├─────┼─────────────────────────────────────┼───────┼──────────┼──────────────┼────────────┼──────────┼─────────┼─────┤
  │ k   │ Display offset right (boat heading) │       │          │              │     ✓      │    ✓     │         │     │
  ├─────┼─────────────────────────────────────┼───────┼──────────┼──────────────┼────────────┼──────────┼─────────┼─────┤
  │ j   │ Display offset left  (boat heading) │       │          │              │     ✓      │    ✓     │         │     │
  ├─────┼─────────────────────────────────────┼───────┼──────────┼──────────────┼────────────┼──────────┼─────────┼─────┤
  │ [   │ Goniometer H/V switch               │       │          │      ✓       │            │          │         │     │
  ├─────┼─────────────────────────────────────┼───────┼──────────┼──────────────┼────────────┼──────────┼─────────┼─────┤
  │ 9   │ Goniometer tune left                │       │          │      ✓       │            │          │         │     │
  ├─────┼─────────────────────────────────────┼───────┼──────────┼──────────────┼────────────┼──────────┼─────────┼─────┤
  │ 0   │ Goniometer tune right               │       │          │      ✓       │            │          │         │     │
  ├─────┼─────────────────────────────────────┼───────┼──────────┼──────────────┼────────────┼──────────┼─────────┼─────┤
  │ .   │ Toggle PRF (25/12.5 Hz)             │       │          │      ✓       │            │          │         │     │
  ├─────┼─────────────────────────────────────┼───────┼──────────┼──────────────┼────────────┼──────────┼─────────┼─────┤
  │ n   │ Calibrator shrink                   │       │          │      ✓       │            │          │         │     │
  ├─────┼─────────────────────────────────────┼───────┼──────────┼──────────────┼────────────┼──────────┼─────────┼─────┤
  │ m   │ Calibrator stretch                  │       │          │      ✓       │            │          │         │     │
  ├─────┼─────────────────────────────────────┼───────┼──────────┼──────────────┼────────────┼──────────┼─────────┼─────┤
  │ 1   │ Gain increase                       │       │    ✓     │      ✓       │     ✓      │    ✓     │    ✓    │  ✓  │
  ├─────┼─────────────────────────────────────┼───────┼──────────┼──────────────┼────────────┼──────────┼─────────┼─────┤
  │ 2   │ Gain decrease                       │       │    ✓     │      ✓       │     ✓      │    ✓     │    ✓    │  ✓  │
  ├─────┼─────────────────────────────────────┼───────┼──────────┼──────────────┼────────────┼──────────┼─────────┼─────┤
  │ 3   │ Rain clutter filter increase        │       │    ✓     │      ✓       │     ✓      │    ✓     │    ✓    │  ✓  │
  ├─────┼─────────────────────────────────────┼───────┼──────────┼──────────────┼────────────┼──────────┼─────────┼─────┤
  │ 4   │ Rain clutter filter decrease        │       │    ✓     │      ✓       │     ✓      │    ✓     │    ✓    │  ✓  │
  ├─────┼─────────────────────────────────────┼───────┼──────────┼──────────────┼────────────┼──────────┼─────────┼─────┤
  │ 5   │ Wave clutter filter increase        │       │    ✓     │      ✓       │     ✓      │    ✓     │         │     │
  ├─────┼─────────────────────────────────────┼───────┼──────────┼──────────────┼────────────┼──────────┼─────────┼─────┤
  │ 6   │ Wave clutter filter decrease        │       │    ✓     │      ✓       │     ✓      │    ✓     │         │     │
  └─────┴─────────────────────────────────────┴───────┴──────────┴──────────────┴────────────┴──────────┴─────────┴─────┘

Note: k/j (display bearing offset) are for marine PPI scopes only. The fixed ATC tower
at BLI has no heading offset need. On the fixed Marine PPI, k/j demonstrate North-up vs.
Head-up orientation as a teaching aid. On the Boat PPI, k/j are operationally meaningful:
zero offset = North-up (chart-style); offset matching boat heading = Head-up (bow at top).

Table for general controls not yet implemented on the keyboard in the table above:

1. Intensity
2. Focus
3. Astigmatism
4. Graticule light intensity

Note: Gain (keys 1/2), rain clutter (keys 3/4), and wave clutter (keys 5/6) are now
in the keyboard control table above. They remain physical encoder controls on the
operator panel when that hardware is installed; the keyboard keys are the temporary
stand-in. All three have defaults in settings.h.

SUMMARY of target handling:

The traffic cop handles anything that is coming from the simulator as well as the raspberry pi's
It will use polling to find if anything is available from the raspberry pis and the simulator. It will
poll each source once per beam update

The raspberry pi receiver pulls the data from each raspberry pi. If nothing, it does nothing else 
for this sweep. If there is data, it will provide data to the traffic cop upon poll.

Each raspberry pi, after boot-up, will respond to polls from the raspberry pi receiver (thread 2)

The Simulator will run all fake targets. It will provide data to the traffic cop upon traffic cop poll.
It can run as a separate thread. It will not write data to anywhere except when polled by the traffic cop.


CONTROLS

Every control listed above — both keyboard controls and the 7 general operator controls —
shall have a corresponding default constant in settings.h. This allows any startup value
to be changed at compile time without touching scope or rendering code.

The 7 general controls (Intensity, Focus, Astigmatism, Gain, Rain Clutter, Wave Clutter,
Graticule Intensity) are physical encoder controls not yet purchased. Their placeholder
default constants are defined in settings.h. KnobPanel (Thread 3) will compile and run
from the start but will idle without ever acquiring Mutex A until real hardware is wired
in. SharedRenderState holds the default values unchanged; Thread 1 reads and applies them
every frame. No feature flags or conditional compilation are needed — the code path is
complete end-to-end, always at the compile-time default.

Three of the 7 general controls — Gain, Rain Clutter, and Wave Clutter — have temporary
keyboard implementations (keys 1/2, 3/4, and 5/6 respectively) that write to the same
SharedRenderState fields the hardware encoders will eventually write to. When physical
encoders are installed, the keyboard keys can be removed or left as redundant overrides.

Things to note about the keyboard type controls. 
The letter on the keyboard are temporary. When I get around to making
the operators panel, this all will go away.

Implementation of controls:

1. For keyboard controls. Those are run as thread one where The keyboard callback 
   belongs to GLFW (glfwSetKeyCallback)
   They will manipulate the shaders only.

2. The control desk controls will have to mutex to access the state variables that 
   thread 1 sends to the shaders.

3. If the control does not yet exist, we still want stubs for receiving control
   data for that control. It's just that nothing will call it.

Scope and left window arrangement.

For each scope, put the scope itself on the right hand of the window.
On the left hand of the window will be a text description of that scope.

Underneath each scope's description will be cursor range and bearing from the radar
location; and the setting of maximum range; and the bearing offset; for 0 would
be to have 0 degrees pointing to true north (this is needed if I decide to
implement a radar on a boat. If implemented, use k for bearing to right; and
j for bearing to left. Make note in description that this is only used if this
is a radar on a boat. (perhaps later on, I could add a PPI on a boat scenario)

Please note that some keys may be the same from scope to scope. This is okay. Each
scope's controls are for that scope that you are connected do.They will not effect
settings on another scope.

Please note that the maximum range setting on a scope specific to that scope
and will be in that scope's definition. and the bearing selection
is scope specific. The manually operated radar dish for the a scope is not the same
as the PPI radar dishes. They are from different eras. In addition, all range and
bearing data for marine is separate than for air traffic control. They are completely
different radars. Range and bearing for the precision approach radar will be different
than any other radar as that radar is located at the end of the runway and scan both
horizontal and vertical.

Please analyze and comment. Please do not generate any code file nor shader files.

==================================================================

CLASS DESIGN AND FILE LAYOUT

==================================================================

Class Hierarchy:

  Scope  (abstract base)
  ├── ExhibitIntro
  ├── AScope  (abstract)
  │   ├── MarineAScope
  │   └── ChainHomeAScope
  ├── PPIScope  (abstract)
  │   ├── MarinePPIScope
  │   ├── BoatPPIScope
  │   └── ATCPPIScope
  └── PARScope

Scope (abstract base) — everything all scopes share:
  - Left panel text rendering
  - s / S key handling (scope advance / reverse)
  - Auto-advance timer reset on any key or control input
  - Pure virtual: render(), handleKey(), getDescription()

ExhibitIntro : public Scope
  - Text-only rendering, no radar display
  - Header: "WELCOME TO MUSEUM VINTAGE RADAR EXHIBIT" (all caps)

AScope : public Scope (abstract) — shared A-scope behavior:
  - Horizontal range axis, vertical amplitude axis
  - Noise floor rendering (rain/wave clutter)
  - Incandescent graticule (three horizontal amplitude lines + vertical range lines)
  - Bearing control with key-hold acceleration
  - Phosphor type as parameter (P1, P2, or P7)

MarineAScope : public AScope
  - P1 phosphor (green)
  - Range settings: 2, 4, 6 miles
  - Graticule swap animation state machine (NORMAL/SLIDING_OUT/BARE_CRT/SLIDING_IN)
    when operator changes max range — see Marine A-Scope section above for full detail
  - Keys: c (bearing CW), v (bearing CCW), u (range up), d (range down)
    u and d ignored during graticule swap animation

ChainHomeAScope : public AScope
  - P2 phosphor (long-persistence green)
  - Goniometer state: H/V mode toggle, azimuth angle, elevation angle
  - PRF toggle: 25 Hz / 12.5 Hz
  - Calibrator stretch/shrink scale factor
  - Fixed 100-mile range
  - Keys: [ (goniometer H/V toggle), 9/0 (tune), . (PRF), n/m (calibrator)

PPIScope : public Scope (abstract) — shared PPI behavior:
  - Clockwise sweep with P7 phosphor persistence (blue strike, green/yellow decay)
  - Incandescent bearing graticule (1-degree ticks, 15-degree labels, inner/outer rings)
  - Yellow cursor: 10-degree arc + bearing crossline
  - Cursor range/bearing readout under scope (white text)
  - Bearing offset for boat mode (k/j)
  - Cursor range clamped to max range
  - Range rings and labels are beam-painted per sweep sector, NOT a static overlay;
    the sweep shader evaluates ring radii for the CURRENT max range at the moment
    each beam angle is rendered; the phosphor buffer retains whatever was last
    written, so old ring positions fade naturally as the sweep overwrites them with
    new geometry — no special transition code required

MarinePPIScope : public PPIScope
  - Sweep time: 4 seconds
  - Max range: 2, 4, 6 miles with correct ring sets
  - Keys: u (range up), d (range down) — this scope only

BoatPPIScope : public PPIScope
  - Direct subclass of PPIScope (not of MarinePPIScope)
  - Range steps: 0.5 / 1 / 2 miles; sweep time 4 s; phosphor P7
  - Radar parameters: 6 kW, 1.9° open array (distinct from 30 kW / 0.5° coastal)
  - Radar origin = boat lat/lon from SharedRenderState, updated every sweep
  - Variable patrol speed per route segment (loaded from data/patrol_route.json)
  - Heading marker: white dashed line from scope center toward boat heading;
    drawn after all phosphor content so it always appears fully bright
  - Nearest pre-computed shadow mask selected each sweep via
    TerrainMap::selectNearestBoatMask() — no runtime ray-marching
  - Display mode indicator: "North-up" / "Head-up" based on offset vs heading
  - Left panel status: zone text, lat/lon, boat heading, speed, display mode
  - Keys: u/d (range 0.5/1/2), r/l (cursor bearing), t/y (cursor range),
    k/j (display offset), 1/2 (gain), 3/4 (rain filter), 5/6 (wave filter)

ATCPPIScope : public PPIScope
  - Sweep time: 5 seconds
  - Max range: 5, 10, 15, 20 miles with correct ring sets
  - Keys: u (range up), d (range down) — this scope only

PARScope : public Scope
  - Two stacked sub-scopes: azimuth (top, ~1/3 larger) and elevation (bottom)
  - 30 Hz alternating scan between planes (~15 Hz each)
  - Fixed 10-mile range, non-linear scale (inner 5 miles = 70% width)
  - P7 phosphor; incandescent etched glass graticules
  - All targets simulated; no cursor or bearing controls

Supporting classes:
  ScopeManager    Thread 1   — owns scope list, s/S switching, 120s auto-advance timer
  PhosphorRenderer Thread 1  — P1 and P7 decay/persistence; shared dependency
  Graticule        Thread 1  — incandescent graticule lines/text; parameterized per scope
  LeftPanel        Thread 1  — scope description text panel (left side of window)
  SharedRenderState Threads 1,2,3 — Mutex A; state vars Thread 1 reads each frame for shader uniforms
  TargetBuffer     Threads 2,4   — Mutex B; target data handoff between TrafficCop and Simulator
  TrafficCop       Thread 2  — polls Simulator and RPi receivers; writes to SharedRenderState
  Simulator        Thread 4  — runs fake targets; returns data to TrafficCop when polled
  KnobPanel        Thread 3  — future hardware stub; writes to SharedRenderState under Mutex A
  RPiReceiver      Thread 2  — stub; one per Raspberry Pi; called by TrafficCop

File layout:

  src/
    main.cpp
    scope_manager.h / scope_manager.cpp
    scope.h / scope.cpp                        — abstract Scope base
    scope_intro.h / scope_intro.cpp
    scope_ascope.h / scope_ascope.cpp          — abstract AScope
    scope_marine_a.h / scope_marine_a.cpp
    scope_chain_home.h / scope_chain_home.cpp
    scope_ppi.h / scope_ppi.cpp                — abstract PPIScope
    scope_marine_ppi.h / scope_marine_ppi.cpp
    scope_boat_ppi.h / scope_boat_ppi.cpp   — BoatPPIScope; police patrol boat;
                                              moving radar origin; variable speed;
                                              heading marker; display-mode tracking;
                                              nearest-mask selection from TerrainMap
    scope_atc_ppi.h / scope_atc_ppi.cpp
    scope_par.h / scope_par.cpp
    phosphor.h / phosphor.cpp
    graticule.h / graticule.cpp
    left_panel.h / left_panel.cpp
    shared_render_state.h / shared_render_state.cpp
    target_buffer.h / target_buffer.cpp
    traffic_cop.h / traffic_cop.cpp
    simulator.h / simulator.cpp
    knob_panel.h / knob_panel.cpp
    rpi_receiver.h / rpi_receiver.cpp

    settings.h                         — all tunable constants; no .cpp needed

  data/
    patrol_route.json                  — boat waypoints with lat/lon and speed per
                                         segment; loaded by Simulator at startup;
                                         not compiled in — edit without rebuild

  shaders/
    phosphor.vert / phosphor.frag   — parameterized for P1 and P7 via uniforms
    graticule.vert / graticule.frag
    text.vert / text.frag
    sweep.vert / sweep.frag

settings.h — tunable constants:
  All magic numbers live here. Every source file that needs a tunable value
  includes settings.h. No values are hardcoded elsewhere.
  Categories planned:
    - Phosphor P1 color
    - Phosphor P2 color and decay time (Chain Home A-Scope)
    - Phosphor P7 strike color, persistence color, decay times (PPI scopes and PAR)
    - Sweep line width, brightness, fade trail, periods per scope
    - PAR scan rate; Chain Home PRF high and low
    - Graticule incandescent color, line widths
    - PPI bearing ring tick lengths, label interval, font size
    - PPI range ring line width, label size, label color
    - Cursor color, line width, arc span
    - Noise floor amplitude and variation (Marine A-Scope)
    - Graticule swap animation durations (slide out, bare CRT, slide in)
    - Key-hold acceleration (initial step, rate, max) — separate for goniometer
    - Auto-advance timer interval (120 seconds)
    - Window size and panel layout fractions
    - PAR azimuth/elevation height fractions
    - UI text color and size; cursor readout text size
    - Graticule label color (incandescent)
    - Gain: default (0.5), minimum (0.0), maximum (1.0), keyboard step size
    - Rain clutter filter: default (0.0 = off), minimum (0.0), maximum (1.0), keyboard step size
    - Wave clutter filter: default (0.0 = off), minimum (0.0), maximum (1.0), keyboard step size
    - Key-hold acceleration for gain, rain clutter, and wave clutter keys
    - Terrain bounding box (lat/lon min/max) and processed cell size
    - Terrain material σ° values (soil, rock, concrete, water calm/rough)
    - Terrain material speckle/grain amplitudes (soil, rock, concrete)
    - Terrain classification thresholds (rock elevation, rock slope)
    - Terrain polar grid dimensions (range bins, bearing bins)
    - Terrain clutter brightness scale for marine PPI
    - Terrain boat recompute threshold (degrees bearing offset change)
    - ATC terrain clutter suppressed flag (bool, default true)
    - ATC terrain shadow enabled flag (bool, default true)
    - LiDAR structure height threshold for man-made classification
    - BOAT_PATROL_ROUTE_JSON: path to patrol route file (default "data/patrol_route.json")
      Waypoints and per-segment speeds live in the JSON, not in settings.h, so
      the route can be adjusted without recompiling
    - BOAT_WAYPOINT_ARRIVAL_M: radius within which a waypoint is considered
      reached, advancing to the next (default 50.0 m)
    - BOAT_HEADING_TURN_RATE_DEG_S: maximum turn rate deg/s for heading
      interpolation — realistic, not instantaneous (default 3.0)
    - BOAT_HEADUP_TOLERANCE_DEG: offset within ±this value of boat heading
      triggers "Head-up" label in left panel (default 5.0)
    - BOAT_HEADING_MARKER_COLOR: RGB color of heading marker line (default white)
    - BOAT_HEADING_MARKER_FRACTION: marker length as fraction of scope radius
      (default 0.35)
    - BOAT_HEADING_MARKER_DASH_PX: dash length in pixels (default 8)
    - BOAT_HEADING_MARKER_GAP_PX: gap length in pixels (default 4)
    - BOAT_CLUTTER_MASK_COUNT: number of pre-computed boat shadow masks (default 6)
    - BOAT_MASK_SWITCH_THRESHOLD_M: minimum boat displacement from last selected
      mask waypoint before a new mask is selected (default 500.0 m)
    - METERS_PER_DEGREE: flat-earth scale factor for lat/lon → meters conversion
      (111320.0, valid for 2 nm max range)
    - Patrol boat radar parameters: BOAT_PEAK_POWER_W (6000), BOAT_FREQ_HZ,
      BOAT_HORIZ_BEAMWIDTH_DEG (1.9), BOAT_VERT_BEAMWIDTH_DEG (20.0),
      BOAT_ANTENNA_HEIGHT_M (3.5)
    - Boat PPI range steps: BOAT_RANGE_STEPS[] = {0.5, 1.0, 2.0} miles,
      BOAT_RANGE_STEP_COUNT = 3

==================================================================

LEFT PANEL TEXT — ONE ENTRY PER SCOPE

==================================================================

Note: Each left panel renders its description text followed immediately by its
control table. Every panel also states the next scope name so the visitor knows
what pressing s will show.  The s / S advance note appears in every panel.

------------------------------------------------------------------
PANEL 1 — EXHIBIT INTRODUCTION
------------------------------------------------------------------

Header (all caps, large):
  WELCOME TO THE MUSEUM VINTAGE RADAR EXHIBIT

Body:
  Welcome! This exhibit lets you experience how radar worked from the 1940s
  through the 1960s — technology that changed the course of World War 2 and
  shaped modern aviation and maritime safety.

  Radar works by sending out short bursts of radio energy and listening for
  the echo that bounces back from ships, aircraft, and terrain. By measuring
  the time it takes for the echo to return, the radar calculates how far away
  the object is. Rotating the antenna builds a map of everything around it.

  This exhibit features seven radar displays. Explore each one at your own pace.

  Press s at any time to jump to the next display — do not wait for the
  automatic 120-second advance. Press S (shift+s) to go back.
  Pressing any key or control resets the 120-second timer.

  Next: Marine A-Scope →

  ┌─────┬───────────────────────┐
  │ KEY │ FUNCTION              │
  ├─────┼───────────────────────┤
  │  s  │ Next display          │
  │  S  │ Previous display      │
  └─────┴───────────────────────┘

------------------------------------------------------------------
PANEL 2 — MARINE A-SCOPE
------------------------------------------------------------------

  The A-Scope was one of the earliest radar displays, used aboard ships and
  in coastal stations in the 1950s. Unlike the circular display you may have
  seen in movies, the A-Scope sweeps left to right: distance (range) runs
  along the bottom axis, and the height of each spike shows how strong the
  echo is from that direction.

  To look in a different direction, the operator physically rotates the
  antenna by hand. Use c and v to rotate the antenna on this display.

  The green glow is the P1 phosphor coating on the inside of the cathode ray
  tube. In a real radar room this was often the only light in the space.

  The glass panel in front of the screen is the graticule — an etched,
  back-lit calibration scale for measuring range. When you change the maximum
  range setting, watch the operator swap the graticule panel by hand — just
  as it was done in the period.

  Location: Bellingham Bay, WA — a fictional 100-foot mid-bay platform.

  Press s to advance.  Press S to go back.  Auto-advance in 120 seconds.
  Any key press resets the timer.   Next: Chain Home A-Scope →

  ┌──────┬──────────────────────────────┐
  │  KEY │ FUNCTION                     │
  ├──────┼──────────────────────────────┤
  │  s/S │ Next / previous scope        │
  │  c/v │ Bearing clockwise / CCW      │
  │  u/d │ Max range up / down (2,4,6)  │
  │  1/2 │ Gain increase / decrease     │
  │  3/4 │ Rain filter increase / dec   │
  │  5/6 │ Wave filter increase / dec   │
  └──────┴──────────────────────────────┘

------------------------------------------------------------------
PANEL 3 — CHAIN HOME A-SCOPE
------------------------------------------------------------------

  Chain Home was Britain's early warning radar network, built in the late
  1930s and critical during the Battle of Britain (1940). Instead of a
  rotating dish, it used a fixed array of tall transmit towers that flooded
  the sky over the English Channel with radio energy — a technique called
  floodlighting. Aircraft crossing the Channel reflected this energy back
  to separate receive antennas, sometimes at ranges up to 100 miles.

  FINDING DIRECTION — THE GONIOMETER
  Because there was no rotating dish, finding the bearing and altitude of a
  target required a technique called nulling. A device called a goniometer
  electronically combined signals from several receive antennas. The operator
  slowly turned the goniometer dial while watching the screen. In most
  positions the signal was strong. But at one precise setting the signal
  suddenly disappeared — this was the null. When nulled, the goniometer was
  pointing directly at the aircraft. Think of it like tuning a radio: you
  search for the silent spot between two stations. Press [ to switch the
  goniometer between bearing (horizontal) and elevation (vertical) modes.
  Use keys 9 and 0 to tune — turn slowly, the null appears suddenly.

  CALIBRATOR PIPS
  Early electronics drifted, stretching or compressing the range scale.
  The crystal calibrator injects tiny spikes at exact 10-mile intervals.
  Use n to shrink and m to stretch the trace until the pips line up with
  the 10-mile marks on the glass graticule.

  This display is set at the Chain Home station at Poling, East Sussex,
  facing the English Channel. All targets are simulated German aircraft.

  Press s to advance.  Press S to go back.  Auto-advance in 120 seconds.
  Any key press resets the timer.   Next: Marine PPI →

  ┌──────┬─────────────────────────────────┐
  │  KEY │ FUNCTION                        │
  ├──────┼─────────────────────────────────┤
  │  s/S │ Next / previous scope           │
  │   [  │ Goniometer mode: bearing / elev │
  │  9/0 │ Goniometer tune left / right    │
  │   .  │ Toggle PRF: 25 Hz / 12.5 Hz     │
  │  n/m │ Calibrator shrink / stretch     │
  │  1/2 │ Gain increase / decrease        │
  │  3/4 │ Rain filter increase / dec      │
  │  5/6 │ Wave filter increase / dec      │
  └──────┴─────────────────────────────────┘

------------------------------------------------------------------
PANEL 4 — MARINE PPI SCOPE
------------------------------------------------------------------

  The PPI (Plan Position Indicator) became the standard radar display for
  ships from the late 1950s onward. The antenna rotates clockwise and the
  sweep line rotates with it, painting a map of everything within range.
  Targets glow bright blue the instant the sweep passes over them, then
  fade through green to yellow before the sweep returns — this is the P7
  phosphor persistence that keeps the picture visible between sweeps.

  The dotted range rings give distance reference. The incandescent bearing
  scale shows True direction (0 = North, clockwise to 359).

  The yellow overlay is a mechanical cursor — a plastic ring and crosshair
  mounted in front of the screen. Use the cursor keys to position it over a
  target; range and bearing read out below the scope.

  Location: Bellingham Bay, WA — a fictional 100-foot mid-bay platform.
  Targets: AIS-equipped vessels and simulated traffic.

  Press s to advance.  Press S to go back.  Auto-advance in 120 seconds.
  Any key press resets the timer.   Next: Marine PPI — Boat Scenario →

  ┌──────┬────────────────────────────────────────────────┐
  │  KEY │ FUNCTION                                       │
  ├──────┼────────────────────────────────────────────────┤
  │  s/S │ Next / previous scope                          │
  │  u/d │ Max range up / down (2, 4, 6 mi)               │
  │  r/l │ Cursor bearing right / left                    │
  │  t/y │ Cursor range increase / decrease               │
  │  k/j │ Display offset right / left                    │
  │      │  (zero = North-up; rotate to match heading     │
  │      │   = Head-up — a teaching aid on fixed radar)   │
  │  1/2 │ Gain increase / decrease                       │
  │  3/4 │ Rain filter increase / decrease                │
  │  5/6 │ Wave filter increase / decrease                │
  └──────┴────────────────────────────────────────────────┘

------------------------------------------------------------------
PANEL 5 — POLICE PATROL BOAT PPI
------------------------------------------------------------------

  This display is from the radar aboard a Bellingham Police Department
  patrol vessel making its routine waterfront patrol. The scope center
  is the boat — it moves with the patrol route, not fixed in the bay.

  THE RADAR IS DIFFERENT HERE
  The patrol boat carries a professional 6-kilowatt open-array radar
  with a 1.9-degree beam — narrower than a cheap boat radar, but wider
  than the big 30-kilowatt coastal radar you saw two displays back. The
  blips look noticeably fatter than on the coastal radar. The maximum
  range here is 2 miles: the patrol mission is close-in harbor work,
  not long-range scanning.

  THE FERRY LANE PROBLEM
  The Bellingham terminal serves Alaska state ferries up to 400 feet
  long. When a ferry departs, anyone in the departure lane — a kayaker,
  a paddleboarder — must get clear. Watch the scope carefully: there
  is a stand-up paddleboarder drifting slowly near the ferry lane.
  Can you spot it?

  Stand-up paddleboards are very hard to see on radar. No metal, almost
  no freeboard, small size — the radar echo is barely above the noise.
  Try adjusting the wave clutter filter (keys 5 and 6). Turning it up
  reduces the sea clutter that is hiding the target — but it may also
  suppress the target itself. This trade-off is real. Radar does not
  see everything.

  NORTH-UP AND HEAD-UP
  By default the scope shows North-up: True North at the top, like a
  chart. Press k or j to rotate the display. When the heading marker
  (white dashed line) points straight up, you are in Head-up mode —
  the bow is at the top. This is how most early marine radars worked.

  Location: Bellingham waterfront patrol, Bellingham Bay, WA.
  Targets: AIS vessels, paddleboarder, random kayakers.

  Press s to advance.  Press S to go back.  Auto-advance in 120 seconds.
  Any key press resets the timer.   Next: ATC PPI →

  ┌──────┬────────────────────────────────────────────────┐
  │  KEY │ FUNCTION                                       │
  ├──────┼────────────────────────────────────────────────┤
  │  s/S │ Next / previous scope                          │
  │  u/d │ Max range up / down (0.5, 1, 2 mi)             │
  │  r/l │ Cursor bearing right / left                    │
  │  t/y │ Cursor range increase / decrease               │
  │  k/j │ Display offset right / left                    │
  │      │  (0° = North-up;  match heading = Head-up)     │
  │  1/2 │ Gain increase / decrease                       │
  │  3/4 │ Rain filter increase / decrease                │
  │  5/6 │ Wave filter increase / decrease                │
  └──────┴────────────────────────────────────────────────┘

------------------------------------------------------------------
PANEL 6 — AIR TRAFFIC CONTROL PPI SCOPE
------------------------------------------------------------------

  This is the Airport Surveillance Radar (ASR) display used by air traffic
  controllers at regional airports in the 1960s. It works on the same
  principle as the marine PPI but covers a larger area (up to 20 miles) and
  is optimised for tracking aircraft rather than ships. The S-Band frequency
  (3 GHz) gives a good balance of range, resolution, and resistance to
  weather clutter for airspace surveillance.

  Controllers used this display to sequence arriving and departing aircraft,
  identify potential conflicts, and provide navigation guidance to pilots
  flying in low visibility conditions.

  Location: Bellingham International Airport (BLI), dedicated radar tower.
  Targets: ADS-B equipped aircraft and simulated traffic.

  Press s to advance.  Press S to go back.  Auto-advance in 120 seconds.
  Any key press resets the timer.   Next: Precision Approach Radar →

  ┌──────┬──────────────────────────────────────────┐
  │  KEY │ FUNCTION                                 │
  ├──────┼──────────────────────────────────────────┤
  │  s/S │ Next / previous scope                    │
  │  u/d │ Max range up / down (5, 10, 15, 20 mi)   │
  │  r/l │ Cursor bearing right / left              │
  │  t/y │ Cursor range increase / decrease         │
  │  1/2 │ Gain increase / decrease                 │
  │  3/4 │ Rain filter increase / decrease          │
  └──────┴──────────────────────────────────────────┘

------------------------------------------------------------------
PANEL 7 — PRECISION APPROACH RADAR (PAR)
------------------------------------------------------------------

  The Precision Approach Radar was developed during World War 2 and refined
  through the 1950s. It gives a controller a precise picture of exactly where
  an aircraft is on its final approach to the runway — both its left-right
  position and its altitude.

  Unlike instrument landing systems that require special equipment on the
  aircraft, PAR needed nothing from the pilot's plane. The controller watched
  the display and talked the pilot down over the radio: "You are slightly
  right of centerline, begin correcting left. You are above the glide path,
  begin a slightly steeper descent." This made PAR invaluable when a plane's
  own instruments failed or when visibility dropped to near zero in heavy fog.

  TOP DISPLAY — AZIMUTH (left-right)
  Shows whether the aircraft is left or right of the runway centerline.
  The centerline runs horizontally through the middle of the display.

  BOTTOM DISPLAY — ELEVATION (up-down)
  Shows whether the aircraft is above or below the correct glide slope.
  The ideal descent path runs through the center of the display.

  Both displays expand the inner 5 miles to 70% of the screen width —
  giving maximum precision during the critical final approach phase.

  Location: South end of Runway 16/34, Bellingham Airport (BLI).
  Active runway: 34 (aircraft landing northbound). All traffic is simulated.

  Press s to advance.  Press S to go back.  Auto-advance in 120 seconds.
  Any key press resets the timer.   Next: Exhibit Introduction →

  ┌──────┬──────────────────────────────┐
  │  KEY │ FUNCTION                     │
  ├──────┼──────────────────────────────┤
  │  s/S │ Next / previous scope        │
  │  1/2 │ Gain increase / decrease     │
  │  3/4 │ Rain filter increase / dec   │
  └──────┴──────────────────────────────┘

==================================================================

UNITS

==================================================================

All dimensions are stored and computed in METERS throughout the
entire system — in shared state, in the database, in shaders, and
in all target data structures.

Any incoming value that arrives in feet (e.g. ADS-B altitude,
antenna heights from configuration) MUST be converted to meters
at the data boundary — in RPiReceiver::parseFrame() or
Simulator::poll() — before the value enters any shared data
structure. No feet values may appear anywhere inside the system
after the conversion point.

Conversion constant in settings.h:
  FEET_TO_METERS = 0.3048  (exact)

==================================================================

DEFAULT TARGET DIMENSIONS

==================================================================

When a target is first seen with no database record, the system
inserts a row using the defaults below. All values in meters.
need_update is set TRUE so the operator knows to fill in real data.

  ┌──────────────────┬───────────┬──────────────────┬────────────┐
  │ Category         │ Length (m)│ Width/Beam (m)   │ Material   │
  ├──────────────────┼───────────┼──────────────────┼────────────┤
  │ GA aircraft      │   4.0     │ 1.0 (fuselage)   │ aluminum   │
  │ Commercial a/c   │  30.0     │ 5.0 (fuselage)   │ aluminum   │
  │ AIS vessel       │  20.0     │ 5.0 (beam)       │ steel      │
  │ Simulator boat   │   6.0     │ 2.0 (beam)       │ fiberglass │
  └──────────────────┴───────────┴──────────────────┴────────────┘

Notes:
- AIS vessels are legally required commercial traffic, so steel is
  the correct default material.
- Simulator boats are small pleasure craft, so fiberglass is correct.
- The system defaults all new aircraft to GA. The operator must
  update commercial entries via the --database panel.
- Height above water/ground defaults to 0 for vessels and 0 for
  aircraft (updated by live data).

==================================================================

POSTGRESQL DATABASE

==================================================================

PostgreSQL is installed. Database: radar. User: radar. Password: radar.
User has full privileges on database radar.

Schema (all dimensions in METERS):

  1. target_type   ENUM('AIS', 'ADSB', 'LOCAL')
  2. target_id     BIGINT  — MMSI for marine; ICAO 24-bit address for
                             aircraft; simulator may reuse same space
  3. length_m      REAL    — length in meters
  4. width_m       REAL    — beam (vessels) or fuselage width (aircraft)
                             in meters
  5. height_m      REAL    — height above water/ground in meters
  6. material      ENUM('fiberglass', 'wood', 'aluminum', 'steel')
  7. need_update   BOOLEAN DEFAULT TRUE
  8. last_seen     TIMESTAMPTZ  — ISO 8601, updated each time target
                                  is received from any source
  9. last_updated  TIMESTAMPTZ  — ISO 8601, updated when operator saves
                                  changes via DB panel
  10. track history — deferred to a future version; not in v1 schema

The fields length_m, width_m, height_m, and material are used by the
CPU to compute RCS before passing to the shader. They are static
per-target data uploaded to GPU via SSBO or uniform array each frame.

Per-frame dynamic fields (location, heading, altitude) are NOT stored
in the database. They live in TargetBuffer (Threads 2/4) and are
updated from live Raspberry Pi or simulator data each sweep.

==================================================================

DATABASE MANAGEMENT PANEL

==================================================================

Activated by command-line flag: --database
In this mode NO radar display is shown. main.cpp skips all scope
and shader initialization and starts the Dear ImGui loop instead.

Toolkit: Dear ImGui, integrated with GLFW/OpenGL 3.3.
Source lives in src/imgui/ and is compiled directly into the project
(no separate install step required).

The panel provides:
  - Scrollable table of all targets with need_update highlighted
  - Inline edit fields for length, width, height, material
  - Dropdown for target_type
  - Save button that writes to PostgreSQL via libpq
  - Quit button to exit the DB panel (then start main exhibit separately)

==================================================================

RADAR EQUATION AND BLOOM

==================================================================

The radar equation is evaluated CPU-side to compute received power
(P_r) for each target. The result drives target brightness and bloom.
Computation happens in TrafficCop after each poll, outside any mutex
lock (read-only access to target physical data at that point).

The computed brightness value is uploaded per-target to the GPU via
SSBO or uniform array. The phosphor shader reads it to set amplitude.

BLOOM POST-PROCESSING (bloom.vert / bloom.frag):
  Separate shader pair. Pipeline:
    1. Render targets to offscreen FBO at full computed brightness
    2. Apply two-pass Gaussian blur to pixels above luminance threshold
    3. Additively blend blurred result back onto main framebuffer
  Constants in settings.h: BLOOM_LUMINANCE_THRESHOLD,
  BLOOM_BLUR_RADIUS_PX, BLOOM_BLEND_STRENGTH

RADAR PARAMETERS — MARINE (X Band):
  Frequency:           9225 MHz
  Peak power:          30 kW
  Horizontal beamwidth: 0.5 degrees
  Vertical beamwidth:  20 degrees
  Antenna length:      15 feet (4.572 m)
  Antenna height:      50 feet (15.24 m) above surface

RADAR PARAMETERS — ATC (S Band):
  Frequency:           3000 MHz (3 GHz)
  Peak power:          25 kW  (exhibit value; real ASR is higher)
  Horizontal beamwidth: 1.4 degrees
  Vertical beamwidth:  5 degrees
  Antenna width:       20 feet (6.096 m)
  Antenna height:      50 feet (15.24 m) above runway

RADAR PARAMETERS — CHAIN HOME (AMES Type 1):
  Frequency:           30 MHz
  Peak power:          500 kW
  Pulse width:         20 microseconds
  PRF:                 25 Hz or 12.5 Hz (operator selectable)
  Transmit gain (Gt):  ~7.5 linear (~8.7 dBi) — floodlight, not a beam
                       formula: G = 30000 / (az_bw_deg × el_bw_deg)
                       with az ~100 deg, el ~40 deg
  Receive gain (Gr):   4 to 10 linear (6–10 dBi)
  System loss:         ~8 dB (transmission line)
  Bistatic:            yes — separate transmit and receive antennas
  RCS resonance:       at 30 MHz (lambda ~10 m) aircraft are in the
                       Mie/resonant scattering region; RCS is multiplied
                       by CHAIN_HOME_RCS_RESONANCE_FACTOR (default 3.0)
                       before radar equation computation. Applied to
                       Chain Home targets only.

RADAR PARAMETERS — PAR (X Band):
  Peak power:          100 kW
  Frequency:           ~10 GHz (lambda ~3 cm)
  Antenna size:        ~5 meters
  Beamwidth:           ~0.34 degrees (lambda/D)
  Horizontal scan:     20 degrees total
  Vertical scan:       10 degrees total
  Pulse width:         short (high range resolution)
  PRF:                 high (~30 Hz alternating az/el)

RADAR PARAMETERS — PATROL BOAT (X Band):
  Frequency:           9300–9500 MHz (X-band; treat same as marine for exhibit)
  Peak power:          6 kW
  Antenna type:        Open array, 24-inch (610 mm)
  Horizontal beamwidth: 1.9 degrees
  Vertical beamwidth:  20 degrees
  Antenna height:      3.5 m above waterline (flybridge or radar arch)
  Radar horizon:       ~4.3 nm to sea-level target
  Operational max range: 2 miles (mission-limited, not horizon-limited)

  NOTE — beamwidth comparison:
    Fixed coastal marine:  0.5° — sharp blips, high azimuth resolution
    Police patrol boat:    1.9° — noticeably fatter blips; good exhibit contrast
    Consumer radome:       4–6° — poorest resolution (not used in this exhibit)

  NOTE — small target detection in sea clutter:
    The narrower 1.9° beam illuminates ~1/3 the sea surface area per range
    cell compared to a 5° radome, improving signal-to-clutter ratio by ~5 dB.
    Even so, a stand-up paddleboard (RCS ~0.1–0.5 m²) is marginal in any chop.
    Detection is realistic only in near-calm conditions at ≤1 mile.

All radar parameters shall have corresponding constexpr constants
in settings.h so they can be tuned without touching equation code.

==================================================================

TERRAIN AND LAND CLUTTER

==================================================================

DATA SOURCES
  map/charts_enc/US5WA45M.000
      S-57 Electronic Navigational Chart; coastline polygon,
      pier/breakwater geometry, named landmarks. Parsed with GDAL/OGR.

  map/charts_enc/n48_w123_1arc_v3.tif
      USGS 1 arc-second (~30 m) DEM GeoTIFF. Terrain elevation for the
      Bellingham area — Cascades, Chuckanut Mountain, coastal lowlands.

  map/lidar_raw/wa2016_west_dem_J1364939.zip
      2016 western WA LiDAR DEM. Resolves waterfront structures,
      breakwaters, piers, Boulevard Park boardwalk, and other man-made
      features at ~1 m resolution.

  map/lidar_raw/wa2022_nooksack_dem_J1364940.zip
      2022 Nooksack basin LiDAR DEM. Covers the delta and lowlands
      northeast of Bellingham Bay; used for terrain shadowing from the
      northeast quadrant.

  All three sources are processed offline by terrain_preprocess into
  binary grids in map/lidar_processed/. The raw files are never opened
  at exhibit runtime. See TERRAIN PREPROCESSING section below.

  GDAL (libgdal-dev) is a required build dependency for
  terrain_preprocess. It is NOT linked into the main radar binary.

CLASSES

  TerrainMap (Thread 1, read-only after init)
    Loaded once at startup from map/lidar_processed/. Reads the four
    binary grids and the metadata JSON. Provides:
      - Elevation query by lat/lon
      - Material query by lat/lon
      - Pre-computed polar clutter grid (range × bearing bins) per
        fixed radar location
      - Line-of-sight shadow mask per radar location
    Thread 1 only after init; no mutex required.

  LandClutter (Thread 1)
    Queries TerrainMap to generate clutter returns for each scope.
    Called once per full sweep rotation, not once per frame.
      - Marine A-Scope: produces amplitude samples along the current
        antenna bearing for injection into the range trace.
      - Marine PPI / ATC PPI: produces a polar texture (range × bearing)
        uploaded to the GPU once per sweep period.

TERRAIN MATERIALS AND RCS

  Each terrain cell is classified as one of four materials. Normalized
  backscatter coefficient σ° (linear, m²/m²) is defined in settings.h:

    TERRAIN_SIGMA0_SOIL          ~0.010  (−20 dB) — moist vegetated soil
    TERRAIN_SIGMA0_ROCK          ~0.032  (−15 dB) — exposed rock, rough face
    TERRAIN_SIGMA0_CONCRETE      ~0.100  (−10 dB) — smooth hard surface;
                                                     structures produce
                                                     corner-reflector effect
    TERRAIN_SIGMA0_WATER_CALM    ~0.0003 (−35 dB) — open water, calm;
                                                     mostly specular, low
                                                     backscatter
    TERRAIN_SIGMA0_WATER_ROUGH   ~0.010  (−20 dB) — choppy sea, sea clutter

  Classification rules applied by terrain_preprocess at build time:
    - Below sea level or inside S-57 coastline polygon → water
    - Inside S-57 pier/breakwater/wharf feature        → concrete
    - Elevation > TERRAIN_ROCK_THRESHOLD_M AND slope
      > TERRAIN_ROCK_SLOPE_THRESHOLD_DEG               → rock
    - All remaining land cells                         → soil

RADAR EQUATION FOR TERRAIN (area-extensive / clutter form)

  P_r = (P_t × G² × λ² × σ° × A_cell) / ((4π)³ × R⁴)

  A_cell is the terrain cell area projected along the beam.
  Evaluated CPU-side in LandClutter for each illuminated cell.
  Result drives per-cell brightness in the clutter texture.
  Same radar parameters (P_t, G, λ) used for point targets.

SHADOW / LINE-OF-SIGHT MASKING

  Computed by terrain_preprocess for each fixed radar location and
  stored in the processed data as uint8 shadow masks. Algorithm: march
  outward from the radar along each azimuth radial, tracking the maximum
  elevation angle seen. Any cell whose surface angle falls below that
  maximum is shadowed — clutter amplitude = 0 and target returns through
  that cell are attenuated proportionally.

  Shadow masks stored per radar location:
    map/lidar_processed/shadow_marine.u8  — marine bay platform
    map/lidar_processed/shadow_atc.u8     — BLI ATC tower

  Bearing offset (k/j keys on PPI scopes):
    The k/j keys change display heading only — which direction appears
    at the top of the scope. They do NOT move the radar geographically.
    The shadow mask is unchanged. The terrain clutter shader receives
    the offset as a rotation uniform and samples the polar texture at
    the offset angle. Zero CPU overhead; no shadow recomputation.

  Boat PPI scope scenario:
    When BoatPPIScope is active, SharedRenderState.boatModeActive is set
    TRUE. LandClutter uses boatLatDeg / boatLonDeg as the polar grid origin
    instead of the fixed marine platform position.

    Shadow mask selection: terrain_preprocess pre-computes BOAT_CLUTTER_MASK_COUNT
    shadow masks for waypoints evenly spaced around the simulated route.
    These are written as:
      map/lidar_processed/shadow_boat_NNN.u8   — one file per boat mask waypoint
                                                 NNN = zero-padded index
    At runtime, TerrainMap::selectNearestBoatMask() scans the BOAT_CLUTTER_MASK_COUNT
    waypoints and returns the index whose position is nearest to the current boat
    lat/lon (straight-line distance). The selected mask changes at most once per
    4-second sweep when the boat has moved more than BOAT_MASK_SWITCH_THRESHOLD_M
    (default 500 m) from the last selected waypoint position.

    Since Bellingham Bay is open water, the major terrain shadowing features
    (Chuckanut Mountain, Eliza Island, Lummi Island) are visible from most bay
    positions. The nearest-mask approximation introduces negligible error within
    the 500 m switching threshold.

    The terrain clutter shader receives the boat position offset as a translation
    uniform (u_radarOffsetM, a vec2) in addition to the bearing offset rotation.
    The shader adds this offset when sampling the polar texture so coastline
    features appear at their correct positions relative to the moving radar.

PER-SCOPE TERRAIN BEHAVIOR

  Marine A-Scope:
    Land returns appear as stable blips at fixed ranges on the current
    antenna bearing. Concrete structures give strong returns; soil/hills
    give moderate returns; shadowed areas return nothing. Period
    operators were trained to discard stable blips when searching for
    moving targets.

  Marine PPI (fixed platform and boat heading offset):
    Land clutter is fully visible. Coastline, hills, piers, and
    breakwaters paint exactly as on a real period marine radar. The
    clutter texture updates once per 4-second sweep rotation.

  ATC PPI:
    Moving Target Indicator (MTI) cancellation suppresses land clutter.
    Controlled by ATC_TERRAIN_CLUTTER_SUPPRESSED (default true) in
    settings.h. Terrain shadowing of aircraft IS applied — controlled
    by ATC_TERRAIN_SHADOW_ENABLED (default true). Aircraft approaching
    behind a ridge appear at reduced amplitude or disappear until they
    clear the ridge, as on period ASR equipment.

  Chain Home A-Scope:
    No terrain data applied. The exhibit scenario faces the English
    Channel; the transmitter floodlights the sea. Land returns are
    not simulated for this scope.

  PAR:
    No terrain clutter. PAR points at a single fixed approach path;
    the narrow beam and short range make land returns negligible.

TERRAIN CLUTTER SHADER

  terrain_clutter.vert / terrain_clutter.frag
    Renders the polar clutter texture as a quad overlay on the PPI
    scope. Converts screen coordinates to polar, samples the clutter
    texture, and outputs P7-compatible phosphor color and alpha so
    terrain returns decay on the same timescale as target echoes.
    Uniforms:
      u_bearingOffsetDeg   — display heading correction (default 0.0)
      u_radarOffsetM       — vec2 (dx_m, dy_m) from fixed marine platform origin
                             to current radar position; (0,0) for fixed scopes,
                             non-zero for Boat PPI as boat moves around the bay
      u_clutterSuppressed  — bool; 1 = suppress (ATC mode)
      u_maxRangeM          — current scope max range in meters
      u_clutterBrightness  — TERRAIN_MARINE_CLUTTER_BRIGHTNESS scale
    Used by MarinePPIScope, BoatPPIScope, and ATCPPIScope.

==================================================================

TERRAIN PREPROCESSING

==================================================================

All three raw terrain sources are processed in a single pass by the
offline tool terrain_preprocess, which writes ready-to-use binary grids
to map/lidar_processed/. Must be run before first launch and re-run
whenever TERRAIN_BBOX_* or TERRAIN_PROCESSED_CELL_DEG constants in
settings.h change.

Sources processed:
  map/charts_enc/n48_w123_1arc_v3.tif         — USGS 1 arc-second DEM
                                                 (~30 m); already WGS84;
                                                 covers full ATC radar range
                                                 including distant ridges
  map/lidar_raw/wa2016_west_dem_J1364939.zip   — 2016 LiDAR DEM (~1 m);
                                                 projected; requires warp
  map/lidar_raw/wa2022_nooksack_dem_J1364940.zip — 2022 LiDAR DEM (~1 m);
                                                 projected; requires warp
  map/charts_enc/US5WA45M.000                  — S-57 ENC; material
                                                 classification only; no
                                                 elevation data

TOOL
  Build target:  terrain_preprocess  (separate CMake executable)
  Source:        src/terrain_preprocess.cpp
  Links:         GDAL only; NOT linked into the main radar binary
  Run:           ./terrain_preprocess   (from build directory)

PIPELINE (runs in order)
  1. Open USGS DEM (n48_w123_1arc_v3.tif) directly via GDAL — already
     in WGS84, no warp needed. Crop to TERRAIN_BBOX_LAT/LON bounds.
  2. Unzip both LiDAR archives to a temp directory.
  3. Inventory LiDAR tiles — enumerate .tif / .img files, check CRS
     and native resolution of each survey.
  4. Merge tiles within each LiDAR survey into a GDAL VRT mosaic.
  5. Warp both LiDAR surveys to WGS84 (EPSG:4326) at
     TERRAIN_PROCESSED_CELL_DEG resolution. Crop to bounding box.
  6. Three-way elevation merge — priority order, highest to lowest:
       LiDAR 2022 (Nooksack survey)  — highest resolution, newest vintage
       LiDAR 2016 (western survey)   — fills gaps in 2022 coverage
       USGS DEM (1 arc-second)       — floor; covers full extent of all
                                       radar scopes including distant
                                       terrain beyond LiDAR coverage
                                       (Cascades, far ridges for ATC shadow)
       Any cell with no data from any source → elevation 0.0 m (water).
  7. Material classification:
       Load S-57 ENC (US5WA45M.000) via GDAL/OGR.
       Apply classification rules described in TERRAIN section above.
  8. Compute shadow masks using radial elevation-angle march along each
     azimuth bearing for each of the following radar positions:
       - Fixed marine platform (lat 48.7436, lon -122.5647)
       - ATC tower at BLI
       - BOAT_CLUTTER_MASK_COUNT boat waypoints from BOAT_SIM_WAYPOINTS[]
     All shadow masks use the same algorithm; only the origin differs.
  9. Write to map/lidar_processed/:
       elevation.f32             float32 row-major grid, meters, WGS84
       material.u8               uint8 per cell (0=water 1=soil 2=rock 3=concrete)
       shadow_marine.u8          uint8 visibility mask for marine radar
       shadow_atc.u8             uint8 visibility mask for ATC radar
       shadow_boat_000.u8 …
       shadow_boat_NNN.u8        uint8 visibility masks for boat waypoints;
                                 NNN = zero-padded waypoint index
       terrain_meta.json         grid dimensions, lat/lon origin, cell size,
                                 checksums for all three source files,
                                 processing date,
                                 boat mask waypoint lat/lon list

RUNTIME VALIDATION
  TerrainMap reads terrain_meta.json at startup and compares the stored
  bounding box and cell-size values against current settings.h constants.
  If they differ it prints a warning and continues with stale data:
    WARNING: terrain data was built with different bounding box or
    cell size — re-run terrain_preprocess before exhibit launch.
  The exhibit does not crash; it runs with the old grid.

OUTPUT FILES (map/lidar_processed/)
  elevation.f32             — float32 elevation grid
  material.u8               — uint8 material classification grid
  shadow_marine.u8          — uint8 line-of-sight mask, marine radar
  shadow_atc.u8             — uint8 line-of-sight mask, ATC radar
  shadow_boat_000.u8 …      — uint8 line-of-sight masks for boat waypoints
  shadow_boat_NNN.u8          (BOAT_CLUTTER_MASK_COUNT files total)
  terrain_meta.json         — metadata, provenance record, and boat mask
                              waypoint lat/lon list used for nearest-mask lookup

  These files are the only terrain inputs at runtime.
  The raw source files in map/lidar_raw/ and map/charts_enc/ are
  never opened by the exhibit binary.

==================================================================

BOAT SCENARIO

==================================================================

The boat scenario (scope 5 — Police Patrol Boat PPI) simulates a Bellingham
Police Department patrol vessel making its waterfront patrol. The radar is a
6 kW professional open-array unit (1.9° beamwidth), not the same hardware as
the fixed coastal marine radar. The radar origin moves with the boat every sweep.

PATROL ROUTE FILE — data/patrol_route.json

  Loaded by the Simulator at startup. Not compiled in — the route can be
  refined without a rebuild. Format (approximate):

    {
      "waypoints": [
        { "lat": 48.7530, "lon": -122.5150, "speed_kt": 10.0,
          "zone": "Ferry lane — open waterfront" },
        { "lat": 48.7480, "lon": -122.5050, "speed_kt": 4.0,
          "zone": "Near Squalicum breakwater" },
        { "lat": 48.7460, "lon": -122.5120, "speed_kt": 10.0,
          "zone": "Open waterfront west" },
        { "lat": 48.7380, "lon": -122.5200, "speed_kt": 10.0,
          "zone": "Boulevard Park approach" },
        { "lat": 48.7340, "lon": -122.5150, "speed_kt": 4.0,
          "zone": "Taylor Dock area" },
        { "lat": 48.7320, "lon": -122.5050, "speed_kt": 4.0,
          "zone": "Community Boating Center" }
      ],
      "loop": "reverse"
    }

  "loop": "reverse" means the boat reaches the last waypoint then reverses
  direction back through the list — a back-and-forth patrol, not a closed loop.
  All coordinates are open water; marina and Whatcom Waterway entry deferred.
  Adjust lat/lon values to keep the vessel in navigable water once the ENC
  coastline is loaded. Values above are starting approximations.

BOAT NAVIGATION SIMULATION (Simulator, Thread 4)

  The Simulator maintains a BoatNavigator sub-object that loads the JSON route
  at startup and advances the vessel each time TrafficCop polls. Data returned
  to TrafficCop alongside the regular target list:
    boat_lat_deg      — current latitude (degrees WGS84)
    boat_lon_deg      — current longitude (degrees WGS84)
    boat_heading_deg  — current true heading (degrees, 0 = north)
    boat_speed_kts    — current speed from active waypoint segment
    boat_zone_str     — zone label string for left panel display

  TrafficCop writes these to SharedRenderState under Mutex A.
  Thread 1 reads them every frame when BoatPPIScope is active.

  Navigation algorithm (runs in Simulator::poll(), Thread 4):
    1. Compute great-circle bearing from current position to next waypoint.
    2. Rotate boatHeadingDeg toward that bearing at up to
       BOAT_HEADING_TURN_RATE_DEG_S per elapsed second (clamped).
    3. Advance position along current heading at the current segment speed_kt.
    4. If distance to next waypoint < BOAT_WAYPOINT_ARRIVAL_M, advance index.
       On reverse-loop: flip traversal direction at each end.
    5. Store updated state in BoatNavigator; return to TrafficCop on poll.

SIMULATED SMALL TARGETS

  The Simulator generates two categories of small targets for the patrol scope:

  Scripted paddleboarder:
    A single stand-up paddleboarder drifts slowly across the ferry departure
    lane on a fixed looping path (~0.5 kt, random drift added). RCS set to
    BOAT_SUP_RCS_M2 (default 0.2 m²). This target also appears on the fixed
    Marine PPI scope (same Bellingham Bay coverage area, same target pipeline).

  Random kayakers:
    BOAT_RANDOM_KAYAK_COUNT (default 2) kayaks wander within a defined zone
    near the ferry terminal and harbor mouth. RCS set to BOAT_KAYAK_RCS_M2
    (default 0.4 m² — slightly larger than SUP due to hull and occupant).
    Random targets also appear on the fixed Marine PPI.

  These use the same radar equation path as all other targets; the low RCS
  values naturally produce faint, intermittent blips in any sea state, which
  is the exhibit's intended behavior. No special-casing required.

  Settings.h additions for small targets:
    BOAT_SUP_RCS_M2         0.2    — stand-up paddleboard + paddler RCS (m²)
    BOAT_KAYAK_RCS_M2       0.4    — kayak + occupant RCS (m²)
    BOAT_RANDOM_KAYAK_COUNT 2      — number of random kayak targets
    BOAT_KAYAK_ZONE_LAT/LON        — bounding box for random kayak positions

SHARED STATE ADDITIONS

  SharedRenderState new fields (all under Mutex A):
    float boatLatDeg      = 0.0f   (set from JSON WP0 at startup)
    float boatLonDeg      = 0.0f
    float boatHeadingDeg  = 0.0f
    float boatSpeedKts    = 0.0f
    char  boatZone[64]    = ""     — zone label, copied from JSON waypoint
    bool  boatModeActive  = false  — set TRUE by ScopeManager when BoatPPIScope
                                     active, FALSE for all other scopes

TARGET PROJECTION FOR MOVING RADAR ORIGIN

  For fixed scopes, target positions are projected from a known constant origin.
  For the Boat PPI, TrafficCop recalculates each target's polar coordinates
  relative to the boat's current position after every poll.

  Flat-earth projection (adequate for 2 nm max range):
    dx_m = (target_lon − boat_lon) × cos(boat_lat × π/180) × METERS_PER_DEGREE
    dy_m = (target_lat − boat_lat) × METERS_PER_DEGREE
    range_m    = sqrt(dx_m² + dy_m²)
    bearing_deg = atan2(dx_m, dy_m) × 180/π   (adjusted to 0–360, CW from north)

  METERS_PER_DEGREE = 111320.0 (constexpr in settings.h).
  Result (range_m, bearing_deg, brightness) stored in TargetBuffer under Mutex B.

HEADING MARKER RENDERING

  BoatPPIScope::renderHeadingMarker() runs inside render() after all phosphor
  content, using the graticule shader so the line never decays.

  Geometry: dashed line from scope center to
    center + BOAT_HEADING_MARKER_FRACTION × scope_radius
    in the direction (boatHeadingDeg + bearingOffsetDeg).
  Color: BOAT_HEADING_MARKER_COLOR (default white).
  Dash/gap: BOAT_HEADING_MARKER_DASH_PX / BOAT_HEADING_MARKER_GAP_PX.

DISPLAY MODE LOGIC

  Every frame, BoatPPIScope computes:
    float diff = fabs(fmod(bearingOffsetDeg − boatHeadingDeg + 540.0f, 360.0f) − 180.0f)
    mode = (diff <= BOAT_HEADUP_TOLERANCE_DEG) ? "Head-up" : "North-up"
  Rendered as white text in the left-panel status area.

TERRAIN CLUTTER AND BREAKWATER SHADOWS

  See SHADOW / LINE-OF-SIGHT MASKING → Boat PPI scope scenario for the
  mask-selection algorithm and shadow_boat_NNN.u8 file set.

  The Squalicum Harbor outer breakwater is a significant shadow-caster.
  From any open-water patrol position the interior of the marina basin is
  shadowed — nothing behind the breakwater is visible. This is realistic
  and is visible on the scope as a sharp shadow arc on the far side of the
  breakwater return.

  At the start of each 4-second sweep, BoatPPIScope::updateLandClutter() calls:
    1. TerrainMap::selectNearestBoatMask(boatLatDeg, boatLonDeg)
    2. LandClutter::generateForBoat(boatLatDeg, boatLonDeg, maskIndex)
    3. Upload new polar clutter texture to GPU.
  If the mask index is unchanged from the previous sweep, steps 2–3 are skipped.

  Terrain clutter shader receives:
    u_radarOffsetM = vec2(
      (boatLon − MARINE_PLATFORM_LON) × cos(boatLat × π/180) × METERS_PER_DEGREE,
      (boatLat − MARINE_PLATFORM_LAT) × METERS_PER_DEGREE)

V1 GEOMETRY SCOPE (open water only — marina deferred)

  Vector features needed from NOAA ENC 18424 for the v1 patrol route:
    - Outer shoreline of Bellingham Bay
    - Squalicum Harbor outer breakwater (solid, strong return, shadow-caster)
    - Ferry terminal structure (Bellingham Cruise Terminal area)
    - Taylor Dock pier outline (weak return — wood, but pilings visible)
    - Boulevard Park shoreline
  Internal marina dock fingers, Whatcom Waterway channel walls, and Georgia
  Pacific / Waterfront District structures are deferred until the patrol
  route is extended into those areas in a future version.

==================================================================

FILE LAYOUT (COMPLETE — including additions)

==================================================================

src/
  main.cpp
  scope_manager.h / scope_manager.cpp
  scope.h / scope.cpp
  scope_intro.h / scope_intro.cpp
  scope_ascope.h / scope_ascope.cpp
  scope_marine_a.h / scope_marine_a.cpp
  scope_chain_home.h / scope_chain_home.cpp
  scope_ppi.h / scope_ppi.cpp
  scope_marine_ppi.h / scope_marine_ppi.cpp
  scope_boat_ppi.h / scope_boat_ppi.cpp
  scope_atc_ppi.h / scope_atc_ppi.cpp
  scope_par.h / scope_par.cpp
  phosphor.h / phosphor.cpp
  graticule.h / graticule.cpp
  left_panel.h / left_panel.cpp
  shared_render_state.h / shared_render_state.cpp
  target_buffer.h / target_buffer.cpp
  traffic_cop.h / traffic_cop.cpp
  simulator.h / simulator.cpp
  knob_panel.h / knob_panel.cpp
  rpi_receiver.h / rpi_receiver.cpp
  db_panel.h / db_panel.cpp        — Dear ImGui DB management panel
                                     (--database mode only)
  terrain_map.h / terrain_map.cpp  — DEM load, shadow mask, polar clutter
                                     grid; read-only after init, Thread 1
  land_clutter.h / land_clutter.cpp — per-sweep clutter arrays for A-scope
                                     range trace and PPI clutter texture
  terrain_preprocess.cpp           — standalone offline preprocessing tool;
                                     separate CMake target; links GDAL only;
                                     NOT part of main radar binary
  settings.h                       — all constexpr constants; no .cpp

  imgui/                           — Dear ImGui source, compiled in
    imgui.h / imgui.cpp
    imgui_impl_glfw.h / imgui_impl_glfw.cpp
    imgui_impl_opengl3.h / imgui_impl_opengl3.cpp
    imgui_draw.cpp / imgui_tables.cpp / imgui_widgets.cpp

shaders/
  phosphor.vert / phosphor.frag       — P1 and P7 via uniforms
  graticule.vert / graticule.frag
  text.vert / text.frag
  sweep.vert / sweep.frag
  bloom.vert / bloom.frag             — FBO bloom post-processing
  terrain_clutter.vert / terrain_clutter.frag — polar clutter texture overlay
                                               on PPI; P7-compatible decay;
                                               bearing offset rotation uniform;
                                               u_radarOffsetM vec2 for boat origin
