Architecture

This chapter describes XCSoar’s internal code architecture.

Source Organisation

XCSoar’s source code is stored in the src directory. This section tries to give a rough overview where you can find what.

  • util/: generic C++ utilities that do not depend on external libraries, such as data structures, string operations

  • Math/: math data types (fixed-point math, angles) and generic formulas

  • Geo/: geographic data structures and formulas

  • time/: clocks, civil time, and Validity (last-changed timestamps)

  • Atmosphere/: ICAO ISA pressure, density, and indicated airspeed from pitot

  • Radio/: VHF frequency and transponder code/mode values

  • Units/: SI conversion factors and user-unit tables

  • Operation/: cancel/progress for long-running work

  • Formatter/: code that formats internal values to strings

  • Units/: conversion from SI units (“System” units) to configured user units

  • NMEA/: data structures for values parsed from NMEA

  • Profile/: user profiles, loading from and saving to

  • IGC/: support for the IGC file format

  • Logger/: all loggers (NMEA, IGC, flights)

  • thread/: multi-threading support (OS specific)

  • ui/: base library for the graphical user interface

  • Renderer/: various graphical renderers, for map and analysis

  • MapWindow/: the map

  • Form/: modal dialogs and their controls (based on the screen library)

  • Dialogs/: modal dialogs implementations (based on the form library)

  • net/: networking code (OS specific)

  • Operation/: generic code to support cancellable long-running operations

  • Android/: code specific to Android (the native part only; Java code is in android/src/)

  • Engine/PathSolvers/: an implementation of Dijkstra’s path finding algorithm, for task and contest optimisation

  • Engine/Airspace/: airspace data structures and airspace warnings

  • Engine/Waypoint/: waypoint data structures

  • Engine/Observation/: thermal and wave marks, tagged by feed

  • Engine/GlideSolvers/: a MacCready implementation

  • Engine/Task/: task data structures and calculations

  • Engine/Contest/: contest optimisation

  • Engine/Route/: the route planner (airspace and terrain)

  • io/: stream and file I/O (readers, writers, archives). Keep this layer free of UI and backend singletons; device-specific listing belongs in Storage/

  • Repository/: filename patterns and typed data directories (FileType); see XCSoar data directory for the on-disk layout

  • Storage/: removable storage enumeration, hotplug monitors, and the StorageDevice abstraction (platform code in Storage/linux/, Storage/win/, Storage/android/)

  • Device/, Computer/, Blackboard/: sensor drivers, glide computer, and thread-specific data copies

  • Dialogs/DataManagement/: data management UI (import, export, backup, file explorer)

Layer dependencies

Rough dependency direction (see also project rules in .cursor/rules/xcsoar-project-rules.mdc):

  • Foundation (util/, Math/, Geo/, io/, system/, time/, Atmosphere/, Radio/, Units/, Operation/) must not include Engine, Backend, or UI headers.

  • Engine uses Foundation only.

  • Backend (Device/, Computer/, Storage/, NOTAM/, …) uses Foundation and Engine. Access UI only through event queues (InputEvents, UI::Notify), not dialogs.

  • UI (Dialogs/, Form/, Interface.hpp) may use all layers below it. Helpers such as Storage/StorageUtil.cpp that read BackendComponents are backend/UI glue, not Foundation.

Threads and Locking

Threads

XCSoar runs on multiple threads, to make the UI responsive but still allow expensive background calculations.

This is how it looks like on Windows and Linux/SDL (software rendering):

digraph threads {
  graph [fontsize=10];
  node [fontsize=10];
  edge [fontsize=8];

  Devices -> MergeThread [label="sensors"];
  MergeThread -> CalcThread [label="sensors"];
  CalcThread -> UIThread [label="results"];
  IOThread -> UIThread [label="data"];
  UIThread -> DrawThread [dir=both, label="redraw"];
}

The UI thread is the main thread. It starts the other threads and is responsible for the UI event loop. No other thread is allowed to manipulate windows. The UI thread has a timer which does regular house keeping twice per second (ProcessTimer.cpp).

The calculation thread (CalculationThread.cpp, GlideComputer*.cpp) does all the expensive calculations in background. It gets data from the devices (through MergeThread) and forwards it together with calculation results to the drawing thread and the main thread.

Each device has its own thread (SerialPort.cpp). Blocking serial I/O is isolated from the UI thread. The thread is stopped during task declaration (which happens in the UI thread).

