Compare commits

...

2 Commits

13 changed files with 539 additions and 65 deletions

View File

@@ -167,3 +167,41 @@ struct target_data_to_thread_1_structure {
uint32_t mmsi; // AIS unique identifier; ICAO hex address for aircraft uint32_t mmsi; // AIS unique identifier; ICAO hex address for aircraft
}; };
===============================================================================
CURRENT IMPLEMENTATION STATUS (stub phase)
Files: include/01_classes.h, src/01_classes.cpp
What is built:
RadarBase — abstract base class with three pure virtual methods:
render_left_panel() — renders radar description and keyboard control list
render_scope() — renders scope content (stub: radar name + "stub" label)
render_status_bar() — renders current control uniform values
Four stub subclasses, each overriding all three methods:
ChainHomeRadar (Ventnor, Isle of Wight)
MarineAScopeRadar (Community Boating Center, Bellingham Bay)
MarineTrafficRadar (center of Bellingham Bay)
PoliceboatRadar (Taylor Dock / Boulevard Park, Bellingham Bay)
One global instance of each subclass is declared in 01_classes.h and defined
in 01_classes.cpp: g_chain_home, g_marine_ascope, g_marine_traffic, g_police_boat.
What is NOT yet built (future components):
- Real scope rendering (radar sweep, target blips, graticule, range rings/ticks,
north indicator, P7 persistence layer, land/terrain background)
- Target data ingestion from Traffic Cop
- The Keyboard controls class (keyboard handling currently lives in the
static key_callback() function in src/main.cpp)
- The Traffic Cop class (Thread 2) — not yet started
- The Simulator class (Thread 3) — not yet started
- Per-frame update() method — the 30 Hz render loop in main.cpp calls
render_left_panel(), render_scope(), and render_status_bar() directly
Both target data structures (target_data_structure and
target_data_to_thread_1_structure) are defined in
include/data_structure_and_constants.h and will be used when the
Traffic Cop and Simulator components are built.

View File

@@ -69,3 +69,57 @@ field:
instructs the user to use the 1 and 2 keyboard keys to select the radar. The 1 key instructs the user to use the 1 and 2 keyboard keys to select the radar. The 1 key
steps forward in a circle and the 2 key steps backward and the enter key makes the steps forward in a circle and the 2 key steps backward and the enter key makes the
selection selection
===============================================================================
CURRENT IMPLEMENTATION STATUS
This spec covers two related but distinct pieces of the 02_text_description component:
A. Text renderer engine
Files: include/02_text_description.h, src/02_text_description.cpp
Status: COMPLETE
Implements the TextRenderer class using FreeType. Loads printable ASCII glyphs
(codes 32-126) from a TTF font into individual GL_RED textures and renders them
as textured quads into whichever viewport is currently active.
Public methods:
initialize_shaders() — compiles/links shaders/02_text_description_{vert,frag}.glsl
initialize_font(path, size) — loads a TTF font at a given pixel height
render_text() — renders a string at a viewport-local pixel coordinate
render_text_wrapped() — renders with automatic word-wrapping at a max pixel width
get_text_width() — returns the pixel width of a string at a given scale
set_projection(w, h) — sets the orthographic projection for a viewport
cleanup() — releases all GL resources and glyph textures
Two global instances are defined in src/02_text_description.cpp and declared
extern in the header:
g_text_renderer — loaded at FONT_SIZE_NORMAL (body text, controls, status bar)
g_text_renderer_large — loaded at FONT_SIZE_TITLE (panel titles, scope headings)
Font path and sizes are defined in include/settings.h:
FONT_PATH "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
FONT_SIZE_NORMAL 18 px
FONT_SIZE_TITLE 26 px
B. Left-panel text content for each radar
File: src/01_classes.cpp (render_left_panel() methods in each radar subclass)
Status: STUB — text content implemented; actual scope rendering is not yet done.
Each radar subclass in 01_classes renders:
- Radar title (large renderer, white)
- Location sub-header (small scale, white)
- 3-4 lines of description text (white)
- "--- CONTROLS ---" header (red) followed by per-control entries
(label in red, keystroke(s) in pink, on the same line)
- "1 - Return to Radar Selection" (yellow)
The intro scene (item 5 above) is rendered by render_intro_left_panel() in
src/main.cpp, which shows a welcome paragraph on the left panel and the
radar selection list centred in the scope viewport.
Status bar content for each radar is rendered by render_status_bar() in
src/01_classes.cpp, displaying current uniform values (intensity, sensitivity,
STC sensitivity/range, and radar-specific values such as max range, cursor
positions, radiogoniometer, or antenna bearing) in yellow.

View File

