Compare commits

..

4 Commits

13 changed files with 847 additions and 175 deletions

View File

@@ -27,19 +27,21 @@ Examples of data and functions for this class and it subordinates:
of the left hand panel. Please note that in the future, this left hand panel may be moved
to a second physical monitor.
3. Text status panel to be located below the radar scope panel. This will display the current
control settings for the controls and the range and bearing and description of
targets that are on or near the bearing and range cursor. The text status panel shall manage
the hit-test. The hit test shall determine which (if any) targets are either directly the same
rand and the same bearing as the cursor or close to it. The closeness shall be determined
by a configuration variable. Since the cursor information as well as the target information
is already in thread 1, this operatoin shall be performed by the CPU as the CPU will have
all information on the target, including the name and description.
3. Text status panel to be located below the left-hand text panel (bottom of the left
side of the screen, NOT below the scope). The scope takes the full right-side height
with no panel beneath it. The status panel will display the current control settings
and the range and bearing and description of targets that are on or near the bearing
and range cursor. The text status panel shall manage the hit-test. The hit test shall
determine which (if any) targets are either directly the same range and the same
bearing as the cursor or close to it. The closeness shall be determined by a
configuration variable. Since the cursor information as well as the target information
is already in thread 1, this operation shall be performed by the CPU as the CPU will
have all information on the target, including the name and description.
Please note that any text (such as the vessel name and registration will not be part
Please note that any text (such as the vessel name and registration) will not be part
of the SSBO target information. It will need to be formed in text blocks that can
be submitted as textures to be inserted into the status text panel below the scope
panel.
be submitted as textures to be inserted into the status text panel at the bottom of
the left side of the screen.
4. Target data. This will handle the target data that is presented by both the simulator
and the raspberry pi data. All that data will be in the format of the
@@ -165,3 +167,41 @@ struct target_data_to_thread_1_structure {
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
steps forward in a circle and the 2 key steps backward and the enter key makes the
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.
======================================================================
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

@@ -148,23 +148,29 @@ of {PROJECT_DIR}/build/radar_simulator.
In the CMakeLists.txt, plese use the name radar_simulator for the add_executable call.
There will be three main areas of the screen. On the right hand side will be the radar
scope.
There will be three screen areas (viewports):
On the left hand side of the screen will be a text description of the scope as well
as the controls of the scope and keyboard keys for each control. This text will be
white while the control labels will be red and the keystrokes will be in pink.
At some point, pending a decision with the museum, we may purchase components to mount the controls on a panel. Until that is done, the controls will be on the keyboard.
1. Left text panel (upper-left): displays the description of the current scope and the
keyboard controls for that scope. Text is white; control labels are red; keystrokes
are pink. This panel occupies the upper portion of the left side of the window.
At some point, pending a decision with the museum, we may purchase components to
mount the controls on a panel. Until that is done, the controls will be on the keyboard.
On the right hand side of the screen, below the scope, would be a text status window, showing current
maximum range, range, and bearing as appropriate to which scope we are using as well as the identification
of the scope that we are using.
2. Status bar (lower-left): sits directly below the left text panel. It shows the current
control values — maximum range, range cursor, bearing cursor, and scope identification
as appropriate to whichever scope is active. This text will be yellow.
Suggest that we use glViewport to define the left-hand text area, the main scope,
and the yellow status bar. This allows you to render the scope with its own coordinate
system while keeping the text fixed. Along with glViewport, use glScissor and glEnable(GL_SCISSOR_TEST)
3. Scope (right side, full height): the radar scope takes the entire right side of the
window from top to bottom. There is no status bar below the scope.
Below the scope will be a text status window. This text will be yellow
Use glViewport, glScissor, and glEnable(GL_SCISSOR_TEST) to define each of the three
areas so that each can render with its own coordinate system.
The left side dimensions are defined in settings.h:
LEFT_PANEL_WIDTH — horizontal width of the left side
STATUS_BAR_HEIGHT — height of the status bar (bottom of left side)
LEFT_TEXT_HEIGHT — height of the text panel (top of left side; = WINDOW_HEIGHT - STATUS_BAR_HEIGHT)
The scope fills the remaining right side at full window height (SCOPE_WIDTH x WINDOW_HEIGHT).
Scopes in the right panel
@@ -221,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;
8. Maximum Range for all scopes except for chain home, which is fixed. This control
will not operate for Chain Home. These maximum
range selections will be defined for each scope; If you try to go beyond the stated
maximum ranges, the control will have no effect; note that the default maximum range would
be the highest for that scope; keyboard o for lower; p for higher uniform is float max_range;
This is a stop control. Each step for for each maximum range for the selected radar.
those maximum ranges are in a table in settings.h. If you try to step beyond the top maximum
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)
keyboard a for lower; s for higher - uniform is float range_cursor;
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
any workings. This is only for testing the operation of the initialization and management.
As you know, you have enabled the controls on each radar enough to show that they are working, although
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
float uniform_radiogoniometer = 0.0f;
// Maximum radar range (scope-dependent units) — keyboard o (lower) / p (higher)
// GLSL uniform: float max_range
// Components: Marine A-scope, PPI scopes (not Chain Home; that is fixed)
float uniform_max_range = 1.0f;
/*
* Maximum radar range in nautical miles (NOT normalized).
* Chain Home: fixed at CHAIN_HOME_MAX_RANGE_NM; o/p keys ignored.
* 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)
// GLSL uniform: float range_cursor

View File

@@ -78,6 +78,14 @@ public:
/* Return the rendered pixel width of text at the given scale. */
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.
* Call this each time glViewport changes before rendering text. */
void set_projection(int w, int h);
@@ -92,5 +100,8 @@ private:
GLuint shader_program_;
};
/* Global instance — shared by all panels and radar stubs. */
/* Global instances — shared by all panels and radar stubs.
* g_text_renderer : FONT_SIZE_NORMAL (body text, controls, status bar)
* g_text_renderer_large: FONT_SIZE_TITLE (panel titles, scope headings) */
extern TextRenderer g_text_renderer;
extern TextRenderer g_text_renderer_large;