When new data arrives on the serial port, the MergeThread gets notified, which will merge all sensor values into one data structure. It will then run cheap calculations, and forwards everything to the CalculationThread.

With OpenGL, the map is rendered live without a buffer. There is no DrawThread.

On Android, the UI thread is not the main thread - the main thread is implemented in Java, managed by Android itself. The UI thread listens for events which the Java part drops into the event queue (NativeView.java and others). The internal GPS does not need a thread, it is implemented with Java callbacks. For Bluetooth I/O, there are two threads implemented in Java (InputThread.java and OutputThread.java, managed by BluetoothHelper.java).

Network thread and background HTTP

In addition to the sensor and UI threads above, XCSoar runs a dedicated asio event-loop thread (io/async/GlobalAsioThread.cpp, started from XCSoar.cpp). It hosts CurlGlobal and runs coroutines injected with Co::InjectTask / Net::AsyncTask.

Typical uses:

  • NOTAM fetches (NOTAM/NOTAMGlue.cpp)

  • EDL tile downloads (Weather/EDL/DownloadGlue.cpp)

  • TIM thermal index, LiveTrack24, SkyLines, and similar clients in NetComponents.hpp

The UI thread (main event loop) must not perform blocking network I/O during flight. Long-lived background work belongs on the network thread; modal downloads tied to one dialog may still use ShowCoDialog on the UI thread.

Thread rules (same as for Interface.hpp elsewhere):

  • Do not call CommonInterface, ActionInterface, or other UI-only APIs from the network thread. InMainThread() checks will fail.

  • Do read any UI state needed for a download on the main thread before starting the coroutine (for example forecast time and isobar for EDL tiles), and pass snapshots into the network work.

  • Do report completion to the UI with UI::Notify (ui/event/Notify.cpp), which queues a callback on the main event loop. Apply overlays, update status labels, and call ActionInterface::SendUIState only from that callback.

NetComponents (NetComponents.hpp) owns long-lived network clients. Each client that uses Net::AsyncTask should implement BeginShutdown() and be stopped from NetComponents::BeginShutdown() before the map and other UI are torn down. The global pointer is cleared later in Startup.cpp (DestroyNetComponents).

Shutdown order (see Startup.cpp; simplified):

  1. NetComponents::BeginShutdown() — cancel coroutines and queued downloads; clear the network clients’ pointer to the observation store

  2. MainWindow::BeginShutdown(); stop merge and calculation threads; join them; deinitialise map and devices

  3. MainWindow::DeinitialiseStorage() — unregister storage UI listeners

  4. StorageManager::StopMonitoring() — stop hotplug; destructor joins the enumeration worker when delete backend_components runs

  5. delete backend_components; clear the map’s observation pointer and delete data_components

  6. DestroyNetComponents(); destroy MainWindow

  7. On process exit, Net::Deinitialise destroys CurlGlobal on the asio thread (DrainCurl in net/http/Init.cpp)

Deferred UI refresh: callbacks such as async terrain load or blackboard updates must not call PageActions::Update or ActionInterface::SendUIState synchronously if that can re-enter layout while InfoBoxes are being created. Use MainWindow::SchedulePageActionsUpdate and ScheduleRefreshInfoBoxes instead (next event-loop iteration). InfoBoxManager skips work until Create() has finished (infoboxes_ready).

Weather overlays: map overlays may combine a blackboard listener (ongoing GPS/time sync, for example Weather/EDL/Glue.cpp) with a download glue in NetComponents (HTTP fetch and cache, for example Weather/EDL/DownloadGlue.cpp). New providers (such as XCTherm) should follow the same split: listener on the UI thread, network I/O on the asio thread, UI updates via UI::Notify.

Origin stores

Waypoints, airspaces, thermals, and waves each live in one list. Every row is tagged with the feed that produced it: a waypoint file, the Thermal Information Map, Cloud, SkyLines, or XCSoar’s own calculation. Replacing a feed deletes only that feed’s rows. A live packet appends or updates one row and leaves the other feeds in place.