@@ -33,3 +33,65 @@ The following is suggested.
5. Run in a loop where every 30th of a second, update the selected radar. 5. Run in a loop where every 30th of a second, update the selected radar.
====================================================================== ======================================================================
CURRENT IMPLEMENTATION STATUS
File: src/main.cpp
Status: COMPLETE (stub phase)
The five-step flow described above is fully implemented in main():
Step 1 — parse_args()
Parses argv[] for radar names (chain_home, marine_ascope, marine_traffic,
police_boat). Sets radar_management[].available for each named radar.
With no arguments, all radars are made available.
STUB NOTE: The current build unconditionally sets operatable=1 for all four
radars before checking availability. When real radar components are built,
only the ones that are fully implemented should have operatable set to 1.
The check "available only if operatable" already works correctly in the code;
only the stub's operatable override needs to be updated.
Step 2 — GLFW / GLAD / OpenGL initialization
Creates a 1920x1080 window (WINDOW_WIDTH x WINDOW_HEIGHT from settings.h),
initialises GLAD, enables GL_DEBUG_OUTPUT (controlled by ENABLE_GL_DEBUG_OUTPUT
in settings.h), and sets global GL state (blending, no depth test).
Step 3 — Intro / selection screen
Renders the welcome text on the left panel (render_intro_left_panel()) and
the radar selection list centred in the scope viewport (render_radar_list()).
The status bar shows keyboard instructions (render_selection_status_bar()).
Step 4 — Selection loop
The GLFW key callback (key_callback) handles:
- Key 1: step selection forward
- Key 2: step selection backward
- ENTER: activate the highlighted radar; call reset_radar_controls()
- ESC: close the window
If exactly one radar is available, the application auto-activates it after
5 seconds (shows the selection screen during the countdown; breaks early
if ENTER is pressed).
Step 5 — Main render loop (~30 Hz)
Uses std::chrono::steady_clock to target a 1/30 s frame period.
Each frame:
- Polls GLFW events
- If SELECTION state: calls render_selection_screen()
- If OPERATING state: calls render_operating_screen(), which delegates
to g_active_radar->render_left_panel(), render_status_bar(),
render_scope() through the RadarBase virtual interface.
Additional features implemented:
- GL debug callback (gl_debug_callback) enabled via ENABLE_GL_DEBUG_OUTPUT
- Debug border-box renderer (draw_border_box) enabled via DEBUG_DRAW_WINDOW_BORDERS
in settings.h — draws white outlines around each panel for layout verification
- reset_radar_controls(): resets uniform_max_range, uniform_range_cursor,
uniform_bearing_cursor to defaults for the selected radar when it is activated
- Keyboard controls for all scope uniforms (intensity, sensitivity, STC, noise
filter, radiogoniometer, max range, range cursor, bearing cursor, antenna
rotation) are implemented in key_callback(); the controls are appropriately
restricted by radar type (e.g. noise filter only for PPI scopes)
Application state is tracked by AppState enum (SELECTION, OPERATING) in main.cpp.
The active radar object is pointed to by g_active_radar (a RadarBase*).
current_radar (global in global_data.cpp) holds the RADAR_* index of the active radar.

View File

@@ -227,9 +227,11 @@ These are knobs on the panel once that is built. Keyboard keys until then
i for right turn. - uniform is float radiogoniometer; i for right turn. - uniform is float radiogoniometer;
8. Maximum Range for all scopes except for chain home, which is fixed. This control 8. Maximum Range for all scopes except for chain home, which is fixed. This control
will not operate for Chain Home. These maximum will not operate for Chain Home. These maximum
range selections will be defined for each scope; If you try to go beyond the stated This is a stop control. Each step for for each maximum range for the selected radar.
maximum ranges, the control will have no effect; note that the default maximum range would those maximum ranges are in a table in settings.h. If you try to step beyond the top maximum
be the highest for that scope; keyboard o for lower; p for higher uniform is float max_range; range for that radar, the step higher will do noting. If yoi try to step beyond the lowest
maximum range, the the lower step key will be ignored.
keyboard o for stepping lower; p for stepping higher. uniform is float max_range;
9. Range Cursor (for ppi scopes, not for a scopes) 9. Range Cursor (for ppi scopes, not for a scopes)
keyboard a for lower; s for higher - uniform is float range_cursor; keyboard a for lower; s for higher - uniform is float range_cursor;
10. Bearing Cursor (for all ppi scopes; not for a scopes) 10. Bearing Cursor (for all ppi scopes; not for a scopes)

101
README.md
View File

@@ -1,3 +1,100 @@
# radar-simulation # radar-simulator
Source code for possible radar exhibit Museum exhibit software simulating 1940s1960s vintage radar systems.
## Project overview
This application runs as a full-screen interactive exhibit on a Geekom A8 Max
(AMD Ryzen 9 8945HS, Radeon 780M GPU, Ubuntu 25.10). Visitors can explore four
historical radar types by cycling through a selection screen and interacting with
keyboard controls.
**Four radar displays:**
| # | Name | Type | Location |
|---|------|------|----------|
| 1 | Chain Home | A-scope (WWII, 1940s) | RAF Ventnor, Isle of Wight |
| 2 | Marine A-Scope | A-scope (pre-PPI, manually steered dish) | Community Boating Center, Bellingham Bay WA |
| 3 | Marine Traffic Control | PPI scope | Center of Bellingham Bay WA |
| 4 | Police Boat | PPI scope (moving platform) | Taylor Dock / Boulevard Park, Bellingham Bay WA |
## Tech stack
- **Language:** C++20
- **Graphics:** OpenGL 4.3 Core, GLFW, GLAD
- **Text:** FreeType 2
- **Maps/LIDAR:** GDAL (libgdal-dev)
- **Database:** PostgreSQL (database: radar, user: radar)
- **Build:** CMake
## Build
```bash
mkdir -p build && cd build
cmake ..
make -j$(nproc)
```
The executable is `build/radar_simulator`.
## Running
```bash
# All four radars available
./radar_simulator
# Specific radars only
./radar_simulator chain_home marine_ascope
./radar_simulator marine_traffic police_boat
```
If only one radar is specified, it auto-activates after 5 seconds.
## Screen layout
The window is 1920×1080 and is divided into three areas:
- **Left text panel** (upper-left, 500 px wide): radar description and keyboard controls
- **Status bar** (lower-left, 500×250 px): current control values in yellow
- **Scope** (right side, square, full height): radar display
## Keyboard controls
| Key | Action |
|-----|--------|
| **1** | Step forward in radar selection (or return to selection when operating) |
| **2** | Step backward in radar selection |
| **ENTER** | Activate selected radar |
| **ESC** | Exit |
| **3 / 4** | Intensity lower / higher |
| **5 / 6** | Receiver sensitivity lower / higher |
| **Q / W** | STC sensitivity lower / higher |
| **E / R** | STC range lower / higher |
| **T / Y** | Rain noise filter lower / higher (PPI scopes only) |
| **U / I** | Radiogoniometer left / right (Chain Home only) |
| **O / P** | Max range step down / up (all except Chain Home) |
| **A / S** | Range cursor lower / higher (PPI scopes only) |
| **D / F** | Bearing cursor CCW / CW (PPI scopes only) |
| **G / H** | Antenna rotation CCW / CW (Marine A-Scope only) |
## Component .md files
Each component has a specification and status file:
- `01_classes.md` — class hierarchy (RadarBase + 4 subclasses; stubs built)
- `02_text_description.md` — text renderer and left-panel text content (complete)
- `03_setup_and_radar_selection.md` — startup, selection loop, main render loop (complete)
## Current build state
The initialization and radar-management infrastructure is complete:
- Four stub radar subclasses render description text, keyboard control lists,
and current control values in the status bar.
- The scope area shows the radar name with a `[stub]` label — no actual radar
simulation is drawn yet.
- All keyboard controls update CPU globals; uniforms will be pushed to shaders
as each scope component is built.
Real radar scope rendering (sweep, targets, graticule, range rings/ticks,
terrain, P7 phosphor persistence) will be added one radar at a time.

