Property Tree/Explained
Intro
Just about any information you want is available in the property tree. There's an interactive GUI browser built into FlightGear. You could think of it as a virtual filesystem containing the internal state of the sim.
Another way to look at the property manager is as a 'key/value' hash. Any part of the program can lookup a property name (key) and it's associated value. Values can be of type integer, boolean, float, double, and string.
For convenience the property names (keys) are arranged in a way that appears like a hierarchical tree structure. You can traverse the property 'tree' in much the same way as you would traverse the directories in a file system.
The relative merits of this approach have been discussed at great length and with great vigor on this list in the past so I'd rather not replay that again now, but let me summarize.
The upside of this approach is it is *very* convenient and flexible. When you begin dealing with things like aircraft with vastly different types of control inputs, flight model codes with vastly different types of data outputs/inputs, instruments and instrument panels that care about vastly different sets of parameters, the traditional computer science taught approach to building interfaces (at best) becomes extremely cumbersum and (at worst) starts to completely break down.
The downside of this is that now we have tossed out several key principles drilled into our heads starting at day one by our computer science professors. We now have things like 'global' shared state that can change anywhere in the code. (Bye bye abstract data types.) Properties aren't variables so they can't be type checked or even 'spell' checked by the compiler. And people have brought up performance concerns incurred by the extra layers of indirection.
However, from a practical standpoint, we need *some* global state. Much of this computer science theory works wonderfully in simple demo programs, but doesn't always scale well to the practical needs of large complex software projects. Profiling has shown us that at least at this point, the performance of the property manager isn't a problem. We have been bit on occasion by 'spelling' errors that the compiler can no longer flag for us, but those have been pretty straight forward to track down and fix.
There are some definite trade off's that can be intelligently argued either way.
So the conclusion here is that 'practice' seems to have won the battle over 'theory'. And all those evil things we were told would happen to us if we disobeyed our computer science professors ... well as long as we know and understand the dangers, we seem to have been able to minimize, manage, and containt the problems. Life isn't so bad this way and the project hasn't imploded upon itself. The longer we run with the property manager without encountering a violent end to the world, the more confidence we have in our approach. :-)
An abstract View
The Property Tree System in FlightGear is generally spoken, used by pretty much all FlightGear subsystems that are basically tied together by it, in other words: it is the property tree that is the enabling mechanism to "share" important runtime data between different components of FlightGear.
While this doesn't necessarily apply to all internal FlightGear data structures, it does mostly apply to those variables that may need to be modified at runtime, be it for customization purposes or other uses. In addition, it is made very straightforward to publish/expose new variables from C++ space to the property tree. In addition, new property tree variables can be easily created using XML files or Nasal scripts.
But the Property Tree is also used by other FlightGear-related software (such as Atlas), where the Property Tree's purpose is extended to being that of an network-based "Inter Process Communications (IPC)"-mechanism/enabler, so that other software (open source or or not) can easily interface with and "look into" FlightGear, and inspect/modify runtime state variables (in a peek/poke manner).
This makes it for example possible, to basically "remote control" FlightGear, but also to modify environmental settings easily at runtime, from a different process, computer, network or even continent!
Inside FlightGear, code may be triggered by modifying properties using so called "listeners", similar to the signal/slots mechanism supported by QT. Listeners are code routines that are bound to specific properties and which are automatically invoked by FlightGear once a property is modified. Listeners can be implemented as native C/C++ code or Nasal scripts.
# Example Nasal script listening to a switch
setlistener("/some/switch", func {
if (cmdarg().getBoolValue()) {
print("turned on");
} else {
print("turned off");
}
});
Access to the Property Tree is provided via well-defined means, such as C++ APIs, network protocols, XML files and scripting interfaces (via Nasal).
Furthermore, the Property Tree has been designed to directly map to XML files, specifically to "PropertyList-encoded" XML files, these follow a key/value structure that can be directly translated to the hierarchy used in the Property Tree.
In fact, most of the contents of the FlightGear Property Tree that you get to see at runtime is dynamically composed from such PropertyList-encoded XML files, that are directly loaded into the property tree.
Property Tree: Step by Step
A dump space for your data
For starters, it's probably sufficient to imagine the property tree as a very powerful global (process-wide) "dump space" for simple variables, such as numbers and strings (in its current form, the property tree doesn't contain binary blobs such as actual file contents like images, audio files etc), instead properties usually such data as references to plain text file paths.
Key/Value pairs
The property tree stores variables as "key/value" pairs, so that you can easily refer to a variable by using its "key" (such as for example "altitude") and query the property tree for the value that it has stored for this key (variable).
aircraft-dir = '/home/flight-sim/flight-gear-9/data/Aircraft/A380' description = 'Airbus A380-House' longitude-deg = '-122.3576543' altitude-ft = '28.24378768' altitude-agl-ft = '22.46997954'
Hierarchical Storage / where the tree part comes in
To provide support for hierarchically/logically grouped variable storage, the property tree is implemented as a tree-like structure of nested key/value pairs, this can be imagined pretty much like a virtual file system where you have folders (locations) and files (data).
/position/ /position/longitude-deg = '-122.3576835' (double) /position/latitude-deg = '37.61373348' (double) /position/altitude-agl-ft = '22.47111218' (double) /velocities/ /velocities/uBody-fps = '0.01999366548' (double) /velocities/airspeed-kt = '0.01184097074' (double) /velocities/mach = '1.808156878e-05' (double) /velocities/vertical-speed-fps = '-3.065542307e-05' (double) /velocities/groundspeed-kt = '0.01195743728' (double) /engine/ /engine[n]/thrust = 0.9 /cabin/back/light/
Categories for your data
This approach is powerful because it supports the concept of "categories" for data storage, so that you can place -by convention- certain "data" in a specific place (think "drawer"), pretty much like storing work-related stuff in a "work" folder on your computer, or storing your media files (video/audio) in a distinct "media" folder, too.
Which is why we have a category named positions in FlightGear, and another one named velocities, and yet another one named orientation.
So, this system allows for logical groups or "categories" of data items, so that you know where to search for certain data. On the other hand, this is based purely on conventions - there's nothing that enforces things to be placed in a certain location and sometimes developers forget about these conventions, too - which is the reason why you may find data in places that are not really straightforward.
Property Tree Paths
In fact, the file system analogy previously used pretty much also applies to accessing (writing/reading) values: Values (or data items) in the property tree can be referred to via a "path" which acts as the "key" to the data, this path pretty much looks like a conventional file system path, for example something like "/private/settings/foo" could be an imaginary path/key to access a property tree variable stored at that place. So, from a syntactic point of view this is pretty much similar to file system paths, like they are used on Linux/Unix computers (i.e. no drive letters or backslashes are used).
Naming Properties
In addition, some naming restrictions apply as well. In general, you should
- use lower-case, alphanumeric ASCII characters in these paths
- and only use the hyphen to separate words or append a suffix.
- these conventions simplify working with properties and make them more intuitive.
cool: foo-bar not cool: Foo-bar foo_bar fo0Bar
Naming Conventions
To ensure that keys (paths) to variables are not simply named arbitrarily, some naming conventions apply, these can be mostly deduced from existing properties, such as for example appending a suffix to make properties more self-explanatory by indicating what unit the properties is provided in (e.g. "deg", "rad", "hz", "ft", "nm").
Identically named properties
Multiple identically named properties, for example inside an XML file, will map to an array-like representation inside the property tree. Consider for example:
<PropertyList> <foo/> <foo/> <foo/> <foo/> </PropertyList>
When read into the property tree, the four different foo nodes will be converted into nodes named foo[0] - foo[3] respectively.
Subscripts
- "/controls/engines/engine/magneto" is the same property as
- "/controls/engines/engine[0]/magneto" and
- "/controls/engines/engine[0]/magneto[0]"
That is, the subscript [0] is the same as no subscript.
Typing
In general, the property tree is pretty much untyped, that is it can easily work with untyped properties (generally represented as strings) that can be dynamically converted to different representations, such as numbers like int, float, double or boolean values at runtime.
For example from a telnet session or a socket the values returned could be:
gear_up = 1 / bool (gear_up) elevator = -1.0 to +1.0 / long(-0.56) callsign = "AC234" / str(callsign) throttle = 55 / int(throttle)
When creating your own properties, it is generally considered good practice to chose the most suitable data type, as well.
It is important to keep in mind that storing a property without its type (i.e. untyped), will inevitably result in a conversion being performed somewhere to convert the data to a non-string representation.
So, to avoid unnecessary performance penalties it's always a good idea to also define valid types for your properties.
Likewise, using the proper access means to avoid unnecessary conversions, is also important.
Implementation
- I would like to point out that in many situations your code can do the "expensive" string lookup once at init time, cache the pointer to the node, and from then on, just use the cached node pointer to read/write the value. With this technique, there is an extra level of indirection over using a standard variable, but it still is very fast.
- Logically, the property system is a tree of labelled nodes rather than a lookup table. That means that if you get the value of /controls/flight/elevator, the property system starts at the root, looks up the first child named "controls" (in a list, since hashing gives no benefit for short lists), then looks up a child of the "controls" node named "flight", then looks up a child of the "flight" node named "elevator".
- However, to speed things up, each node has an STL map attached for *caching* lookups, so when you lookup a node relative to another node, it will cache the result and get it from the map with a single lookup the next time.
- Since every property value exists in a node, FlightGear subsystems often simply look up the node once and then manipulate it directly, just as Curt described. The node for /controls/flight/elevator will always be the same, even though the actual value changes. In YASim, Andy decided not to bother working through nodes and just goes straight through the lookup in #2, and I cannot see a speed difference against JSBSim. It wouldn't be hard to change YASim, though.
- Note that all of the lookups can be relative to *any* node, so in the future, we can pass each subsystem a reference node at initialization time. For example, instead of running one copy of JSBSim lookup up properties in /, we could run three copies, one with the reference node "/vehicles[0]", one with the reference node "/vehicles[1]", and one with the reference node "/vehicles[2]". As long as the subsystems do relative property references (i.e. "controls/flight/elevator" instead of "/controls/flight/elevator", there should be no conflicts.