The engine types (Engine/Waypoint/Waypoints, Engine/Airspace/Airspaces, Engine/Observation/Store) have no lock and no thread. Waypoint and airspace reloads run on the UI thread while the other threads are suspended (ScopeSuspendAllThreads). Thermals and waves are published while the map is drawing, so Observation/Database holds the mutex. DataComponents owns that database. The network thread may call ReplaceThermals, AppendThermal, and the wave equivalents directly. ReceiveCalculated publishes own-ship rows from the UI thread through the same mutex. The draw thread and the map click list copy a snapshot with CopyThermals / CopyWaves and then work on the copy.

UI::Notify is for windows, status text, and ActionInterface. A new thermal or wave appears on the next map draw.

Traffic (FLARM/List) is a different store. One slot per aircraft id is updated and expired in place. Replacing a whole feed does not fit a single traffic sentence.

Shutdown clears each network client’s store pointer in its BeginShutdown() before that client’s work is cancelled, and clears the map pointer before delete data_components.

Loading configured files

A file on disk is not the in-memory object. Three kinds of source feed the lists in Origin stores.

Map zip. The configured .xcm supplies terrain.jp2 and topology.tpl. It may also contain waypoints.cup or waypoints.xcw, airspace.txt, and airfields.txt. Those members are still read: map waypoints only when no primary or watched waypoint file produced a row; airspace.txt is merged with the configured OpenAir files; airfields.txt is applied as waypoint details. SettingsLeave therefore treats MapFileChanged as a waypoint, airspace, airfield, terrain, and topography change.

Profile path. A Site files entry is one source: the map file, a waypoint or airspace list, FLARMNet, RASP, the checklist, or the input file. Reloading one origin deletes only that origin’s rows.

Live feed. NOTAM, traffic, Cloud, and SkyLines are not files. NOTAM replaces AirspaceOrigin::NOTAM and leaves file airspace in place. Traffic (FLARM/List) keeps one slot per aircraft id, updated and expired in place.

The reload runs on the UI thread. A download callback queues the path and UI::Notify delivers it. Waypoint and airspace edits suspend the other threads (ScopeSuspendAllThreads, or SuspendAllThreads inside SettingsLeave) and bump Serial on the list.

Map zip finished loading. MainWindow::LoadTerrain reads the zip on a worker. OnTerrainLoaded then installs the terrain, fills waypoint elevations that were missing, updates home, and sets airspace ground levels. Topography and the waypoint and airspace files stay as they were.

File Manager, or startup auto-update. ApplyConfiguredDataFiles compares each downloaded path with the Site files profile keys. A path that is not selected is left on disk. A match sets the same flags Setup uses and calls SettingsEnter / SettingsLeave. An updated .xcm still reloads the zip’s waypoint, airspace, and airfield members. An OpenAir update drops file and map airspace rows, rereads them, and clears airspace warnings; NOTAM rows stay. Startup auto-update skips that reload while flying: the new file stays on disk until the next start.

Background file jobs

Not all background work uses the network thread. Local file jobs (tar backup/restore, import/export copies, device enumeration) use other mechanisms:

  • Modal progress on the UI thread: JobDialog runs a Job subclass on a short-lived worker thread (Job/Thread.cpp) while showing ProgressDialog. The UI thread stays responsive; progress is reported through OperationEnvironment.

  • Modal network work: ShowCoDialog runs a coroutine on the asio thread (see above). Do not use JobDialog for HTTP.

  • Fire-and-forget helpers: some UI actions start a detached std::thread for a single task (for example deleting a file on removable media). Keep captured state alive (for example std::shared_ptr<StorageDevice>) and avoid UI calls from that thread.

When modifying shared backend data (waypoints, airspaces) during such jobs, use ScopeSuspendAllThreads from Protection.hpp where appropriate.

Storage and removable media

BackendComponents (BackendComponents.hpp) owns backend singletons including storage_manager (StorageManager). NetComponents is separate and holds long-lived HTTP clients only.

StorageManager (Storage/StorageManager.cpp):

  • Owns the platform hotplug monitor and storage enumerator.

  • Receives topology events (from a platform worker or the UI event loop, depending on the backend).

  • Runs device re-enumeration on a dedicated worker thread so sysfs, Win32, or SAF walks do not block the UI.

  • Invokes a constructor-supplied NotifyCallback (wired in Startup.cpp to MainWindow::SendStorageNotification) so the UI thread calls ProcessPendingChanges() and dispatches StorageEvent notifications to listeners.