View File

@@ -194,9 +194,14 @@ extern float uniform_noise_filter;
// Components: Chain Home A-scope
extern float uniform_radiogoniometer;
// Maximum radar range, all scopes except Chain Home — keyboard o (lower) / p (higher)
// GLSL uniform: float max_range
// Components: Marine A-scope, PPI scopes
/*
* Maximum radar range in nautical miles (NOT normalized).
* 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;
// Range cursor, PPI scopes only — keyboard a (lower) / s (higher)

View File

@@ -86,47 +86,85 @@
* Component: Chain Home scope (1940s, World War 2)
* ============================================================ */
// Fixed maximum range in km; the max_range control has no effect for this scope
#define CHAIN_HOME_MAX_RANGE_KM 200.0f
// Fixed maximum range in nautical miles; the o/p range control has no effect for this scope
#define CHAIN_HOME_MAX_RANGE_NM 200.0f
/* ============================================================
* MARINE A-SCOPE
* Component: 1940s marine A-scope (pre-PPI)
* ============================================================ */
// Selectable maximum range steps in nautical miles (o/p keys cycle through these)
#define MARINE_A_SCOPE_NUM_RANGES 4
// Selectable maximum range steps in nautical miles (o/p keys step through these)
#define MARINE_A_SCOPE_NUM_RANGES 4
#define MARINE_A_SCOPE_RANGE_STEP_1 3.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_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
* Component: PPI scope, marine traffic control station
* ============================================================ */
// 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_2 6.0f
#define PPI_MTC_RANGE_STEP_3 12.0f
#define PPI_MTC_RANGE_STEP_4 24.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
* Component: PPI scope, on-board boat view
* ============================================================ */
// 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_2 3.0f
#define PPI_BOAT_RANGE_STEP_3 6.0f
#define PPI_BOAT_RANGE_STEP_4 12.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
* Component: Thread 1 (main.cpp), 01_classes, 02_text_description
@@ -136,13 +174,28 @@
#define WINDOW_HEIGHT 1080
#define WINDOW_TITLE "RAF / Marine Radar Exhibit"
// Left text panel occupies left side; scope and status bar share the right side
/* Left side is split vertically: text panel on top, status bar on bottom.
* Right side is the scope viewport — it takes the full window height. */
#define LEFT_PANEL_WIDTH 500
#define STATUS_BAR_HEIGHT 250
// Derived dimensions — scope area fills remaining right-side space
#define SCOPE_WIDTH (WINDOW_WIDTH - LEFT_PANEL_WIDTH)
#define SCOPE_HEIGHT (WINDOW_HEIGHT - STATUS_BAR_HEIGHT)
// Derived dimensions
#define SCOPE_WIDTH (WINDOW_WIDTH - LEFT_PANEL_WIDTH)
#define SCOPE_HEIGHT WINDOW_HEIGHT // scope takes full right-side height
#define LEFT_TEXT_HEIGHT (WINDOW_HEIGHT - STATUS_BAR_HEIGHT) // text portion of left panel
// Pixel margin on every side of the scope area (keeps edges visible on the monitor)
#define SCOPE_MARGIN 10
// Available scope area after removing the margin
#define SCOPE_VW (SCOPE_WIDTH - 2 * SCOPE_MARGIN)
#define SCOPE_VH (SCOPE_HEIGHT - 2 * SCOPE_MARGIN)
// Square scope: side length is the smaller of SCOPE_VW and SCOPE_VH (height wins here)
#define SCOPE_SQUARE SCOPE_VH
// Horizontal offset to centre the square inside the SCOPE_VW-wide available area
#define SCOPE_SQUARE_XOFF ((SCOPE_VW - SCOPE_SQUARE) / 2)
// 1 = draw a white debug border box around each screen panel (temporary layout aid)
#define DEBUG_DRAW_WINDOW_BORDERS 1
/* ============================================================
* TEXT RENDERING
@@ -151,10 +204,12 @@
#define FONT_PATH "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
#define FONT_SIZE_NORMAL 18u
#define FONT_SIZE_TITLE 24u
#define FONT_SIZE_TITLE 26u
// Pixels between text baselines at scale 1.0 with FONT_SIZE_NORMAL
#define LINE_HEIGHT 24.0f
// Pixels between text baselines at scale 1.0 with FONT_SIZE_TITLE
#define LINE_HEIGHT_LARGE 34.0f
// Pixel margin from panel edge for text placement
#define TEXT_MARGIN 10.0f
@@ -181,7 +236,7 @@
#define BEARING_CURSOR_STEP 1.0f
#define RANGE_CURSOR_STEP 0.02f
#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
@@ -200,5 +255,4 @@
#define NOISE_FILTER_MAX 1.0f
#define RANGE_CURSOR_MIN 0.0f
#define RANGE_CURSOR_MAX 1.0f
#define MAX_RANGE_MIN 0.1f
#define MAX_RANGE_MAX 1.0f
// Max range limits removed — range is now step-indexed, clamped by table size

View File

@@ -66,13 +66,13 @@ static const float COL_GREEN_LO[3] = { 0.00f, 0.55f, 0.18f };
/* Layout helpers */
/* ------------------------------------------------------------------ */
static const float SCALE_TITLE = 1.25f;
static const float SCALE_NORMAL = 1.00f;
static const float SCALE_SMALL = 0.85f;
static const float MARGIN = static_cast<float>(TEXT_MARGIN);
static const float LH = LINE_HEIGHT; /* line height at scale 1.0 */
static const float LH = LINE_HEIGHT;
static const float LH_LARGE = LINE_HEIGHT_LARGE;
/* Render one line of text and advance y downward by one line. */
/* Render one body-text line and advance y downward. */
static void text_line(const std::string& text, float x, float& y,
float scale,
float cr, float cg, float cb) {
@@ -80,6 +80,21 @@ static void text_line(const std::string& text, float x, float& y,
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. */
static void text_line_large(const std::string& text, float x, float& y,
float cr, float cg, float cb) {
g_text_renderer_large.render_text(text, x, y, 1.0f, cr, cg, cb);
y -= LH_LARGE;
}
/* Render a blank spacer line. */
static void blank_line(float& y, float scale) {
y -= LH * scale * 0.6f;
@@ -120,19 +135,19 @@ static void control_line(const std::string& label,
/* ------------------------------------------------------------------ */
static void render_scope_name(const std::string& radar_name) {
const float scope_w = static_cast<float>(SCOPE_WIDTH);
const float scope_h = static_cast<float>(SCOPE_HEIGHT);
const float scope_w = static_cast<float>(SCOPE_SQUARE);
const float scope_h = static_cast<float>(SCOPE_SQUARE);
float name_w = g_text_renderer.get_text_width(radar_name, 2.0f);
float name_w = g_text_renderer_large.get_text_width(radar_name, 1.0f);
float nx = (scope_w - name_w) / 2.0f;
float ny = scope_h / 2.0f + 20.0f;
g_text_renderer.render_text(radar_name, nx, ny, 2.0f,
COL_GREEN_DIM[0], COL_GREEN_DIM[1], COL_GREEN_DIM[2]);
g_text_renderer_large.render_text(radar_name, nx, ny, 1.0f,
COL_GREEN_DIM[0], COL_GREEN_DIM[1], COL_GREEN_DIM[2]);
std::string sub = "[ stub - initialization test only ]";
float sub_w = g_text_renderer.get_text_width(sub, SCALE_NORMAL);
float sx = (scope_w - sub_w) / 2.0f;
float sy = ny - LH * 2.0f;
float sy = ny - LH_LARGE;
g_text_renderer.render_text(sub, sx, sy, SCALE_NORMAL,
COL_GREEN_LO[0], COL_GREEN_LO[1], COL_GREEN_LO[2]);
}
@@ -144,12 +159,12 @@ static void render_scope_name(const std::string& radar_name) {
static void render_common_status(float x, float& y, float scale) {
char buf[320];
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_stc_sensitivity, uniform_stc_range);
g_text_renderer.render_text(buf, x, y, scale,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
y -= LH * scale;
uniform_stc_sensitivity, uniform_stc_range * uniform_max_range);
float max_w = static_cast<float>(LEFT_PANEL_WIDTH) - 2.0f * MARGIN;
text_line_wrapped(buf, x, y, scale, max_w,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
}
/* ================================================================== */
@@ -157,11 +172,11 @@ static void render_common_status(float x, float& y, float scale) {
/* ================================================================== */
void ChainHomeRadar::render_left_panel() {
float y = static_cast<float>(WINDOW_HEIGHT) - MARGIN - LH * SCALE_TITLE;
float y = static_cast<float>(LEFT_TEXT_HEIGHT) - MARGIN - LH_LARGE;
float x = MARGIN;
text_line("CHAIN HOME RADAR", x, y, SCALE_TITLE,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
text_line_large("CHAIN HOME RADAR", x, y,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
blank_line(y, SCALE_NORMAL);
text_line("Location: Ventnor, Isle of Wight", x, y, SCALE_SMALL,
@@ -214,16 +229,18 @@ void ChainHomeRadar::render_scope() {
void ChainHomeRadar::render_status_bar() {
float x = MARGIN;
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",
x, y, SCALE_NORMAL, COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
text_line_wrapped("CHAIN HOME RADAR (Ventnor, Isle of Wight) | Fixed range: 200 nm",
x, y, SCALE_NORMAL, max_w,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
render_common_status(x, y, SCALE_NORMAL);
char buf[200];
snprintf(buf, sizeof(buf), "Radiogoniometer: %.1f deg", uniform_radiogoniometer);
g_text_renderer.render_text(buf, x, y, SCALE_NORMAL,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
text_line_wrapped(buf, x, y, SCALE_NORMAL, max_w,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
}
/* ================================================================== */
@@ -231,11 +248,11 @@ void ChainHomeRadar::render_status_bar() {
/* ================================================================== */
void MarineAScopeRadar::render_left_panel() {
float y = static_cast<float>(WINDOW_HEIGHT) - MARGIN - LH * SCALE_TITLE;
float y = static_cast<float>(LEFT_TEXT_HEIGHT) - MARGIN - LH_LARGE;
float x = MARGIN;
text_line("MARINE A-SCOPE RADAR", x, y, SCALE_TITLE,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
text_line_large("MARINE A-SCOPE RADAR", x, y,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
blank_line(y, SCALE_NORMAL);
text_line("Location: Community Boating Center,", x, y, SCALE_SMALL,
@@ -284,18 +301,20 @@ void MarineAScopeRadar::render_scope() {
void MarineAScopeRadar::render_status_bar() {
float x = MARGIN;
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)",
x, y, SCALE_NORMAL, COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
text_line_wrapped("MARINE A-SCOPE RADAR (Community Boating Center, Bellingham Bay)",
x, y, SCALE_NORMAL, max_w,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
render_common_status(x, y, SCALE_NORMAL);
char buf[200];
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);
g_text_renderer.render_text(buf, x, y, SCALE_NORMAL,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
text_line_wrapped(buf, x, y, SCALE_NORMAL, max_w,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
}
/* ================================================================== */
@@ -303,11 +322,11 @@ void MarineAScopeRadar::render_status_bar() {
/* ================================================================== */
void MarineTrafficRadar::render_left_panel() {
float y = static_cast<float>(WINDOW_HEIGHT) - MARGIN - LH * SCALE_TITLE;
float y = static_cast<float>(LEFT_TEXT_HEIGHT) - MARGIN - LH_LARGE;
float x = MARGIN;
text_line("MARINE TRAFFIC CONTROL RADAR", x, y, SCALE_TITLE,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
text_line_large("MARINE TRAFFIC CONTROL RADAR", x, y,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
blank_line(y, SCALE_NORMAL);
text_line("Location: Center of Bellingham Bay,", x, y, SCALE_SMALL,
@@ -358,20 +377,22 @@ void MarineTrafficRadar::render_scope() {
void MarineTrafficRadar::render_status_bar() {
float x = MARGIN;
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)",
x, y, SCALE_NORMAL, COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
text_line_wrapped("MARINE TRAFFIC CONTROL RADAR (Bellingham Bay)",
x, y, SCALE_NORMAL, max_w,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
render_common_status(x, y, SCALE_NORMAL);
char buf[280];
snprintf(buf, sizeof(buf),
"Noise Filter: %.2f | Max Range (norm): %.2f | "
"Range Cursor: %.2f | Bearing Cursor: %.1f deg",
"Noise Filter: %.2f | Max Range: %.1f nm | "
"Range Cursor: %.2f nm | Bearing Cursor: %.1f deg",
uniform_noise_filter, uniform_max_range,
uniform_range_cursor, uniform_bearing_cursor);
g_text_renderer.render_text(buf, x, y, SCALE_NORMAL,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
uniform_range_cursor * uniform_max_range, uniform_bearing_cursor);
text_line_wrapped(buf, x, y, SCALE_NORMAL, max_w,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
}
/* ================================================================== */
@@ -379,11 +400,11 @@ void MarineTrafficRadar::render_status_bar() {
/* ================================================================== */
void PoliceboatRadar::render_left_panel() {
float y = static_cast<float>(WINDOW_HEIGHT) - MARGIN - LH * SCALE_TITLE;
float y = static_cast<float>(LEFT_TEXT_HEIGHT) - MARGIN - LH_LARGE;
float x = MARGIN;
text_line("POLICE BOAT RADAR", x, y, SCALE_TITLE,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
text_line_large("POLICE BOAT RADAR", x, y,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
blank_line(y, SCALE_NORMAL);
text_line("Starting location: Taylor Dock /", x, y, SCALE_SMALL,
@@ -436,18 +457,20 @@ void PoliceboatRadar::render_scope() {
void PoliceboatRadar::render_status_bar() {
float x = MARGIN;
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)",
x, y, SCALE_NORMAL, COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
text_line_wrapped("POLICE BOAT RADAR (Bellingham Bay - moving)",
x, y, SCALE_NORMAL, max_w,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
render_common_status(x, y, SCALE_NORMAL);
char buf[280];
snprintf(buf, sizeof(buf),
"Noise Filter: %.2f | Max Range (norm): %.2f | "
"Range Cursor: %.2f | Bearing Cursor: %.1f deg",
"Noise Filter: %.2f | Max Range: %.1f nm | "
"Range Cursor: %.2f nm | Bearing Cursor: %.1f deg",
uniform_noise_filter, uniform_max_range,
uniform_range_cursor, uniform_bearing_cursor);
g_text_renderer.render_text(buf, x, y, SCALE_NORMAL,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
uniform_range_cursor * uniform_max_range, uniform_bearing_cursor);
text_line_wrapped(buf, x, y, SCALE_NORMAL, max_w,
COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]);
}

View File

@@ -42,6 +42,7 @@
/* Global singleton used by all components. */
TextRenderer g_text_renderer;
TextRenderer g_text_renderer_large;
/* ------------------------------------------------------------------ */
/* Internal helpers */
@@ -246,6 +247,38 @@ float TextRenderer::get_text_width(const std::string& text, float scale) const {
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) {
/* Column-major orthographic matrix mapping pixel space [0,w]x[0,h]
* to NDC. Near=-1, far=1 (depth unused for 2D text). */

View File

@@ -57,6 +57,147 @@
#include <chrono>
#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) */
/* ------------------------------------------------------------------ */
#if DEBUG_DRAW_WINDOW_BORDERS
static const char* LINE_VERT_SRC =
"#version 430 core\n"
"layout(location = 0) in vec2 pos;\n"
"uniform mat4 projection;\n"
"void main() {\n"
" gl_Position = projection * vec4(pos, 0.0, 1.0);\n"
"}\n";
static const char* LINE_FRAG_SRC =
"#version 430 core\n"
"uniform vec3 line_color;\n"
"out vec4 frag_color;\n"
"void main() {\n"
" frag_color = vec4(line_color, 1.0);\n"
"}\n";
static GLuint g_line_vao = 0;
static GLuint g_line_vbo = 0;
static GLuint g_line_prog = 0;
static bool init_line_renderer() {
GLint ok = 0;
char log_buf[512];
GLuint vert = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vert, 1, &LINE_VERT_SRC, nullptr);
glCompileShader(vert);
glGetShaderiv(vert, GL_COMPILE_STATUS, &ok);
if (!ok) {
glGetShaderInfoLog(vert, 512, nullptr, log_buf);
fprintf(stderr, "[main] border vert shader error:\n%s\n", log_buf);
glDeleteShader(vert);
return false;
}
GLuint frag = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(frag, 1, &LINE_FRAG_SRC, nullptr);
glCompileShader(frag);
glGetShaderiv(frag, GL_COMPILE_STATUS, &ok);
if (!ok) {
glGetShaderInfoLog(frag, 512, nullptr, log_buf);
fprintf(stderr, "[main] border frag shader error:\n%s\n", log_buf);
glDeleteShader(vert);
glDeleteShader(frag);
return false;
}
g_line_prog = glCreateProgram();
glAttachShader(g_line_prog, vert);
glAttachShader(g_line_prog, frag);
glLinkProgram(g_line_prog);
glGetProgramiv(g_line_prog, GL_LINK_STATUS, &ok);
if (!ok) {
glGetProgramInfoLog(g_line_prog, 512, nullptr, log_buf);
fprintf(stderr, "[main] border shader link error:\n%s\n", log_buf);
glDeleteShader(vert);
glDeleteShader(frag);
return false;
}
glDeleteShader(vert);
glDeleteShader(frag);
glGenVertexArrays(1, &g_line_vao);
glGenBuffers(1, &g_line_vbo);
glBindVertexArray(g_line_vao);
glBindBuffer(GL_ARRAY_BUFFER, g_line_vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 8, nullptr, GL_DYNAMIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
fprintf(stdout, "[main] debug border renderer initialised\n");
return true;
}
/* Draw a white line-loop rectangle inset 3 px from the edges of the
* current viewport (w x h). Call after setting glViewport/glScissor. */
static void draw_border_box(int w, int h) {
const float fw = static_cast<float>(w);
const float fh = static_cast<float>(h);
const float pad = 3.0f;
float verts[8] = {
pad, pad,
fw - pad, pad,
fw - pad, fh - pad,
pad, fh - pad
};
/* Same orthographic pixel-space projection as TextRenderer::set_projection. */
float proj[16] = {
2.0f / fw, 0.0f, 0.0f, 0.0f,
0.0f, 2.0f / fh, 0.0f, 0.0f,
0.0f, 0.0f, -1.0f, 0.0f,
-1.0f, -1.0f, 0.0f, 1.0f
};
glUseProgram(g_line_prog);
glUniformMatrix4fv(glGetUniformLocation(g_line_prog, "projection"),
1, GL_FALSE, proj);
glUniform3f(glGetUniformLocation(g_line_prog, "line_color"),
1.0f, 1.0f, 1.0f);
glBindVertexArray(g_line_vao);
glBindBuffer(GL_ARRAY_BUFFER, g_line_vbo);
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(verts), verts);
glDrawArrays(GL_LINE_LOOP, 0, 4);
glBindVertexArray(0);
glUseProgram(0);
}
#endif /* DEBUG_DRAW_WINDOW_BORDERS */
/* ------------------------------------------------------------------ */
/* Application state */
/* ------------------------------------------------------------------ */
@@ -93,6 +234,7 @@ static const float COL_PINK[3] = { 1.00f, 0.60f, 0.80f };
static const float COL_RED[3] = { 1.00f, 0.25f, 0.25f };
static const float LH_MAIN = LINE_HEIGHT;
static const float LH_LARGE = LINE_HEIGHT_LARGE;
static const float MARGIN = static_cast<float>(TEXT_MARGIN);
/* ------------------------------------------------------------------ */
@@ -121,6 +263,36 @@ static void GLAPIENTRY gl_debug_callback(GLenum /*source*/,
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) */
/* ------------------------------------------------------------------ */
@@ -146,13 +318,14 @@ static void key_callback(GLFWwindow* window, int key, int /*scancode*/,
(g_selection_cursor - 1 + static_cast<int>(g_selectable.size()))
% static_cast<int>(g_selectable.size());
} 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;
for (int i = 0; i < RADAR_COUNT; i++) radar_management[i].selected = 0;
radar_management[mi].selected = 1;
current_radar = mi;
g_active_radar = g_selectable[g_selection_cursor].instance;
g_app_state = AppState::OPERATING;
reset_radar_controls(mi);
}
return;
}
@@ -242,17 +415,43 @@ static void key_callback(GLFWwindow* window, int key, int /*scancode*/,
}
break;
/* Max range (all scopes except Chain Home) */
/* Max range — step lower (shorter range / zoom in); Chain Home: no effect */
case GLFW_KEY_O:
if (current_radar != RADAR_CHAIN_HOME) {
uniform_max_range = std::max(MAX_RANGE_MIN,
uniform_max_range - MAX_RANGE_STEP);
if (current_radar == RADAR_MARINE_ASCOPE) {
if (ascope_range_setting > 0) {
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;
/* Max range — step higher (longer range / zoom out); Chain Home: no effect */
case GLFW_KEY_P:
if (current_radar != RADAR_CHAIN_HOME) {
uniform_max_range = std::min(MAX_RANGE_MAX,
uniform_max_range + MAX_RANGE_STEP);
if (current_radar == RADAR_MARINE_ASCOPE) {
if (ascope_range_setting < MARINE_A_SCOPE_NUM_RANGES - 1) {
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;
@@ -372,12 +571,12 @@ static void parse_args(int argc, char* argv[]) {
/* ------------------------------------------------------------------ */
static void render_intro_left_panel() {
float y = static_cast<float>(WINDOW_HEIGHT) - MARGIN - LH_MAIN * 1.25f;
float y = static_cast<float>(LEFT_TEXT_HEIGHT) - MARGIN - LH_LARGE;
float x = MARGIN;
g_text_renderer.render_text("RAF / MARINE RADAR EXHIBIT", x, y, 1.25f,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
y -= LH_MAIN * 1.25f;
g_text_renderer_large.render_text("RAF / MARINE RADAR EXHIBIT", x, y, 1.0f,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
y -= LH_LARGE;
y -= LH_MAIN * 0.5f;
@@ -409,17 +608,17 @@ static void render_intro_left_panel() {
}
static void render_radar_list() {
float scope_w = static_cast<float>(SCOPE_WIDTH);
float scope_h = static_cast<float>(SCOPE_HEIGHT);
float scope_w = static_cast<float>(SCOPE_SQUARE);
float scope_h = static_cast<float>(SCOPE_SQUARE);
/* Title */
std::string title = "SELECT A RADAR";
float tw = g_text_renderer.get_text_width(title, 1.5f);
g_text_renderer.render_text(title,
(scope_w - tw) / 2.0f,
scope_h - 60.0f,
1.5f,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
float tw = g_text_renderer_large.get_text_width(title, 1.0f);
g_text_renderer_large.render_text(title,
(scope_w - tw) / 2.0f,
scope_h - 60.0f,
1.0f,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
if (g_selectable.empty()) {
std::string msg = "No radars available. Restart with radar names as arguments.";
@@ -440,44 +639,50 @@ static void render_radar_list() {
std::string entry = g_selectable[i].display_name;
bool highlighted = (i == g_selection_cursor);
float scale = highlighted ? 1.3f : 1.0f;
const float* col = highlighted ? COL_GREEN_HI : COL_GREEN_DIM;
std::string prefix = highlighted ? "> " : " ";
std::string line = prefix + entry;
float lw = g_text_renderer.get_text_width(line, scale);
TextRenderer& tr = highlighted ? g_text_renderer_large : g_text_renderer;
float lw = tr.get_text_width(line, 1.0f);
float lx = (scope_w - lw) / 2.0f;
float ly = list_y_start - i * LH_MAIN * 1.4f;
g_text_renderer.render_text(line, lx, ly, scale,
col[0], col[1], col[2]);
tr.render_text(line, lx, ly, 1.0f, col[0], col[1], col[2]);
}
}
static void render_selection_status_bar() {
float x = MARGIN;
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,
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,
COL_PINK[0], COL_PINK[1], COL_PINK[2]);
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]);
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,
COL_PINK[0], COL_PINK[1], COL_PINK[2]);
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]);
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,
COL_PINK[0], COL_PINK[1], COL_PINK[2]);
@@ -486,13 +691,16 @@ static void render_selection_status_bar() {
g_text_renderer.render_text(" to activate.", xp, y, 1.0f,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
/* Line 3: "Press ESC to exit the exhibit." */
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("ESC", xp, y, 1.0f,
COL_PINK[0], COL_PINK[1], COL_PINK[2]);
xp += g_text_renderer.get_text_width("ESC", 1.0f);
g_text_renderer.render_text(" to exit the exhibit.", xp, y, 1.0f,
COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]);
}
@@ -502,69 +710,89 @@ static void render_selection_status_bar() {
/* ------------------------------------------------------------------ */
static void render_selection_screen() {
int scope_x = LEFT_PANEL_WIDTH;
int scope_y = STATUS_BAR_HEIGHT;
int scope_w = SCOPE_WIDTH;
int scope_h = SCOPE_HEIGHT;
/* Left panel */
glViewport(0, 0, LEFT_PANEL_WIDTH, WINDOW_HEIGHT);
glScissor(0, 0, LEFT_PANEL_WIDTH, WINDOW_HEIGHT);
glEnable(GL_SCISSOR_TEST);
/* Left text panel — upper portion of the left side */
glViewport(0, STATUS_BAR_HEIGHT, LEFT_PANEL_WIDTH, LEFT_TEXT_HEIGHT);
glScissor(0, STATUS_BAR_HEIGHT, LEFT_PANEL_WIDTH, LEFT_TEXT_HEIGHT);
glClearColor(0.04f, 0.04f, 0.04f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
g_text_renderer.set_projection(LEFT_PANEL_WIDTH, WINDOW_HEIGHT);
g_text_renderer.set_projection(LEFT_PANEL_WIDTH, LEFT_TEXT_HEIGHT);
g_text_renderer_large.set_projection(LEFT_PANEL_WIDTH, LEFT_TEXT_HEIGHT);
render_intro_left_panel();
#if DEBUG_DRAW_WINDOW_BORDERS
draw_border_box(LEFT_PANEL_WIDTH, LEFT_TEXT_HEIGHT);
#endif
/* Scope area — radar list */
glViewport(scope_x, scope_y, scope_w, scope_h);
glScissor(scope_x, scope_y, scope_w, scope_h);
/* Left status bar — lower portion of the left side */
glViewport(0, 0, LEFT_PANEL_WIDTH, STATUS_BAR_HEIGHT);
glScissor(0, 0, LEFT_PANEL_WIDTH, STATUS_BAR_HEIGHT);
glClearColor(0.04f, 0.04f, 0.04f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
g_text_renderer.set_projection(LEFT_PANEL_WIDTH, STATUS_BAR_HEIGHT);
g_text_renderer_large.set_projection(LEFT_PANEL_WIDTH, STATUS_BAR_HEIGHT);
render_selection_status_bar();
#if DEBUG_DRAW_WINDOW_BORDERS
draw_border_box(LEFT_PANEL_WIDTH, STATUS_BAR_HEIGHT);
#endif
/* Scope — square, centred in the right-side area */
glViewport(LEFT_PANEL_WIDTH + SCOPE_MARGIN + SCOPE_SQUARE_XOFF, SCOPE_MARGIN,
SCOPE_SQUARE, SCOPE_SQUARE);
glScissor(LEFT_PANEL_WIDTH + SCOPE_MARGIN + SCOPE_SQUARE_XOFF, SCOPE_MARGIN,
SCOPE_SQUARE, SCOPE_SQUARE);
glClearColor(0.0f, 0.05f, 0.01f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
g_text_renderer.set_projection(scope_w, scope_h);
g_text_renderer.set_projection(SCOPE_SQUARE, SCOPE_SQUARE);
g_text_renderer_large.set_projection(SCOPE_SQUARE, SCOPE_SQUARE);
render_radar_list();
/* Status bar */
glViewport(scope_x, 0, scope_w, STATUS_BAR_HEIGHT);
glScissor(scope_x, 0, scope_w, STATUS_BAR_HEIGHT);
glClearColor(0.04f, 0.04f, 0.04f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
g_text_renderer.set_projection(scope_w, STATUS_BAR_HEIGHT);
render_selection_status_bar();
#if DEBUG_DRAW_WINDOW_BORDERS
draw_border_box(SCOPE_SQUARE, SCOPE_SQUARE);
#endif
glDisable(GL_SCISSOR_TEST);
}
static void render_operating_screen() {
int scope_x = LEFT_PANEL_WIDTH;
int scope_y = STATUS_BAR_HEIGHT;
int scope_w = SCOPE_WIDTH;
int scope_h = SCOPE_HEIGHT;
/* Left panel — radar description and controls */
glViewport(0, 0, LEFT_PANEL_WIDTH, WINDOW_HEIGHT);
glScissor(0, 0, LEFT_PANEL_WIDTH, WINDOW_HEIGHT);
glEnable(GL_SCISSOR_TEST);
/* Left text panel — upper portion of the left side */
glViewport(0, STATUS_BAR_HEIGHT, LEFT_PANEL_WIDTH, LEFT_TEXT_HEIGHT);
glScissor(0, STATUS_BAR_HEIGHT, LEFT_PANEL_WIDTH, LEFT_TEXT_HEIGHT);
glClearColor(0.04f, 0.04f, 0.04f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
g_text_renderer.set_projection(LEFT_PANEL_WIDTH, WINDOW_HEIGHT);
g_text_renderer.set_projection(LEFT_PANEL_WIDTH, LEFT_TEXT_HEIGHT);
g_text_renderer_large.set_projection(LEFT_PANEL_WIDTH, LEFT_TEXT_HEIGHT);
g_active_radar->render_left_panel();
#if DEBUG_DRAW_WINDOW_BORDERS
draw_border_box(LEFT_PANEL_WIDTH, LEFT_TEXT_HEIGHT);
#endif
/* Scope — radar name (stub) */
glViewport(scope_x, scope_y, scope_w, scope_h);
glScissor(scope_x, scope_y, scope_w, scope_h);
/* Left status bar — lower portion of the left side */
glViewport(0, 0, LEFT_PANEL_WIDTH, STATUS_BAR_HEIGHT);
glScissor(0, 0, LEFT_PANEL_WIDTH, STATUS_BAR_HEIGHT);
glClearColor(0.04f, 0.04f, 0.04f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
g_text_renderer.set_projection(LEFT_PANEL_WIDTH, STATUS_BAR_HEIGHT);
g_text_renderer_large.set_projection(LEFT_PANEL_WIDTH, STATUS_BAR_HEIGHT);
g_active_radar->render_status_bar();
#if DEBUG_DRAW_WINDOW_BORDERS
draw_border_box(LEFT_PANEL_WIDTH, STATUS_BAR_HEIGHT);
#endif
/* Scope — square, centred in the right-side area */
glViewport(LEFT_PANEL_WIDTH + SCOPE_MARGIN + SCOPE_SQUARE_XOFF, SCOPE_MARGIN,
SCOPE_SQUARE, SCOPE_SQUARE);
glScissor(LEFT_PANEL_WIDTH + SCOPE_MARGIN + SCOPE_SQUARE_XOFF, SCOPE_MARGIN,
SCOPE_SQUARE, SCOPE_SQUARE);
glClearColor(0.0f, 0.05f, 0.01f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
g_text_renderer.set_projection(scope_w, scope_h);
g_text_renderer.set_projection(SCOPE_SQUARE, SCOPE_SQUARE);
g_text_renderer_large.set_projection(SCOPE_SQUARE, SCOPE_SQUARE);
g_active_radar->render_scope();
/* Status bar — current control values */
glViewport(scope_x, 0, scope_w, STATUS_BAR_HEIGHT);
glScissor(scope_x, 0, scope_w, STATUS_BAR_HEIGHT);
glClearColor(0.04f, 0.04f, 0.04f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
g_text_renderer.set_projection(scope_w, STATUS_BAR_HEIGHT);
g_active_radar->render_status_bar();
#if DEBUG_DRAW_WINDOW_BORDERS
draw_border_box(SCOPE_SQUARE, SCOPE_SQUARE);
#endif
glDisable(GL_SCISSOR_TEST);
}
@@ -627,7 +855,14 @@ int main(int argc, char* argv[]) {
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
/* 6. Load text renderer shaders and font. */
/* 6a. Initialise debug border renderer (guarded by DEBUG_DRAW_WINDOW_BORDERS). */
#if DEBUG_DRAW_WINDOW_BORDERS
if (!init_line_renderer()) {
fprintf(stderr, "[main] debug border renderer init failed (non-fatal)\n");
}
#endif
/* 6. Load text renderer shaders and fonts. */
if (!g_text_renderer.initialize_shaders()) {
fprintf(stderr, "[main] text renderer shader init failed\n");
glfwTerminate();
@@ -638,6 +873,16 @@ int main(int argc, char* argv[]) {
glfwTerminate();
return 1;
}
if (!g_text_renderer_large.initialize_shaders()) {
fprintf(stderr, "[main] large text renderer shader init failed\n");
glfwTerminate();
return 1;
}
if (!g_text_renderer_large.initialize_font(FONT_PATH, FONT_SIZE_TITLE)) {
fprintf(stderr, "[main] large font load failed: " FONT_PATH "\n");
glfwTerminate();
return 1;
}
/* 7. Register keyboard callback. */
glfwSetKeyCallback(window, key_callback);
@@ -670,6 +915,7 @@ int main(int argc, char* argv[]) {
current_radar = g_selectable[0].management_index;
g_active_radar = g_selectable[0].instance;
g_app_state = AppState::OPERATING;
reset_radar_controls(current_radar);
}
}
@@ -704,6 +950,11 @@ int main(int argc, char* argv[]) {
/* 10. Cleanup. */
g_text_renderer.cleanup();
#if DEBUG_DRAW_WINDOW_BORDERS
if (g_line_vao) { glDeleteVertexArrays(1, &g_line_vao); g_line_vao = 0; }
if (g_line_vbo) { glDeleteBuffers(1, &g_line_vbo); g_line_vbo = 0; }
if (g_line_prog) { glDeleteProgram(g_line_prog); g_line_prog = 0; }
#endif
glfwDestroyWindow(window);
glfwTerminate();