View File

@@ -1,4 +1,33 @@
1. build a stub for each radar that displays it's information in the left panel; then show the settngs of its controls on the status panel. In the main scope, just put the name in that panel. We do not have As you know, you have enabled the controls on each radar enough to show that they are working, although
any workings. This is only for testing the operation of the initialization and management. there are no uniform variables yet in the shaders.
2. Set the radar management structure to have all radars fully built and available to enable the operator to go through the radar selection and operating the controls. This next step is to correct a mistake I made, especially with the maximum range settings for the radars
as well as the cursors.
First of all remove the control for the maximum range for the chain home radar. The only range option
for that is 200 nm. This means removing the control as well as setting the global variable for the
maximum range for chain home to 200nm. Later on when development starts with chain home, we need to
have a uniform variable for maximum range.
Here are the changes I need for the controls.
Impliment the step changes for the maximum range and indicate the maximum range in the text
status. I changed the maximum range setting to the step setting.
The control for the bearing shouid show the following:
1. Degrees from true north, going clockwise for advance or counterclockwise for keyboard for going
down. This is for chain home, ascope marine, and marine traffic control.
2. Degrees from front of boat for the police boat radar, going clockwise for advance or
counterclockwise for keyboard for going
3. Note that clockwise is the direction for bearing going positive.
4. If the operator goes beyond the meximum of 360 clockwise, we reset to 0
and if the operator goes below 0 counterclockwise, we reset to 360.
The control for the range cursor; keep normalized from 0 to 1. However, please take that
normalized value and convert to actual range using the current max range setting and display
the actual range in the text status window.

View File

@@ -122,10 +122,15 @@ float uniform_noise_filter = 0.0f;
// Components: Chain Home A-scope // Components: Chain Home A-scope
float uniform_radiogoniometer = 0.0f; float uniform_radiogoniometer = 0.0f;
// Maximum radar range (scope-dependent units) — keyboard o (lower) / p (higher) /*
// GLSL uniform: float max_range * Maximum radar range in nautical miles (NOT normalized).
// Components: Marine A-scope, PPI scopes (not Chain Home; that is fixed) * Chain Home: fixed at CHAIN_HOME_MAX_RANGE_NM; o/p keys ignored.
float uniform_max_range = 1.0f; * Others: set from the radar's step table when a radar is activated; updated by o/p.
* 0.0 is the placeholder before any radar is selected.
* GLSL uniform: float max_range
* Components: all scope renderers
*/
float uniform_max_range = 0.0f;
// Range cursor position (normalized 0-1) — keyboard a (lower) / s (higher) // Range cursor position (normalized 0-1) — keyboard a (lower) / s (higher)
// GLSL uniform: float range_cursor // GLSL uniform: float range_cursor

View File

@@ -78,6 +78,14 @@ public:
/* Return the rendered pixel width of text at the given scale. */ /* Return the rendered pixel width of text at the given scale. */
float get_text_width(const std::string& text, float scale) const; float get_text_width(const std::string& text, float scale) const;
/* Render text at (x, y), wrapping on word boundaries when a line would
* exceed max_width pixels. y is decremented by line_height * scale after
* each rendered line, including the last. Never breaks within a word. */
void render_text_wrapped(const std::string& text,
float x, float& y, float scale,
float max_width, float line_height,
float color_r, float color_g, float color_b);
/* Set the orthographic projection for a viewport of w × h pixels. /* Set the orthographic projection for a viewport of w × h pixels.
* Call this each time glViewport changes before rendering text. */ * Call this each time glViewport changes before rendering text. */
void set_projection(int w, int h); void set_projection(int w, int h);

View File

@@ -194,9 +194,14 @@ extern float uniform_noise_filter;
// Components: Chain Home A-scope // Components: Chain Home A-scope
extern float uniform_radiogoniometer; extern float uniform_radiogoniometer;
// Maximum radar range, all scopes except Chain Home — keyboard o (lower) / p (higher) /*
// GLSL uniform: float max_range * Maximum radar range in nautical miles (NOT normalized).
// Components: Marine A-scope, PPI scopes * Chain Home is fixed at CHAIN_HOME_MAX_RANGE_NM; the o/p keys have no effect for it.
* For other radars the value is taken from the radar's step table and updated by o/p.
* Set via reset_radar_controls() when a radar is activated.
* GLSL uniform: float max_range
* Components: all scope renderers
*/
extern float uniform_max_range; extern float uniform_max_range;
// Range cursor, PPI scopes only — keyboard a (lower) / s (higher) // Range cursor, PPI scopes only — keyboard a (lower) / s (higher)