UI code registers StorageEventListener instances on the main thread (for example StorageLocationPickerDialog). Use StorageUtil (FindDeviceByName, FormatStorageCaption, EnumerateTarFiles) from UI or backend glue — not from Foundation io/ (stream-only tar create/restore lives in io/TarBackup).

Thread lifetime: when starting a new storage worker, join any finished previous std::thread before move-assigning a new one; otherwise the C++ runtime calls std::terminate(). The destructor joins the worker after StopMonitoring().

Locking

Some data structures are rarely modified. There is no lock for them. For a modifications, all threads must be suspended. Example: waypoints, airspaces.

Other data structures are modified so often that correct locking would be too much overhead. Each thread and each instance has its own copy. The lock needs to be obtained only for making the private copy. The private copy can be used without locking. Example: NMEA_INFO, DERIVED_INFO.

There are objects which are too expensive to copy. Normal locking applies to them. We have a template class called Guard to enforce proper read/write locking. Example: the task.

Accessing Sensor Data

Much of XCSoar deals with obtaining sensor data and visualising it.

Suppose you want to write a dialog that needs the current GPS location, where do you get it? The short and simple answer is: from CommonInterface::Basic() (the InterfaceBlackboard). Example:

#include "Interface.hpp"

...
  const auto &basic = CommonInterface::Basic();
  if (basic.location_available)
    current_location = basic.location;

This is true for the main thread (aka the “user interface thread”). Other threads must not use the Interface.hpp library, because the InterfaceBlackboard is not protected in any way. It contains copies of various data structures just for the main thread.

This is how sensor data moves inside XCSoar:

digraph threads {
  graph [fontsize=10];
  node [fontsize=10];
  edge [fontsize=8];

  Devices [label="Devices"];
  MergeThread [label="MergeThread\nBasicComputer\nDeviceBlackboard"];
  CalcThread [label="CalcThread\nGlideComputer\nGlideComputerBlackboard"];
  UIThread [label="UIThread\nInterfaceBlackboard\nBlackboardListener"];
  DrawThread [label="DrawThread\nMapWindow\nMapWindowBlackboard"];

  Devices -> MergeThread [label="NMEAInfo"];
  MergeThread -> CalcThread [label="MoreData"];
  CalcThread -> UIThread [label="DerivedInfo"];
  UIThread -> DrawThread [label="DerivedInfo"];
}

The device driver parses input received from its device into its own NMEAInfo instance inside DeviceBlackboard (i.e. per_device_data). Then it wakes up the MergeThread to merge the new data into the central NMEAInfo instance. The MergeThread hosts the BasicComputer which attempts to calculate missing data (for example, derives vario from GPS altitude).

The CalculationThread wakes up and receives the MoreData object from DeviceBlackboard. Here, expensive calculations are performed (GlideComputer: task engine, airspace warnings, …), resulting in a DerivedInfo object. The CalculationThread runs no more than twice per second.

Finally, the UI thread wakes up and receives MoreData and DerivedInfo via DeviceBlackboard. This updates InfoBoxes and other UI elements. On Windows, the map is drawn in a separate thread, so there’s another layer.

Let’s get back to the question: where do I get sensor data? That depends on who you are:

  • you are the user interface: (InfoBoxes, dialogs, any Window callback): InterfaceBlackboard (see above). To get notified on changes, register a BlackboardListener (and don’t forget to unregister it).

  • you are the MapWindow: depends! If you’re being called from OnPaintBuffer (i.e. inside the DrawThread), you must use the MapWindowBlackboard, all others must use the InterfaceBlackboard.

  • you are a “computer” library: you will get the values as a parameter. Don’t try to use the GlideComputerBlackboard directly.

  • you are a device driver: implement the method OnSensorUpdate or OnCalculatedUpdate if you need to know values from other devices or calculation results.

  • everybody else may use the DeviceBlackboard, but be sure to lock it while using its data.

Developing

Debugging XCSoar

See also Debugging Tips for replay, simulator, and utility workflows.

The XCSoar source repository contains a module for the GNU debugger (gdb). It contains pretty-printers for various XCSoar types, including Angle, GeoPoint and others. These are helpful when you print values in the debugger. To use it, start the debugging session and load the module:

$ gdb -ex "source tools/gdb.py" output/UNIX/bin/xcsoar
(gdb) run

The module will automatically convert fixed-point to floating point, radian angles to degrees and more. You can now do fancy stuff like:

(gdb) p basic.location
$1 = GeoPoint(7.93911242887 51.1470221074)
(gdb) p basic.date_time_utc
$2 = DateTime(2012/12/23 21:41:57)
(gdb) p basic.track
$3 = 55.2254197961
(gdb) p basic.external_wind
$4 = GeoVector::ZERO
(gdb) p current_leg.vector_remaining
$5 = GeoVector(267.899420345 107957.109724)

User interface guidelines

General

  • Minimise the number of colours, and re-use colour groups already defined.

  • Too much use of colour where it is not required serves only to reduce the effectiveness of bright colours for important items.

  • High colour saturation elements should be reserved for high importance items

  • High contrast against background should be reserved for high importance items

  • Attempt to adopt colours that are intuitive based the function of the item

  • Minimise the clutter where possible — readibility is essential for use in flight

  • Use colours defined in Graphics according to functional name, not their actual colour.

  • Try to maintain consistent use of colours in all uses of that function, such as dialogue graphics as well as map overlays and infoboxes.

  • Text should always be monochrome.

Use aviation conventions or adopt best aviation human factors standards where possible, in particular:

Check for performance with respect to colour blindness. This site has a useful tool that can be used to convert screenshots to how they would look to a person with common color blindness: http://www.etre.com/tools/colourcheck/

For safety purposes, avoid use of elements that may encourage or require the user to stare at the screen continuously.

For safety purposes, avoid user controls that have significant risk of producing unsafe results if misconfigured by the pilot.

General colour conventions

Colour conventions generally in use throughout the program:

  • Red for indicator of warning

  • Orange for indicator of caution

  • Green for positive indicator of safety

  • Blue for neutral indicator of safety

Displayed data

  • Where data is invalid, indicate this by not presenting the data or showing dashes.

  • Present data in user-defined units.

  • Display numerical data with significant digits appropriate to the accuracy of the calculations, or its functional use by the pilot, whichever is lower.

Dialogs and menu buttons

Colors

Colour conventions in use are:

  • Grey for buttons

  • Buttons and other widgets rendered with an evenly shaded border

  • Yellow for clicked items

  • Light blue for the key focused item

  • Medium blue for dialogue title bar

  • Text is black if the item is enabled

  • Text is greyed out (but still visible) if the item is disabled

dialogue types and navigation buttons

There are four types of dialogs in XCSoar, and the navigation buttons for each are different. Navigation buttons are the Close, OK, Cancel and Select buttons.

  • Dialogs that modify and save data when the dialogue closes.

    These shall usually have a Close button (no Cancel) and may have context specific function buttons

  • Dialogs that modify data where Cancel would be important for the user.

    These shall have OK and Cancel buttons. This may include dialogs with children dialogs where hitting Cancel from the parent dialogue cancels all the changes made in the children dialogs

  • Dialogs that have a list of values, one of which can be selected to return to the parent dialogue.

    These shall have Select and Cancel buttons

  • Dialogs that display information that cannot be modified.

    These shall have a Close button

dialogue button placement and size

  • The Close and Cancel buttons will never appear in the same dialogue and are always located in the same place. This location will be:

    For portrait: lower right

    For landscape: lower left

  • The Select button will be accompanied with a Cancel button. The locations will be:

    For portrait: Select in lower left, Cancel in lower right

    For landscape: Cancel in lower left, Select immediately above it

  • Buttons will be 35 (scaled) pixels high

  • Buttons will be flush with the bottom of the screen and with the sides of the screen and against each other (no margins)

  • In portrait, buttons will be 33

  • In landscape, buttons will be 65 to 80 (scaled) pixels wide, as wide as the frame permits. They will generally be a vertical row of buttons flush left of the screen

  • If text won’t fit on a button, the buttons can be made larger consistently for a screen, but this should be the exception because if it must contain that much text consider using a different type of control.

  • Exceptions to all the dialogue concepts above are encouraged, but should be mocked up and reviewed with the development community prior to implementing and possibly documenting in the developers guide.

Usability

  • Minimum size of buttons should be X by Y mm

  • Ensure all dialogs are navigable using cursor keys only

  • Ensure the focussed item is clearly identified. The rectangle of the widget on the canvas may be drawn using the fill_focus method of Canvas.

Screen rectangles