View File

@@ -86,47 +86,85 @@
* Component: Chain Home scope (1940s, World War 2) * Component: Chain Home scope (1940s, World War 2)
* ============================================================ */ * ============================================================ */
// Fixed maximum range in km; the max_range control has no effect for this scope // Fixed maximum range in nautical miles; the o/p range control has no effect for this scope
#define CHAIN_HOME_MAX_RANGE_KM 200.0f #define CHAIN_HOME_MAX_RANGE_NM 200.0f
/* ============================================================ /* ============================================================
* MARINE A-SCOPE * MARINE A-SCOPE
* Component: 1940s marine A-scope (pre-PPI) * Component: 1940s marine A-scope (pre-PPI)
* ============================================================ */ * ============================================================ */
// Selectable maximum range steps in nautical miles (o/p keys cycle through these) // Selectable maximum range steps in nautical miles (o/p keys step through these)
#define MARINE_A_SCOPE_NUM_RANGES 4 #define MARINE_A_SCOPE_NUM_RANGES 4
#define MARINE_A_SCOPE_RANGE_STEP_1 3.0f #define MARINE_A_SCOPE_RANGE_STEP_1 3.0f
#define MARINE_A_SCOPE_RANGE_STEP_2 6.0f #define MARINE_A_SCOPE_RANGE_STEP_2 6.0f
#define MARINE_A_SCOPE_RANGE_STEP_3 12.0f #define MARINE_A_SCOPE_RANGE_STEP_3 12.0f
#define MARINE_A_SCOPE_RANGE_STEP_4 24.0f #define MARINE_A_SCOPE_RANGE_STEP_4 24.0f
// Pip spacing on the A-scope sweep (nm between range pips), one value per range step
#define MARINE_A_SCOPE_PIP_STEP_1 0.5f /* 3 nm range — pip every 0.5 nm */
#define MARINE_A_SCOPE_PIP_STEP_2 1.0f /* 6 nm range — pip every 1.0 nm */
#define MARINE_A_SCOPE_PIP_STEP_3 2.0f /* 12 nm range — pip every 2.0 nm */
#define MARINE_A_SCOPE_PIP_STEP_4 4.0f /* 24 nm range — pip every 4.0 nm */
/* ============================================================ /* ============================================================
* PPI SCOPE - MARINE TRAFFIC CONTROL * PPI SCOPE - MARINE TRAFFIC CONTROL
* Component: PPI scope, marine traffic control station * Component: PPI scope, marine traffic control station
* ============================================================ */ * ============================================================ */
// Selectable maximum range steps in nautical miles // Selectable maximum range steps in nautical miles
#define PPI_MTC_NUM_RANGES 5 #define PPI_MTC_NUM_RANGES 5
#define PPI_MTC_RANGE_STEP_1 3.0f #define PPI_MTC_RANGE_STEP_1 3.0f
#define PPI_MTC_RANGE_STEP_2 6.0f #define PPI_MTC_RANGE_STEP_2 6.0f
#define PPI_MTC_RANGE_STEP_3 12.0f #define PPI_MTC_RANGE_STEP_3 12.0f
#define PPI_MTC_RANGE_STEP_4 24.0f #define PPI_MTC_RANGE_STEP_4 24.0f
#define PPI_MTC_RANGE_STEP_5 48.0f #define PPI_MTC_RANGE_STEP_5 48.0f
/*
* Range ring positions (nm from centre) per range step.
* Two rings per step, placed at 1/3 and 2/3 of the maximum range.
* No ring at the maximum range itself — that is the scope edge / graticule boundary.
*/
#define PPI_MTC_RING1_STEP_1 1.0f /* 3 nm — inner ring */
#define PPI_MTC_RING2_STEP_1 2.0f /* 3 nm — outer ring */
#define PPI_MTC_RING1_STEP_2 2.0f /* 6 nm */
#define PPI_MTC_RING2_STEP_2 4.0f
#define PPI_MTC_RING1_STEP_3 4.0f /* 12 nm */
#define PPI_MTC_RING2_STEP_3 8.0f
#define PPI_MTC_RING1_STEP_4 8.0f /* 24 nm */
#define PPI_MTC_RING2_STEP_4 16.0f
#define PPI_MTC_RING1_STEP_5 16.0f /* 48 nm */
#define PPI_MTC_RING2_STEP_5 32.0f
/* ============================================================ /* ============================================================
* PPI SCOPE - ON-BOARD BOAT * PPI SCOPE - ON-BOARD BOAT
* Component: PPI scope, on-board boat view * Component: PPI scope, on-board boat view
* ============================================================ */ * ============================================================ */
// Selectable maximum range steps in nautical miles // Selectable maximum range steps in nautical miles
#define PPI_BOAT_NUM_RANGES 5 #define PPI_BOAT_NUM_RANGES 5
#define PPI_BOAT_RANGE_STEP_1 1.5f #define PPI_BOAT_RANGE_STEP_1 1.5f
#define PPI_BOAT_RANGE_STEP_2 3.0f #define PPI_BOAT_RANGE_STEP_2 3.0f
#define PPI_BOAT_RANGE_STEP_3 6.0f #define PPI_BOAT_RANGE_STEP_3 6.0f
#define PPI_BOAT_RANGE_STEP_4 12.0f #define PPI_BOAT_RANGE_STEP_4 12.0f
#define PPI_BOAT_RANGE_STEP_5 24.0f #define PPI_BOAT_RANGE_STEP_5 24.0f
/*
* Range ring positions (nm from centre) per range step.
* Two rings per step, placed at 1/3 and 2/3 of the maximum range.
* No ring at the maximum range itself — that is the scope edge / graticule boundary.
*/
#define PPI_BOAT_RING1_STEP_1 0.5f /* 1.5 nm — inner ring */
#define PPI_BOAT_RING2_STEP_1 1.0f /* 1.5 nm — outer ring */
#define PPI_BOAT_RING1_STEP_2 1.0f /* 3 nm */
#define PPI_BOAT_RING2_STEP_2 2.0f
#define PPI_BOAT_RING1_STEP_3 2.0f /* 6 nm */
#define PPI_BOAT_RING2_STEP_3 4.0f
#define PPI_BOAT_RING1_STEP_4 4.0f /* 12 nm */
#define PPI_BOAT_RING2_STEP_4 8.0f
#define PPI_BOAT_RING1_STEP_5 8.0f /* 24 nm */
#define PPI_BOAT_RING2_STEP_5 16.0f
/* ============================================================ /* ============================================================
* WINDOW AND PANEL LAYOUT * WINDOW AND PANEL LAYOUT
* Component: Thread 1 (main.cpp), 01_classes, 02_text_description * Component: Thread 1 (main.cpp), 01_classes, 02_text_description
@@ -198,7 +236,7 @@
#define BEARING_CURSOR_STEP 1.0f #define BEARING_CURSOR_STEP 1.0f
#define RANGE_CURSOR_STEP 0.02f #define RANGE_CURSOR_STEP 0.02f
#define MARINE_A_BEARING_STEP 1.0f #define MARINE_A_BEARING_STEP 1.0f
#define MAX_RANGE_STEP 0.1f // Max range is now step-indexed (see MARINE_A_SCOPE_*, PPI_MTC_*, PPI_BOAT_* tables above)
/* ============================================================ /* ============================================================
* KEYBOARD CONTROL LIMITS * KEYBOARD CONTROL LIMITS
@@ -217,5 +255,4 @@
#define NOISE_FILTER_MAX 1.0f #define NOISE_FILTER_MAX 1.0f
#define RANGE_CURSOR_MIN 0.0f #define RANGE_CURSOR_MIN 0.0f
#define RANGE_CURSOR_MAX 1.0f #define RANGE_CURSOR_MAX 1.0f
#define MAX_RANGE_MIN 0.1f // Max range limits removed — range is now step-indexed, clamped by table size
#define MAX_RANGE_MAX 1.0f

View File

@@ -80,6 +80,14 @@ static void text_line(const std::string& text, float x, float& y,
y -= LH * scale; y -= LH * scale;
} }
/* Render a body-text line with word wrapping; y advances by one line per
* wrapped output line. max_width is the available horizontal pixel span. */
static void text_line_wrapped(const std::string& text, float x, float& y,
float scale, float max_width,
float cr, float cg, float cb) {
g_text_renderer.render_text_wrapped(text, x, y, scale, max_width, LH, cr, cg, cb);
}
/* Render one title-size line using the large renderer at scale 1.0. */ /* Render one title-size line using the large renderer at scale 1.0. */
static void text_line_large(const std::string& text, float x, float& y, static void text_line_large(const std::string& text, float x, float& y,
float cr, float cg, float cb) { float cr, float cg, float cb) {
@@ -151,12 +159,12 @@ static void render_scope_name(const std::string& radar_name) {
static void render_common_status(float x, float& y, float scale) { static void render_common_status(float x, float& y, float scale) {
char buf[320]; char buf[320];
snprintf(buf, sizeof(buf), snprintf(buf, sizeof(buf),
"Intensity: %.2f | Sensitivity: %.2f | STC Sens: %.2f | STC Range: %.2f", "Intensity: %.2f | Sensitivity: %.2f | STC Sens: %.2f | STC Range: %.1f nm",
uniform_intensity, uniform_sensitivity, uniform_intensity, uniform_sensitivity,
uniform_stc_sensitivity, uniform_stc_range); uniform_stc_sensitivity, uniform_stc_range * uniform_max_range);
g_text_renderer.render_text(buf, x, y, scale, float max_w = static_cast<float>(LEFT_PANEL_WIDTH) - 2.0f * MARGIN;
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]); text_line_wrapped(buf, x, y, scale, max_w,
y -= LH * scale; COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
} }
/* ================================================================== */ /* ================================================================== */
@@ -221,16 +229,18 @@ void ChainHomeRadar::render_scope() {
void ChainHomeRadar::render_status_bar() { void ChainHomeRadar::render_status_bar() {
float x = MARGIN; float x = MARGIN;
float y = static_cast<float>(STATUS_BAR_HEIGHT) - MARGIN - LH; float y = static_cast<float>(STATUS_BAR_HEIGHT) - MARGIN - LH;
float max_w = static_cast<float>(LEFT_PANEL_WIDTH) - 2.0f * MARGIN;
text_line("CHAIN HOME RADAR (Ventnor, Isle of Wight) | Fixed range: 200 km", text_line_wrapped("CHAIN HOME RADAR (Ventnor, Isle of Wight) | Fixed range: 200 nm",
x, y, SCALE_NORMAL, COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]); x, y, SCALE_NORMAL, max_w,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
render_common_status(x, y, SCALE_NORMAL); render_common_status(x, y, SCALE_NORMAL);
char buf[200]; char buf[200];
snprintf(buf, sizeof(buf), "Radiogoniometer: %.1f deg", uniform_radiogoniometer); snprintf(buf, sizeof(buf), "Radiogoniometer: %.1f deg", uniform_radiogoniometer);
g_text_renderer.render_text(buf, x, y, SCALE_NORMAL, text_line_wrapped(buf, x, y, SCALE_NORMAL, max_w,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]); COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
} }
/* ================================================================== */ /* ================================================================== */
@@ -291,18 +301,20 @@ void MarineAScopeRadar::render_scope() {
void MarineAScopeRadar::render_status_bar() { void MarineAScopeRadar::render_status_bar() {
float x = MARGIN; float x = MARGIN;
float y = static_cast<float>(STATUS_BAR_HEIGHT) - MARGIN - LH; float y = static_cast<float>(STATUS_BAR_HEIGHT) - MARGIN - LH;
float max_w = static_cast<float>(LEFT_PANEL_WIDTH) - 2.0f * MARGIN;
text_line("MARINE A-SCOPE RADAR (Community Boating Center, Bellingham Bay)", text_line_wrapped("MARINE A-SCOPE RADAR (Community Boating Center, Bellingham Bay)",
x, y, SCALE_NORMAL, COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]); x, y, SCALE_NORMAL, max_w,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
render_common_status(x, y, SCALE_NORMAL); render_common_status(x, y, SCALE_NORMAL);
char buf[200]; char buf[200];
snprintf(buf, sizeof(buf), snprintf(buf, sizeof(buf),
"Max Range (normalized): %.2f | Antenna Bearing: %.1f deg", "Max Range: %.1f nm | Antenna Bearing: %.1f deg",
uniform_max_range, uniform_marine_a_bearing); uniform_max_range, uniform_marine_a_bearing);
g_text_renderer.render_text(buf, x, y, SCALE_NORMAL, text_line_wrapped(buf, x, y, SCALE_NORMAL, max_w,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]); COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
} }
/* ================================================================== */ /* ================================================================== */
@@ -365,20 +377,22 @@ void MarineTrafficRadar::render_scope() {
void MarineTrafficRadar::render_status_bar() { void MarineTrafficRadar::render_status_bar() {
float x = MARGIN; float x = MARGIN;
float y = static_cast<float>(STATUS_BAR_HEIGHT) - MARGIN - LH; float y = static_cast<float>(STATUS_BAR_HEIGHT) - MARGIN - LH;
float max_w = static_cast<float>(LEFT_PANEL_WIDTH) - 2.0f * MARGIN;
text_line("MARINE TRAFFIC CONTROL RADAR (Bellingham Bay)", text_line_wrapped("MARINE TRAFFIC CONTROL RADAR (Bellingham Bay)",
x, y, SCALE_NORMAL, COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]); x, y, SCALE_NORMAL, max_w,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
render_common_status(x, y, SCALE_NORMAL); render_common_status(x, y, SCALE_NORMAL);
char buf[280]; char buf[280];
snprintf(buf, sizeof(buf), snprintf(buf, sizeof(buf),
"Noise Filter: %.2f | Max Range (norm): %.2f | " "Noise Filter: %.2f | Max Range: %.1f nm | "
"Range Cursor: %.2f | Bearing Cursor: %.1f deg", "Range Cursor: %.2f nm | Bearing Cursor: %.1f deg",
uniform_noise_filter, uniform_max_range, uniform_noise_filter, uniform_max_range,
uniform_range_cursor, uniform_bearing_cursor); uniform_range_cursor * uniform_max_range, uniform_bearing_cursor);
g_text_renderer.render_text(buf, x, y, SCALE_NORMAL, text_line_wrapped(buf, x, y, SCALE_NORMAL, max_w,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]); COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
} }
/* ================================================================== */ /* ================================================================== */
@@ -443,18 +457,20 @@ void PoliceboatRadar::render_scope() {
void PoliceboatRadar::render_status_bar() { void PoliceboatRadar::render_status_bar() {
float x = MARGIN; float x = MARGIN;
float y = static_cast<float>(STATUS_BAR_HEIGHT) - MARGIN - LH; float y = static_cast<float>(STATUS_BAR_HEIGHT) - MARGIN - LH;
float max_w = static_cast<float>(LEFT_PANEL_WIDTH) - 2.0f * MARGIN;
text_line("POLICE BOAT RADAR (Bellingham Bay - moving)", text_line_wrapped("POLICE BOAT RADAR (Bellingham Bay - moving)",
x, y, SCALE_NORMAL, COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]); x, y, SCALE_NORMAL, max_w,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
render_common_status(x, y, SCALE_NORMAL); render_common_status(x, y, SCALE_NORMAL);
char buf[280]; char buf[280];
snprintf(buf, sizeof(buf), snprintf(buf, sizeof(buf),
"Noise Filter: %.2f | Max Range (norm): %.2f | " "Noise Filter: %.2f | Max Range: %.1f nm | "
"Range Cursor: %.2f | Bearing Cursor: %.1f deg", "Range Cursor: %.2f nm | Bearing Cursor: %.1f deg",
uniform_noise_filter, uniform_max_range, uniform_noise_filter, uniform_max_range,
uniform_range_cursor, uniform_bearing_cursor); uniform_range_cursor * uniform_max_range, uniform_bearing_cursor);
g_text_renderer.render_text(buf, x, y, SCALE_NORMAL, text_line_wrapped(buf, x, y, SCALE_NORMAL, max_w,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]); COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
} }

View File

@@ -247,6 +247,38 @@ float TextRenderer::get_text_width(const std::string& text, float scale) const {
return width; return width;
} }
void TextRenderer::render_text_wrapped(const std::string& text,
float x, float& y, float scale,
float max_width, float line_height,
float color_r, float color_g, float color_b) {
std::string line;
std::string::size_type pos = 0;
std::string::size_type slen = text.length();
while (pos <= slen) {
std::string::size_type nxt = text.find(' ', pos);
if (nxt == std::string::npos) nxt = slen;
std::string word = text.substr(pos, nxt - pos);
if (!word.empty()) {
std::string candidate = line.empty() ? word : (line + " " + word);
if (!line.empty() && get_text_width(candidate, scale) > max_width) {
render_text(line, x, y, scale, color_r, color_g, color_b);
y -= line_height * scale;
line = word;
} else {
line = candidate;
}
}
pos = nxt + 1;
}
if (!line.empty()) {
render_text(line, x, y, scale, color_r, color_g, color_b);
y -= line_height * scale;
}
}
void TextRenderer::set_projection(int w, int h) { void TextRenderer::set_projection(int w, int h) {
/* Column-major orthographic matrix mapping pixel space [0,w]x[0,h] /* Column-major orthographic matrix mapping pixel space [0,w]x[0,h]
* to NDC. Near=-1, far=1 (depth unused for 2D text). */ * to NDC. Near=-1, far=1 (depth unused for 2D text). */

View File

@@ -57,6 +57,27 @@
#include <chrono> #include <chrono>
#include <thread> #include <thread>
/* ------------------------------------------------------------------ */
/* Range step lookup tables */
/* Mirror the settings.h defines as arrays so the keyboard handler */
/* can index them at run time. */
/* ------------------------------------------------------------------ */
static const float ASCOPE_RANGES[MARINE_A_SCOPE_NUM_RANGES] = {
MARINE_A_SCOPE_RANGE_STEP_1, MARINE_A_SCOPE_RANGE_STEP_2,
MARINE_A_SCOPE_RANGE_STEP_3, MARINE_A_SCOPE_RANGE_STEP_4
};
static const float MTC_RANGES[PPI_MTC_NUM_RANGES] = {
PPI_MTC_RANGE_STEP_1, PPI_MTC_RANGE_STEP_2, PPI_MTC_RANGE_STEP_3,
PPI_MTC_RANGE_STEP_4, PPI_MTC_RANGE_STEP_5
};
static const float BOAT_RANGES[PPI_BOAT_NUM_RANGES] = {
PPI_BOAT_RANGE_STEP_1, PPI_BOAT_RANGE_STEP_2, PPI_BOAT_RANGE_STEP_3,
PPI_BOAT_RANGE_STEP_4, PPI_BOAT_RANGE_STEP_5
};
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Debug border-box line renderer (DEBUG_DRAW_WINDOW_BORDERS) */ /* Debug border-box line renderer (DEBUG_DRAW_WINDOW_BORDERS) */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@@ -242,6 +263,36 @@ static void GLAPIENTRY gl_debug_callback(GLenum /*source*/,
fprintf(stderr, "[GL %s] %s\n", type_str, message); fprintf(stderr, "[GL %s] %s\n", type_str, message);
} }
/* ------------------------------------------------------------------ */
/* reset_radar_controls: called when a radar is activated */
/* Resets range to the widest step and zeroes the cursors. */
/* ------------------------------------------------------------------ */
static void reset_radar_controls(int radar_idx) {
switch (radar_idx) {
case RADAR_CHAIN_HOME:
uniform_max_range = CHAIN_HOME_MAX_RANGE_NM;
break;
case RADAR_MARINE_ASCOPE:
ascope_range_setting = MARINE_A_SCOPE_NUM_RANGES - 1;
uniform_max_range = ASCOPE_RANGES[ascope_range_setting];
break;
case RADAR_MARINE_TRAFFIC:
traffic_control_range_setting = PPI_MTC_NUM_RANGES - 1;
uniform_max_range = MTC_RANGES[traffic_control_range_setting];
break;
case RADAR_POLICE_BOAT:
police_boat_range_setting = PPI_BOAT_NUM_RANGES - 1;
uniform_max_range = BOAT_RANGES[police_boat_range_setting];
break;
default:
uniform_max_range = 0.0f;
break;
}
uniform_range_cursor = 0.0f;
uniform_bearing_cursor = 0.0f;
}
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Keyboard callback (Thread 1) */ /* Keyboard callback (Thread 1) */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@@ -267,13 +318,14 @@ static void key_callback(GLFWwindow* window, int key, int /*scancode*/,
(g_selection_cursor - 1 + static_cast<int>(g_selectable.size())) (g_selection_cursor - 1 + static_cast<int>(g_selectable.size()))
% static_cast<int>(g_selectable.size()); % static_cast<int>(g_selectable.size());
} else if (key == GLFW_KEY_ENTER || key == GLFW_KEY_KP_ENTER) { } else if (key == GLFW_KEY_ENTER || key == GLFW_KEY_KP_ENTER) {
/* Activate selected radar. */ /* Activate selected radar; reset all controls to defaults for that radar. */
int mi = g_selectable[g_selection_cursor].management_index; int mi = g_selectable[g_selection_cursor].management_index;
for (int i = 0; i < RADAR_COUNT; i++) radar_management[i].selected = 0; for (int i = 0; i < RADAR_COUNT; i++) radar_management[i].selected = 0;
radar_management[mi].selected = 1; radar_management[mi].selected = 1;
current_radar = mi; current_radar = mi;
g_active_radar = g_selectable[g_selection_cursor].instance; g_active_radar = g_selectable[g_selection_cursor].instance;
g_app_state = AppState::OPERATING; g_app_state = AppState::OPERATING;
reset_radar_controls(mi);
} }
return; return;
} }
@@ -363,17 +415,43 @@ static void key_callback(GLFWwindow* window, int key, int /*scancode*/,
} }
break; break;
/* Max range (all scopes except Chain Home) */ /* Max range — step lower (shorter range / zoom in); Chain Home: no effect */
case GLFW_KEY_O: case GLFW_KEY_O:
if (current_radar != RADAR_CHAIN_HOME) { if (current_radar == RADAR_MARINE_ASCOPE) {
uniform_max_range = std::max(MAX_RANGE_MIN, if (ascope_range_setting > 0) {
uniform_max_range - MAX_RANGE_STEP); ascope_range_setting--;
uniform_max_range = ASCOPE_RANGES[ascope_range_setting];
}
} else if (current_radar == RADAR_MARINE_TRAFFIC) {
if (traffic_control_range_setting > 0) {
traffic_control_range_setting--;
uniform_max_range = MTC_RANGES[traffic_control_range_setting];
}
} else if (current_radar == RADAR_POLICE_BOAT) {
if (police_boat_range_setting > 0) {
police_boat_range_setting--;
uniform_max_range = BOAT_RANGES[police_boat_range_setting];
}
} }
break; break;
/* Max range — step higher (longer range / zoom out); Chain Home: no effect */
case GLFW_KEY_P: case GLFW_KEY_P:
if (current_radar != RADAR_CHAIN_HOME) { if (current_radar == RADAR_MARINE_ASCOPE) {
uniform_max_range = std::min(MAX_RANGE_MAX, if (ascope_range_setting < MARINE_A_SCOPE_NUM_RANGES - 1) {
uniform_max_range + MAX_RANGE_STEP); ascope_range_setting++;
uniform_max_range = ASCOPE_RANGES[ascope_range_setting];
}
} else if (current_radar == RADAR_MARINE_TRAFFIC) {
if (traffic_control_range_setting < PPI_MTC_NUM_RANGES - 1) {
traffic_control_range_setting++;
uniform_max_range = MTC_RANGES[traffic_control_range_setting];
}
} else if (current_radar == RADAR_POLICE_BOAT) {
if (police_boat_range_setting < PPI_BOAT_NUM_RANGES - 1) {
police_boat_range_setting++;
uniform_max_range = BOAT_RANGES[police_boat_range_setting];
}
} }
break; break;
@@ -578,26 +656,33 @@ static void render_radar_list() {
static void render_selection_status_bar() { static void render_selection_status_bar() {
float x = MARGIN; float x = MARGIN;
float y = static_cast<float>(STATUS_BAR_HEIGHT) - MARGIN - LH_MAIN; float y = static_cast<float>(STATUS_BAR_HEIGHT) - MARGIN - LH_MAIN;
float xp;
/* Line 1: "Use 1 / 2 to change selection." */
g_text_renderer.render_text("Use ", x, y, 1.0f, g_text_renderer.render_text("Use ", x, y, 1.0f,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
float xp = x + g_text_renderer.get_text_width("Use ", 1.0f); xp = x + g_text_renderer.get_text_width("Use ", 1.0f);
g_text_renderer.render_text("1", xp, y, 1.0f, g_text_renderer.render_text("1", xp, y, 1.0f,
COL_PINK[0], COL_PINK[1], COL_PINK[2]); COL_PINK[0], COL_PINK[1], COL_PINK[2]);
xp += g_text_renderer.get_text_width("1", 1.0f); xp += g_text_renderer.get_text_width("1", 1.0f);
g_text_renderer.render_text(" / ", xp, y, 1.0f, g_text_renderer.render_text(" / ", xp, y, 1.0f,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
xp += g_text_renderer.get_text_width(" / ", 1.0f); xp += g_text_renderer.get_text_width(" / ", 1.0f);
g_text_renderer.render_text("2", xp, y, 1.0f, g_text_renderer.render_text("2", xp, y, 1.0f,
COL_PINK[0], COL_PINK[1], COL_PINK[2]); COL_PINK[0], COL_PINK[1], COL_PINK[2]);
xp += g_text_renderer.get_text_width("2", 1.0f); xp += g_text_renderer.get_text_width("2", 1.0f);
g_text_renderer.render_text(" to move the selection. Press ", xp, y, 1.0f, g_text_renderer.render_text(" to change selection.", xp, y, 1.0f,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
xp += g_text_renderer.get_text_width(" to move the selection. Press ", 1.0f);
/* Line 2: "Press ENTER to activate." */
y -= LH_MAIN;
g_text_renderer.render_text("Press ", x, y, 1.0f,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
xp = x + g_text_renderer.get_text_width("Press ", 1.0f);
g_text_renderer.render_text("ENTER", xp, y, 1.0f, g_text_renderer.render_text("ENTER", xp, y, 1.0f,
COL_PINK[0], COL_PINK[1], COL_PINK[2]); COL_PINK[0], COL_PINK[1], COL_PINK[2]);
@@ -606,13 +691,16 @@ static void render_selection_status_bar() {
g_text_renderer.render_text(" to activate.", xp, y, 1.0f, g_text_renderer.render_text(" to activate.", xp, y, 1.0f,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
/* Line 3: "Press ESC to exit the exhibit." */
y -= LH_MAIN; y -= LH_MAIN;
g_text_renderer.render_text("Press ", x, y, 1.0f, g_text_renderer.render_text("Press ", x, y, 1.0f,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
xp = x + g_text_renderer.get_text_width("Press ", 1.0f); xp = x + g_text_renderer.get_text_width("Press ", 1.0f);
g_text_renderer.render_text("ESC", xp, y, 1.0f, g_text_renderer.render_text("ESC", xp, y, 1.0f,
COL_PINK[0], COL_PINK[1], COL_PINK[2]); COL_PINK[0], COL_PINK[1], COL_PINK[2]);
xp += g_text_renderer.get_text_width("ESC", 1.0f); xp += g_text_renderer.get_text_width("ESC", 1.0f);
g_text_renderer.render_text(" to exit the exhibit.", xp, y, 1.0f, g_text_renderer.render_text(" to exit the exhibit.", xp, y, 1.0f,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
} }
@@ -827,6 +915,7 @@ int main(int argc, char* argv[]) {
current_radar = g_selectable[0].management_index; current_radar = g_selectable[0].management_index;
g_active_radar = g_selectable[0].instance; g_active_radar = g_selectable[0].instance;
g_app_state = AppState::OPERATING; g_app_state = AppState::OPERATING;
reset_radar_controls(current_radar);
} }
} }