Full screen draws the map to the edge of the glass. Several rectangles are smaller than that window, and they are not the same rectangle. Use the one that matches the element.

A rectangle from MainWindow is in main-window coordinates. MapWindow::GetHudRect() is in map-window coordinates. Intersect map->GetPosition() with a main-window rectangle before placing a child of the main window on the map.

GetClientRect()

The whole window. On Android in full screen this includes the status bar, the display cutout, the home indicator and the rounded corners. On iOS it is that full screen only while full screen mode is on; otherwise it is already the safe area. Use it for pixels that are allowed to run under system UI: the map itself, a screen-filling backdrop (the Fly/Simulator background, the dimming behind the InfoBox arrange cards).

GetSafeAreaRect()

The window minus the status bar, the navigation bar, the display cutout and the rounded corners. On a rectangular display with no system bars it equals GetClientRect(). Android reports these insets only while full screen is on, because outside full screen the surface is already laid out inside them. The reserved bar size is kept even while full screen hides the bars. Use it for anything the pilot must be able to read or press that is not allowed to slide under system UI: dialogue windows (title and buttons included) and the Fly/Simulator controls. Do not inset a dialogue by the shade band as well; it is already below the status bar.

MainWindow::GetInfoBoxAreaRect()

Per edge, the screen border where Stretch InfoBox area is on, and the safe edge where it is off. A visible status bar clears the top stretch, because nothing can be drawn behind that bar. Use it for the InfoBoxes, the menu, and gauge positions that do not avoid the InfoBoxes. Positions that do avoid them, and a page that replaces the map, use the HUD rectangle below.

MainWindow::GetAreaStackRect()

The hole the InfoBoxes leave inside the InfoBox area. When the InfoBoxes are hidden, or before the first layout, it is the InfoBox area. The top and bottom areas (cross section, airspace and NOTAM warnings) are placed in this rectangle. Callers rarely need it directly; it is the input to the HUD rectangle.

MainWindow::GetHudRect()

The area stack minus the top and bottom areas. This is where in-flight map chrome belongs: the compass, the map scale and the title beside it (Simulator, PAN, replay, the page name), the final glide bar, the vario bar, the thermal profile, the GPS status, the flight mode icon, the gesture label, the page indicator, the pan elevation and coordinates, the overlay buttons (menu, QuickMenu, zoom), status messages, and a widget that replaces the map. LayoutMapArea() then stores the same hole on the map as MapWindow::GetHudRect(). Draw map overlays with that function, not with the map’s GetClientRect(): the map is larger than the hole and runs under the InfoBoxes and the system UI.

Two extra margins sit inside the map HUD and are not part of the rectangle itself. top_right_margin is the overlay-button column; the north arrow and the pan readout move left by it, and the margin goes to zero when those buttons are hidden. bottom_margin is the portrait pan menu (and the InfoBox access dialogue).

Edge chrome is laid out once in MapHudLayout (via GlueMapWindow::GetHudLayout()): HUD, text-padding inset (the same gap the overlay buttons leave), optional top-right and bottom clearances, the compass slot height, and the scale/title clearance for the GPS status. Draw code reads those slots; it does not recompose the insets. The compass slot comes from CompassRenderer, and the GPS clearance from GetMapScaleAndTitleClearance(). The gesture label and the page indicator keep their own optical offset from the HUD edge. Projection-space items (crosshair, aircraft, wind at the aircraft) stay off this layout.

Android::ContentRectBelowTopGesture()

The notification-shade swipe band. It is not the safe area: it can extend below the status bar, and it remains when full screen hides the bars. The value is how far the top of the XCSoar view still lies inside that band, or zero when the surface already starts below it. Use it only for content the pilot drags in that band.

The InfoBox arrange overlay fills the client rectangle, so the backdrop still covers the band. Its cards, text and buttons use the safe area, then the one-argument helper: a stretched outer column must not sit in the edge-gesture strip, and only the part of the shade band that still covers the safe area moves the cards. The Sets editor is a dialogue already placed in the safe area; pass the two-argument helper the dialogue’s Y in the view, so a band that ends at the status bar does not move the cards.

Touch interaction

Adopt DOT/FAA/AR-03/67 (EFB) and FAA flight-deck HF (Cardosi and Murphy; NASA 9.3.3.4.7). Do not invent a parallel gesture language.

  • Acknowledge a press within 100 ms (pressed look, and haptic if the user setting allows). That is “touch received”, not a hold.

  • Prefer lift-off (last contact). The action commits when the finger leaves the target. Sliding off cancels. Do not fire a hold action while the finger is still down; the timer only arms the hold.

  • One press has one winner: tap (lift before the hold is armed), drag (movement past slop before the hold is armed), or hold then lift. After a drag has started, a further hold does nothing.

  • Show a busy indicator if input cannot be processed for more than 0.5 s (SAE ARP 4791). That 0.5 s is “the system is occupied”, not a gesture timeout. XCSoar’s hold-to-arm time (InfoBoxArrange::LONG_PRESS, 500 ms) is a deliberate hold, not that busy limit.

  • Hold progress is a one-way fill that follows the control’s shape. Do not pulse, flash, or use warning colours for a normal hold. Progress must be truthful; pilots ignore misleading bars (AR-03/67).

  • Size hit targets with Layout:: for touch and turbulence. Leave a dead band: movement of about Layout::Scale(20) on a touch screen, or Layout::Scale(10) with a mouse, before the hold is armed is a drag or a cancel, not a tap.

  • Cursor keys and a remote stick must reach the same actions as touch. On a selected InfoBox, the arrow keys move to the InfoBox in that screen direction (InfoBoxArrange::FindNeighbour): left is left, right is right, and they do not follow the slot number. Enter does what a short press does. Holding Enter until the hold is armed, then releasing, opens arrange, the same as a long press. In arrange mode that hold opens the InfoBox list. A short Enter there takes or puts down the selected card, and does not also open the list. Escape on the map, with no dialogue, menu, or special page open, selects the first InfoBox. Escape on a selected InfoBox returns to the map. A long press, and the arrange screen, are the same action. E-paper skips hold animation (HasEPaper() / IsSlowCPU()).

Main graphics

Colors

Colour conventions in use, in order of priority, are:

  • Aircraft black and white, for neutrality but clear identification

  • Traffic (FLARM) use alarm green, orange, and red.

  • Lift is vibrant green, sink is copper orange.

  • Aircraft navigation (route, best cruise track) is (ICAO) dark purple-blue

  • Task navigation lines and areas are (ICAO) magenta.

  • Updraft sources and other updraft derived data is sky blue.

(Todo) airspace alert colours

Map culture (topography) and terrain rendering should conform to ICAO Annex 4 where appropriate. Note that some modifications are reasonable for electronic use given that Annex 4 deals with paper charts. Nevertheless, the colour conventions are useful to adopt as they are likely to be intuitive and are designed for aviation use.

Pen styles

  • Map culture should be rendered with a thin pen

  • Thicker pens used for important (e.g. task, navigational, airspace) lines

  • Dashed lines are used to increase perceptual priority

Map overlays

Elements on the map that are not part of the map layer, such as additional informational widgets (final glide bar, wind, north arrow) should be rendered so as to help those elements be visually separated from the map:

  • Generally adopt higher contrast (higher colour saturation or darker shade) than the background map layer elements.

  • For elements covering an area (non line), draw the entire element or a border with a luminosity contrasting pen, of width IBLSCALE(1).

  • Consider whether the widget is required in all flying states and display modes. if it does not serve a direct functional purpose in some states/modes, do not render it.

  • Avoid locating widgets at the aircraft symbol (ownship symbol). It is important to keep this area clear so the aircraft symbol can be easily found.

Elements that may be rendered over each other should be organised in order of priority, particularly with alert warning items above caution items above non-alert items.

Terminology

Glide Ratio

’Glide ratio’ is a non-specific term which can refer to the ratio of horizontal to vertical motion with reference to either the surrounding airmass or the ground.

To reduce confusion, ground-referenced glide ratios (eg distance travelled over ground vs altitude lost) should be referred to by the term ’glide ratio over ground’ when space allows, or ’glide ratio’ / ’GR’.

Air-referenced glide ratios (eg airspeed vs sink rate) should be specified as ’lift/drag ratio’ / ’L/D ratio’ / ’LD’. The lift/drag ratio is numerically equal to the air-referenced glide ratio when flying at constant speed.

If usage spans both air-referenced and ground-referenced glide ratios, the non-specific term ’glide ratio’ / ’GR’ should be used. ’Lift/drag ratio’ should never be used to refer to ground-referenced glide ratios.