Canvas MapStructure
|
| |
| Started in | 11/2013 |
|---|---|
| Description | Charting abstraction layer for Canvas/Nasal maps |
| Maintainer(s) | Philosopher, Hooray, Stuart |
| Contributor(s) | User:Philosopher (since 11/2013), |
| Status | Under active development |
| Topic branches: | |
| fgdata | main fgdata repo |
| The FlightGear forum has a subforum related to: Canvas |
| Canvas - the FlightGear 2D drawing API |
|---|
|
MapStructure is a scripting-space Nasal framework (designed and maintained by Philosopher) for managing layers of symbols in Nasal/Canvas-based mapping displays, which can be used in both aircraft MFDs/instruments and GUI dialogs, like the airport selection or Map dialogs. MapStructure is all about separating the visualization of the map from the visualized data itself, and the way it is shown to, and controlled by, the user. MapStructure is designed as a Model/View/Controller (MVC) framework.
Introduction
A GUI dialog may respond to mouse events (e.g. panning/zooming etc), while a cockpit display would typically respond to cockpit hot-spots (bindings), such as clicking virtual CDU/MCP buttons. Therefore, user input handling must be well encapsulated and handled by so called "delegates", so that the back-end doesn't need to know anything about its front-end. Otherwise, back-ends designed for aircraft would no longer work when used in GUI dialogs and vice versa.
For instance, imagine a piece of code showing navaids within a range that can be set in the cockpit: This code would stop working when used in a different cockpit, or when used in a GUI dialog. Likewise, referencing properties that are specific to a certain GUI dialog, would stop working once the layer is used in a different GUI dialog or in an aircraft.
Conventionally, a simple navaid layer showing NDBs would be implemented as a single piece of code that's running through these steps:
- run a query to find navaids within 50 nm
- get the positions of each navaid
- for each navaid, render a symbol onto a canvas map
Now, once such a piece of code needs to do something else, it would be typically copied/pasted and adapted, e.g. to change the range of the query, the type of symbol, the type of map or even just the type of query (VORs vs NDBs). Now, MapStructure encourages a more modular design, so that each stage is implemented via separate files that can be easily reused, and that can be augmented by adding new files to accomplish a related task.
The next complication is that different front-ends may require different data to be shown - such as taxiways, runways, fixes, waypoints or routing (waypoints). Therefore, the framework works in terms of "layers", where each layer manages its own set of symbols - symbols typically represent geographic positions (latitude/longitude), as well as a drawable (symbol, SVG file name or a callback handling the implementation).
Control of individual layers is delegated to callbacks that can be overridden by the front-end, i.e. to hide/show a layer.
There's basically several drivers at work here:
- one driver determines what is to be drawn (e.g. VORs)
- one driver determines where it is to be drawn (e.g. lat/lon)
- one driver determines how it is to be drawn (e.g. scaled SVG/PNG images)
- another driver determines how the layer responds to events (such as e.g. recurring updates or mouse clicks)
The primary concern here is to ensure that the DRY-principle (don't-repeat-yourself) is not violated, so that maps, layers and symbols can be easily reused without requring any copy&paste- and so that new use-cases can be easily supported by adding custom controllers that interact with maps/layers/symbols as needed (these are typically boilerplate files that are roughly ~30-50 lines of code, of which 5-10 lines may need customizing).
By establishing this separation of concerns, the design is heavily focused on gathering new contributions in a single place ($FG_ROOT/Nasal/canvas/map), rather than having custom-coded solutions in various places - such as GUI dialogs or aircraft, which used to be the predominant practice prior to this framework, and which often meant that useful code was only available for certain purposes.
Thus, to the framework itself it doesn't matter if you need a layered map to be shown on an instrument or in a GUI dialog, or even as part of the scenery (VGDS) or maybe as a livery. By following this approach, we can use a shared back-end to implement layered maps for different needs, without having to duplicate any code, where people usually would use "copy & paste" and customize things afterwards, a very error-prone and tedious process, that doesn't lend itself to long-term maintenance.
Aircraft developers working on airliners or modern biz jets (and the corresponding MFD avionics) will probably want to get in touch with Philosopher and Hooray via the forum/wiki to coordinate things a little. We also appreciate any related feature requests and other constructive feedback. Flexibility is the ultimate design goal of this effort.
At the moment, the primary users of the framework are the 747-400 and the 777-200ER - both make use of the new ND framework, which internally uses the MapStructure framework.
If you just need an ND, you won't need to deal with MapStructure directly, it is all done transparently by the NavDisplay code.
However, if you'd like to create custom charting displays, or GUI dialogs with embedded charts (map dialog, instructor console, ATC or RADAR displays etc), you'll probably want to use the MapStructure framework, because it reduces the amount of specialized Nasal code significantly - typically to ~10-15 lines of configuration code per layer.
To learn more about the motivation of using a MVC design, see Canvas Map API.
Demonstration
To easily show a moving map layer with all AI/MP traffic within a range of 25 nm, only 10-15 lines of code are needed. Paste this into the Nasal Console:
var MapStructure_demo = func() {
var temp = {};
temp.dlg = canvas.Window.new([600, 400], "dialog");
temp.canvas = temp.dlg.createCanvas().setColorBackground(1, 1, 1, 0.5);
temp.root = temp.canvas.createGroup();
var testMap = temp.root.createChild("map");
testMap.setController("Aircraft position");
testMap.setRange(25);
# this will center the map
testMap.setTranslation(
temp.canvas.get("view[0]") / 2,
temp.canvas.get("view[1]") / 2,
);
var r = func(name, vis = true, zindex = nil) {
return caller(0)[0];
};
# TFC, APT and APS are the layer names as per $FG_ROOT/Nasal/canvas/map and the names used in each .lcontroller file
# in this case, it will load the traffic layer (TFC), airports (APT) and render an airplane symbol (APS)
foreach (var type; [r('TFC'), r('APT'), r('APS')]) {
testMap.addLayer(
factory: canvas.SymbolLayer,
type_arg: type.name,
visible: type.vis,
priority: type.zindex,
);
}
}; # MapStructure_demo
MapStructure_demo();As can be seen, using just ~10-15 lines of copied Nasal code, creates a fully working map that shows AI/MP traffic and airports. Next, you can add/replace additional layers, such as: FIX, VOR, NDB etc. For a complete overview, check out Canvas MapStructure Layers.
Using Layers
- See also Canvas MapStructure Layers.
the kind of object that is typically needed by MapStructure is just an geo-referenced position, which is just fancy lingo for anything that has a 3D position (lat,lon,altitude) MapStructure will internally handle all the details to implement each layer efficiently.
The framework works in terms of "symbols" and "layers" where each layer would have symbols, along with controllers for each symbol (i.e. to animate things, to change the color/style etc) - also, each layer can be controlled using a layer-controller, e.g. to change the range for example.
I suggest to look at some of the simpler examples, e.g. the NDB symbol to see how everything is implemented. You can basically load a SVG file or draw your own symbols using OpenVG instructions.
Then, the layer controller will determine where symbols are to be drawn, and do any filtering (range, altitude, mountains, azimuth).
So the result will just return a vector (resizable array) to the layer controller with drawables (symbols).
You will probably want to experiment first with a really simple example to see how everything works.
I suggest to try out the demo/example mentioned in the article.
Ultimately, you can them combine many layers to form a single map, and each layer can have "many" different symbols
Creating new Layers
As you may have noticed, we have significantly grown the MapStructure docs meanwhile - the short-term goal here is that people should be able to come up with their own layers for MapStructure, or at least be able to help port the old files used by map.nas (*.model/*.layer/*.draw) to MapStructure (*.scontroller/*.lcontroller and *.symbol).
Technically, there's not very much involved meanwhile.
| Note
It makes sense to use existing layers as templates, i.e. copy a set of files that is close to what you want to implement - e.g. a RADAR or ATC layer will typically involve processing AI/MP traffic (changing positions) - which is what the TCAS (TFC) layer is already doing - so it's a good idea to use that as a template for your new layer. Using other layers like VOR, NDB or DME would also be possible - but think about what these represent: navaids, with fixed geographic coordinates, while an ATC/RADAR layer would be all about showing live traffic, so it would be less work to reuse and customize a similar layer instead. Then again, using a complex layer to represent a simple thing would also be more customizing work than necessary obviously. You can also borrow things from different layers-for example, the VOR/DME layers contain support for animating symbols and range-selection based display modes. Thus, it's a good idea to spend 5-10 minutes looking through existing files and playing with them, to see where you can borrow code from. |
So, porting a simple layer can now be done within a few minutes. It mostly boils down to:
- finding a similar layer (i.e. for static objects like navaids - just take an existing navaid layer, for dynamic non-static objects (AI/MP traffic), look at the TFC layer instead - e.g. this could be used for porting WXR/storms or to come up with a radar/ATC layer)
- copying a set of similar *.lcontroller/*.scontroller and *.symbol files (you can also combine things from different files, e.g. to reuse animated symbols (altitude arc)
- add either custom drawing routines (Nasal/Canvas) or Inkscape svg files
- write (or port) the corresponding draw() routine (inside the symbol) file
- registering the whole thing in MapStructure.nas to load the new layer files
Copy three files (all in Nasal/canvas/map/):
- *.symbol (drawing/update routine(s))
- *.scontroller (symbol controller)
- *.lcontroller (layer controller)
People interested in developing/maintaining MapStructure layers, will probably want to start with a simple example first.
The most straightforward starting points should be:
- airplaneSymbol
- tower
- parking
- wxr/storms
You may also want to check out MapStructure Debugger.
Internals
Each file represents a virtual class (no explicit hash needed - just use caller(0)[0]).
First, we need to set up class things, so each file "bootstraps" itself.
Class members/local variables:
| name | Description | *.symbol | *.scontroller | *.lcontroller | *.controller | Links |
|---|---|---|---|---|---|---|
| parents = | Set up class inheritance. | [DotSym] | [Symbol.Controller] | [SymbolLayer.Controller] | [Map.Controller] | |
| __self__ = | Current class being generated. | caller(0)[0] | [6] [7] [8] | |||
| name = | Name to use for referencing (via Symbol |
name of file before the extension, or otherwise as appropriate; e.g. "VOR" | ||||
| element_type = | Type of Canvas Element to use; becomes me.element. | "group" or "path" (usually) | N/A | N/A | N/A | VOR FIX |
Adding this instance to MapStructure's dictionaries and make it be the default controller (where "name" is an arbitrary-but unique-handle used to reference it inside of OOP abstraction):
| *.symbol | *.scontroller | *.lcontroller | *.controller |
|---|---|---|---|
| DotSym.makeinstance( name, __self__ );. | Symbol.registry[ name ].df_controller = __self__; | SymbolLayer.Controller.add( name, __self__); | Map.Controller.add( name, __self__); |
| N/A | Symbol.registry[ name ].df_controller = __self__; | N/A, see below regarding the SymbolLayer | Map.df_controller = __self__; |
Also in *.lcontroller, we need to set up the actual MultiSymbolLayer, whose methods are already handled by MapStructure, so we only need to specify a few items:
SymbolLayer.add(name, {
parents: [MultiSymbolLayer],
type: name, # Symbol type, i.e. for Symbol.get( ... )
df_controller: __self__, # controller to use by default -- this one
});| Note There's also a SingleSymbolLayer which has a slightly different API -- no searchCmd(), only a getModel(), see below |
Symbols
Symbols are pretty simple - just stick to the members/methods outlined above: .init() is called when the symbol is created, and may optionally call me.update() to ensure the symbol is immediately ready to go; and .draw() is called each time the symbol is updated. Do not overwrite .update()! It is already handled by MapStructure, so use .draw() instead.
| Note Should we add a runtime/sanity check to ensure that symbol.update is a func and points to MapStructure, i.e. is not overridden ? |
For each object, one is provided with a handle to the model and controller of the symbol:
A "model" object (me.model) is ultimately specified by the one creating the Canvas.Symbol - aka the SymbolLayer.Controller. It is usually a positioned object, though it could be anything, as long as the methods required by MapStructure are provided by the programmer - specifically the latlon()/getPos() helpers.
(In the case of the TFC layer (handling AI/MP traffic), it is a hash wrapping props.Node and geo.Coord).
The position of the symbol is automatically handled by MapStructure by extracting position information from the object via the corresponding .getPos() call and applying it via .setGeoPosition() on me.element, so the programmer only has to worry about drawing it. The model, however, often provides other information relevant to drawing the symbol, like the frequency of a VOR, or how fast it is going.
A "controller" object (me.controller) is typically obtained from the corresponding *.scontroller file (as a default, see Symbol.df_controller), but can be overridden by the creator of the symbol (hopefully it supports the same API to be compatible!). This will handle the rest of information not handled by the model, typically via "query" methods - stuff like whether this symbol is selected in some way (e.g., by radio setting), settings of the parent map (like range), etc.
In both the .init() and .draw() methods, one can use the automatically created canvas element (me.element) to draw the actual symbol. These are usually static images that are procedurally created via the draw() callback, so it's best to set up lazily-rendered-yet-persistent elements that are simply hidden or shown as needed.
If the symbol is really simple, such that it only needs to be drawn once and never updated, then draw it in .init() and don't define .draw() at all.
Once we adopt the built-in caching scheme, some things will change here - specifically such that each variation in styling of a certain symbol is pre-created so that it will later on only need to be referenced as a canvas raster image (via a texture map lookup).
| Note Stlying is currently being prototyped, see the DME.symbol file in the topics/canvas-radar branch for examples. |
To support LOD-handling and symbol-specific styling, we will also need to expose a handful of methods to encourage separation of scale/style-specific things into symbol files, such as the following methods which should ideally not be hard-coded in symbol files, but use hash lookups for customization purposes:
- setColor()
- setColorFill()
- setLineWidth()
- setScale()
- setSize()
- setFont()
- setFontSize()
- setText()
- setImage()
Currently, we have a bunch of symbol files calling directly the corresponding canvas equivalents via method chaining - however, to allow LOD-handling and styling, we must encourage separating these things, so that things can be customized as needed, i.e. by calling the symbol's me.apply_styling() method after the draw/update routine has finished-which will allow aircraft developers to easily use custom fonts/size/colors or symbols - without having to edit -or worse- duplicate the .symbol-file itself
| Note I am wondering if we could use some fancy metaprogramming to compile a draw() callback and ensure that it does NOT use certain Canvas APIs directly ? That would be kinda cool and useful, i.e. we could do a dry test-run to make sure that methods only use certain allowed APIs. One possible method would be overloading the canvas namespace to provide sstubs for each allowed/disallowed API and use call() to call each method and keep track via any disallowed APIs were called by using a counter or even just doing die()-so that people would get an error message along the lines of "please do not use setColor/setColorFill etc inside the draw() callback. Given that MapStructure itself handles positioning via setGeoPosition(me.element), we could even watch out for such mistakes that way |
The SymbolCache
For the time being, the main optimization that helps speed up rendering Canvas/MapStructure-based displays like the NavDisplay, is using caching.
Caching is accomplished by a little helper framework called SymbolCache which sets up an empty Canvas texture for storing required symbols there.
This is where symbols for VORs, DMEs, FIXes and waypoints will be kept.
Internally, each *.symbol file will still contain all the logic required to actually render the corresponding symbol, which may include hard-coded OpenVG drawing commands, but also SVG or raster images.
However, the cache will be dynamically populated according to all the symbols that are required for each layer. This approach proved quite efficient and straightforward so that even the extra500 developers adopted this method on their Avidyne Entegra R9 instrument.
In the meantime, the SymbolCache has been significantly extended to also support styling: in other words, the SymbolCache now even works for *.symbol files supporting styling by being aware of styling-relevant attributes (think width, symbols, colors etc) and will create distinct cache entries for each symbol variant.
Under the hood, this is using some fancy meta-programming that Philosopher came up with last year - but all you need to know as a MapStructure/ND contributor is that styling and caching work in conjunction as long as you follow a few simple rules - which can easily translate into significant frame rate gains when compared to the old method. This section is intended to describe the basic method, as well as provide a few examples/pointers - we're hoping to grow this over time.
First of all, let's consider an existing layer/example already using caching properly: VOR.symbol:
var drawVOR = func(group) {
return group.createChild("path")
.moveTo(-15,0)
.lineTo(-7.5,12.5)
.lineTo(7.5,12.5)
.lineTo(15,0)
.lineTo(7.5,-12.5)
.lineTo(-7.5,-12.5)
.close()
.setStrokeLineWidth(line_width)
.setColor(color);
};This is a function named drawVOR (note, that the name is arbitrary) which accepts a single argument: a canvas group. The function body itself then uses the passed group to create the relevant geometry for the corresponding symbol (in this case a VOR symbol). There are two configurable ("styling") settings:
- line_width
- color
Styling defaults for things like line_width and color are set up in each symbol file - for example, refer to VOR.symbol:
SymbolLayer.get(name).df_style = { # style to use by default
line_width: 3,
range_line_width: 3,
radial_line_width: 3,
range_dash_array: [5, 15, 5, 15, 5],
radial_dash_array: [15, 5, 15, 5, 15],
scale_factor: 1,
active_color: [0, 1, 0],
inactive_color: [0, 0.6, 0.85],
};df_style is a new hash that contains styling defaults for this particular layer.
Otherwise, everything else in the drawVOR() function can be considered to be "hard-coded". When looking at other examples, the only other thing worth keeping in mind here is that Nasal will implicitly return the last expression to the caller absent any explicit return statements - i.e. the group will be returned to the caller (for the sake of clarity, you could also add an explicit return statement, as is done in the snippet above).
However, the code snippet above is just a drawing routine that is still unknown to the system, so that needs to be done is to set up a corresponding cache entry, which can be seen below:
var cache = StyleableCacheable.new(
name: name,
draw_func: drawVOR,
cache: SymbolCache32x32,
draw_mode: SymbolCache.DRAW_CENTERED,
relevant_keys: ["line_width", "color"],
);What is happening here is that a new cache entry supporting styling is set up (refer to MapStructure.nas for details), specifically:
- a new cache is set up using the name specified at the top of the file: VOR
- drawVOR is passed as the drawing function
- the cache texture to be used is SymbolCache32x32 (which is always available, provided by MapStructure itself)
- next, a draw_mode is set up (refer to MapStructure.nas for etails)
And finally, the really cool stuff is happening in the last line: This is where we meet again the two styling-related variables we saw earlier in the actual drawVOR() implementation: there's an argument called relevant_keys which should be a vector of symbols that are styling-related. This can be used by the SymbolCache/MapStructure framework to tell if a styled symbol is already cached or not.
All of these steps may look complicated at first, but the difficult stuff is happening behind the scenes - now, when it comes to actually using/accessing our "style-able cache", the only thing you need to know is how to get a certain pre-cached symbol out of the cache, which is why we'll take another look at VOR.symbol and its draw() implementation:
# determine the color to be used (remember, this is relevant for styling!)
me.style.color = active ? me.style.active_color : me.style.inactive_color;
# look up the correct symbol from the cache and render it into the group as a raster image, applying custom scaling
me.icon_vor = cache.render(me.element, me.style).setScale(me.style.scale_factor);What is happening here is that the cache entry is looked up for the selected style using me.style, the textured quad (sub-texture) is dynamically retrieved from the SymbolCache and rendered into the canvas group specified via me.element - finally, scaling is applied to the raster image.
Another example can be seen in FIX.symbol
There are still several layers where caching+styling isn't widely used yet - however, over time this is the correct method to lighten the canvas workload quite a bit, and doesn't require any C++ level modifications. If you're seeing heavy impact on performance with complex layers, we suggest you explore adding styling and caching support as described above.
Originally, the whole point of the cache was to simplify symbol management - meanwhile, it also supports styling. Under the hood, all it is dealing with is Canvas elements rendered to a single canvas, with each element having look-up coordinates conveniently stored in a hash. Which means that the SymbolCache could also be easily adapted to become a LayerCache at some point.
This may be very useful in order not to re-draw redundant layers: for instance, the compass rose on the ND can be considered "redundant": no matter how many NDs you are displaying - it makes sense to treat the compass rose as a dedicated layer on its own (see APS.* for examples) and then render that into its own texture map - we could easily set up a LayerCache analogous to the existing SymbolCache - transformations (rotating) would then merely be applied to the referenced raster image - all of a sudden, there will be less work for shivaVG (the OpenVG rendering back-end) to do, because the compass rose will rarely -if ever- need to be updated at all.
All variations will be rendered into a layer cache and the COMPASS.symbol file would merely reference the correct sub-texture (e.g. plan/arc), and merely update/rotate the raster image child referenced by the actual ND.
We kinda discussed the technique a few times already - i.e. introducing the concept of an "Overlay"-layer would make sense at some point- typically, this could be shared among multiple instances of a MFD (think ND/PFD) - this would even be more efficient than the existing hard-coded od_gauge based instruments are currently.
| Work in progress This article or section will be worked on in the upcoming hours or days. See history for the latest developments. |
Basically, you should look at Canvas-based MFDs and ask yourself which elements/layers are likely to be identical (or close enough), so that it would make sense to share certain elements (imagine background images, symbols, overlays and so on). Usually, this will reduce the workload for the Canvas system quite significantly, because an otherwise "complex" layer containing -for instance- OpenVG primitives will only ever be drawn/updated once during initialization and then merely referenced by instances of the actual instrument using it. You'll quickly see the merits of using this approach once you imagine rendering many (think 10+) instances of an ND or PFD.
The basics on turning our existing SymbolCache framework into a LayerCache will be covered below:
| Caution This code is still experimental and hasn't been tested yet ... |
# this sets up a new Canvas texture 1024x1024 with 4 locations for cached layers (each 512x512)
var compassLayerCache = canvas.SymbolCache.new(1024, 512);
# FIXME: check what else is required to cache SVG files loaded into a texture map ...
compassLayerCache.add(name: 'compass', callback: func(group) {
canvas.parsesvg(group, "Nasal/canvas/map/Images/boeingND.svg", { 'font-mapper': me.nd_style.font_mapper });
group.getElementById('compass').updateCenter();
});
# TODO: at some point, this could be a foreach loop moving all compass variants
# into the LayerCache as per navdisplay.styles (compass, compassApp, compassMapCtr)
# troubleshooting code to inspect the contents of the LayerCache ...
var window = canvas.Window.new([1024, 1024], "dialog");
window.setCanvas(compassLayerCache.canvas_texture);| Note We should explore what's required in order to support "animated-style-able layers" - for instance, we could register a single callback to animate a texture and avoid redundant updates this way. Equally, this would allow us to register an update callback along with a layer - e.g. for transforming/rotating the compass rose). |
Controllers
With "models" (=what is to be drawn) and "views" (=how it is to be drawn) being typically generic -and thus- shared, controllers are meant to handle all the specific implementation details and behaviors of the corresponding object itself (Symbol, SymbolLayer, Canvas Map, etc.). The idea being that people will normally only need to parametrize an existing controller, or at worst, copy and customize an existing controller file to be able to use existing layers.
For most of the API, the .new() should be optional and return nil, but if resource management is required (listeners or timers), set up listeners/timers during .new(), store them in a member list, and remove them in .del() using removelistener().
Timers should be added using the maketimer API.
All model objects or canvas map objects should be passed as the first argument to controllers' methods (FIXME: need to make sure this holds, see comment: [9]).
Symbol Controller
| Note In FlightGear 3.1+ there's one additional step between init/draw which creates cache entries for symbols during initialization and may only use a cache lookup in draw/update. Obviously, caching makes only sense for symbols that are fairly static - i.e. text labels or animated elements won't typically use caching at all. You could implement a simple animation by toggling between different cache elements though. |
This is very simple: default symbol controllers can just be wrappers for the corresponding SymbolLayer controller, e.g. [10].
There's a simple API, which currently just passes data on to the layer controller.
In general, you will want to make sure that your classes implement the geo.Coord interface (easy to do by inheriting from geo.coord) - otherwise, you will typically need to add special code to handle custom classes, to extract the required values, such as latitude-deg and longitude-deg.
For this, you will want to refer to the getpos method in Symbol.Controller (see MapStructure.nas). This is also where Nasal ghosts are handled (see for example: positioned/Navaid or Fix).
Another example, [11], uses the controller as a wrapper for the model, but this should really be moved to the model object itself.
FIXME: we need wrapper objects for positioned, so that a class can handle higher-level operations (e.g. like .isActive()). Something like collections.UserDict in Python.
Yet another use would be to have the controller manage listeners for updating its symbol, like the SymbolLayer Controller does for the whole layer. This would be useful for, e.g. keeping track of if a certain symbol changes place, or such. Make sure to implement .new() and .del() functions!
FIXME: I don't think that updating a single symbol can be handled currently.
searchCmd: Filtering
The searchCmd() method is responsible for populating a vector with results that are to be drawn by the draw() method in the *.symbol file. Each object will typically be either a NasalPositioned ghost (i.e. a C++ FGPositioned object returned from a positioned query) or a Nasal hash with latitude/longitude fields/methods, or just a geo.Coord object create via geo.Coord.new().
For example, for navaid-based layers, this will normally serve as the back-end for a positionedSearch using a delta of the current result, compared to the previous result. This is done to enable a layer to tell how many results are new/missing, to selectively update those - rather than having to always delete all previous results and add all new ones.
Whenever a new element (object) is added to the vector, the .onAdded() method will be invoked to add a new symbol to list of managed symbols - once an element is removed, the .onRemoved() method will be called to call the symbol's destructor.
Some of the more straightforward examples are to be found in the implementation of navaid layers:
- NDB
- VOR
- DME
- FIX
Here, searchCmd is typically just a one-liner calling an existing positionedSearch API, such as findNavaidsWithinRange(), which always returns a vector of positioned ghosts (which are automatically supported by MapStructure).
| Note For the sake of simplicity, the MapStructure framework exposes a generator function that returns a proper callback for the most common needs as long as the lcontroller itself inherits from NavaidSymbolLayer. Which is why most navaid lcontroller files will typically just invoke make(TYPE_OF_NAVAID) to obtain a proper searchCmd callback. In the future, we'll probably further generalize lcontroller files accordingly, i.e. by coming up with generators and/or base classes for the more common purposes and needs (post 3.2). |
The only thing that's typically done there is to get the query range (1st argument of the API) via a delegate-callback out of the layer controller, so that navaid range can be easily provided by the MapStructure front-end - such as a GUI dialog or a cockpit display like the ND. in a custom, non-navaid layer, the whole query-type thing can be ignored - it's not even used anywhere, it's really only used to "make" a searchCmd() for navaids like vor, ndb, dme etc - but for that to work, you would have to inherit from "NavaidSymbolLayer", whereas you're probably using "MultiSymbolLayer" now - which is why it's not having any effect. It's really only used in a single place. So "query_type" is really just for navaids.
A more sophisticated example is to be found in the traffic layer (TFC) which handles AI/MP traffic and processes the corresponding properties. This is also where you can see a bunch of helper functions used to "filter" results, e.g. based on range. So you could add other filtering heuristics there.
For other more involved examples, see the implementation of the ROUTE (RTE) and WAYPOINT (WPT) layers.
Support for simple animations can be provided by changing size/color of a symbol if required, i.e. to use a different style for "active/close" symbols (e.g. waypoints). Such simple animations can be pre-created by populating a custom SymbolCache with instances of each required variation in style, more complex animations are better manually implemented by customizing the draw() routine inside the .symbol file.
SymbolLayer Controller
For now, VOR.lcontroller simply handles the individual controller operations on a per-layer scale, e.g. looking up if the VOR is a selected frequency via a list of current frequencies copied from property tree.
For MultiSymbolLayers: this is also responsible for the searchCmd (mandatory!), which handles searching for the model objects used by symbols, and returns a current list of models. The models are compared to the previous list, and models are added/removed to match the new list. If custom equality comparison is needed (i.e. outside of id(a) == id(b)), set layer.searcher._equals(a,b) to an appropriate function during construction.
For SingleSymbolLayers: this is responsible for the mandatory getModel() to provide a model object. This is called once and used for the lifetime of the layer, so the object should be dynamic (this works particularly well with a simple property node that supplies position/latitude-deg + position/longitude-deg or latitude-deg + longitude-deg -- this can of course be extended, see [12]). If the returned object has an update() method, this is called before the position is retrieved.
| Note the _equals line adds a new function to the layer's searcher hash - we need to provide a way to check for "equality", i.e. for navaids that could be position and/or the ID (name). For custom/new layers, we need to provide a custom equality check function. What you are doing there is just adding a custom equality check function that always returns "false" (not equal). This is used by MapStructure to "smartly" identify and differentiate between old and new objects, i.e. to reduce workload and improve performance - imagine the "FIX" layer, which may have hundreds of fixes - while flying, a few dozen will be "new" ones, while most others will be "old" - we'll only remove the old ones, and only add new ones. If the custom definition is not provided, you should get an error suggesting that you add a corresponding method so that the underlying logic can check all objects for equality - see the bottom of the MapStructure article for details, or search for "_equals" |
| The other ugly thing about _equals is that it's essentially duplicated by MapStructure here, and is used here to find models inside of the Layer's internal list, while the geo search obviously uses a competing approach. I might get around to fixing that ;). Basically MapStructure's whole flow is: layer searchCmd -> models -> symbols -> canvas drawings.
|
| The main thing to keep in mind here is that many MapStructure layers deal with positioned objects, i.e. objects that have lat/lon/altitude - internally, the system uses a "diff" (delta) method to compare the current result set against the previous result set to tell how many new/removed items are there - so that things can be selectived updated (added/removed), i.e. to only partially redraw/update things for the sake of efficiency.
— Hooray (Sun Aug 10). Re: Live WXRadar MapStructure Layer Development.
(powered by Instant-Cquotes) |
Canvas Map Controller
This has the most interesting jobs: it manages how the whole map is positioned and re-rendered. Example: [13]. It will have to manage its own updating routines, i.e. keeping track of timers/listeners that are hooked to update it. For example, like in the above, one can make a timer object which calls an "update_pos" method, which will reposition the map (via me.map.setPos) and call update on all layers if necessary (via me.map.update()), which will often call positioned searches and thus should be spaced out, e.g. ~4 or more seconds apart. Obviously one should also check other conditions other than time, like difference in position since last query.
Some things to keep in mind when working on Map Controllers:
- we may also want to support optional mouse-panning (see airport-select dialog)
- we may also want to support optional tooltips for layer elements
Styling
For a list of MapStructure files (meanwhile incomplete), see: Canvas MapStructure Layers Once you have found a layer that is not yet style-able, you need to encode style-able attributes using the relevant_keys vector.[1]
some layers are still missing styling-support - which basically means identifying everything that is currently hard-coded but styling related (think colors, fonts, sie, images/artwork) and replacing that with a variable, the same variable should be added to the df_style (default style) of the symbol, and then you need to set up a cache entry specifiying the symbol specific variables (e.g. color and fontsize), so that the styleable-cache can tell if it has a certain variant of a symbol or not map-canvas.xml is a simple example, artix's Airbus style is more sophisticated.[2]
For example, open the VOR.symbol file and see how it sets up a default style hash (df_style) here: https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Nasal/canvas/map/VOR.symbol[3]
In the drawVOR callback it is then using those variables (as per the hash).
var cache = StyleableCacheable.new(
name:name,
draw_func: drawVOR,
cache: SymbolCache32x32,
draw_mode: SymbolCache.DRAW_CENTERED,
relevant_keys: ["line_width", "color"], # supported style-able attributes
);You can find more sophisticated examples in the Airbus style created by Artix, he also had to touch a bunch of MapStructure files to make that work.
Example: Tutorial Layer TUT
This section will cover the main steps for implementing a new layer whose purpose is showing all targets of a selectable tutorial on a Canvas/MapStructure map.
First of all, we need to find an aircraft that comes with a tutorial that contains targets:
cd $FG_ROOT/Aircraft/c172p/Tutorials && grep -nr targets
taxiing.xml:29: <targets> taxiing.xml:55: </targets> taxiing.xml:221: <property>/sim/tutorials/targets/j2/direction-deg</property> taxiing.xml:225: <property>/sim/tutorials/targets/j2/direction-deg</property> taxiing.xml:245: <property>/sim/tutorials/targets/j2/direction-deg</property> taxiing.xml:249: <property>/sim/tutorials/targets/j2/direction-deg</property> taxiing.xml:267: <property>/sim/tutorials/targets/j2/direction-deg</property> taxiing.xml:271: <property>/sim/tutorials/targets/j2/direction-deg</property> taxiing.xml:290: <property>/sim/tutorials/targets/j2/distance-m</property> taxiing.xml:306: <property>/sim/tutorials/targets/j3/direction-deg</property> taxiing.xml:310: <property>/sim/tutorials/targets/j3/direction-deg</property> taxiing.xml:329: <property>/sim/tutorials/targets/j3/distance-m</property> taxiing.xml:342: <property>/sim/tutorials/targets/a1/direction-deg</property> taxiing.xml:346: <property>/sim/tutorials/targets/a1/direction-deg</property> taxiing.xml:356: <property>/sim/tutorials/targets/a1/distance-m</property> taxiing.xml:370: <property>/sim/tutorials/targets/a2/direction-deg</property> taxiing.xml:374: <property>/sim/tutorials/targets/a2/direction-deg</property> taxiing.xml:385: <property>/sim/tutorials/targets/a2/direction-deg</property> taxiing.xml:389: <property>/sim/tutorials/targets/a2/direction-deg</property> taxiing.xml:407: <property>/sim/tutorials/targets/a2/direction-deg</property> taxiing.xml:411: <property>/sim/tutorials/targets/a2/direction-deg</property> taxiing.xml:421: <property>/sim/tutorials/targets/a2/distance-m</property>
So, for testing/development purposes, we will be using the default FlightGear aircraft, i.e. the c172p, because it comes with well-maintained tutorials, and because its Taxiing tutorials contains a number of targets.
For starters, we need to take a look at $FG_ROOT/Nasal/tutorial/tutorial.nas to see how to get a list of tutorials, so we open the corresponding file and search for tutorials and see this:
tutorialN = nil;
foreach (var c; props.globals.getNode("/sim/tutorials").getChildren("tutorial")) {
if (c.getNode("name").getValue() == name) {
tutorialN = c;
break;
}
}
if (tutorialN == nil) {
screen.log.write('Unable to find tutorial "' ~ name ~ '"');
return;
}As can be seen, this requires only a single variable: name - so we can easily turn this into a helper function:
var getTutorialNode = func(name) {
var tutorialN = nil;
foreach (var c; props.globals.getNode("/sim/tutorials").getChildren("tutorial")) {
if (c.getNode("name").getValue() == name) {
tutorialN = c;
return tutorialN;
}
}
if (tutorialN == nil) {
screen.log.write('Unable to find tutorial "' ~ name ~ '"');
return nil;
}
}Next, we need to check tutorials.nas to see how it gets a list of targets for the corresponding tutorial, which can be seen below:
set_targets(tutorialN.getNode("targets"));So, basically, all we need to do is calling tutorialN.getNode("targets"); to get a list of targets for the corresponding tutorial.
Now, let's look up the definition of the set_targets() function, which is processing all targets, to see what we need to do to extract the latitude/longitude for each target:
##
# For each <target><*><longitude-deg|latitude-deg> calculate and update
# /sim/tutorials/targets/*/...
# heading-deg ... absolute heading to target (0 -> North)
# direction-deg ... relative angle to target (0 -> ahead, 90 -> to the right)
# distance-m ... distance in meters
# eta-min ... estimated time of arrival (assuming aircraft flies in
# in current speed towards target)
#
var set_targets = func(node) {
if (node == nil) {
return;
}
var time = time_elapsedN.getValue();
var dest = props.globals.getNode("/sim/tutorials/targets", 1);
var aircraft = geo.aircraft_position();
var hdg = headingN.getValue() + slipN.getValue();
foreach (var t; node.getChildren()) {
var lon = t.getNode("longitude-deg");
var lat = t.getNode("latitude-deg");
if (lon == nil or lat == nil) {
die("target coords undefined");
}
var target = geo.Coord.new().set_latlon(lat.getValue(), lon.getValue());
var dist = aircraft.distance_to(target);
var course = aircraft.course_to(target);
var angle = geo.normdeg(course - hdg);
if (angle >= 180) {
angle -= 360;
}
var d = dest.getChild(t.getName(), t.getIndex(), 1);
d.getNode("heading-deg", 1).setDoubleValue(course);
d.getNode("direction-deg", 1).setDoubleValue(angle);
var distN = d.getNode("distance-m", 1);
var lastdist = distN.getValue();
distN.setDoubleValue(dist);
if (lastdist != nil) {
var speed = (lastdist - dist) / (time - last_step_time) + 0.00001; # m/s
d.getNode("eta-min", 1).setDoubleValue(dist / (speed * 60));
}
}
last_step_time = time;
}The most relevant/interesting part being the foreach loop where all targets are processed:
foreach (var t; node.getChildren()) {
var lon = t.getNode("longitude-deg");
var lat = t.getNode("latitude-deg");
if (lon == nil or lat == nil) {
die("target coords undefined");
}
var target = geo.Coord.new().set_latlon(lat.getValue(), lon.getValue());
# ...
}We can easily turn this into a new helper function:
var getTargets = func(node) {
var results = [];
foreach (var t; node.getChildren()) {
var lon = t.getNode("longitude-deg");
var lat = t.getNode("latitude-deg");
if (lon == nil or lat == nil) {
die("target coords undefined");
}
var target = geo.Coord.new().set_latlon(lat.getValue(), lon.getValue());
append(results,target);
}
return results;
}
Now, let's use the c172p and the Nasal Console to see if our code works as expected:
var getTutorialNode = func(name) {
var tutorialN = nil;
foreach (var c; props.globals.getNode("/sim/tutorials").getChildren("tutorial")) {
if (c.getNode("name").getValue() == name) {
tutorialN = c;
return tutorialN;
}
}
if (tutorialN == nil) {
screen.log.write('Unable to find tutorial "' ~ name ~ '"');
return nil;
}
}
var tutorial = getTutorialNode("Taxiing");
var allTargets = nil;
if (tutorial != nil) {
allTargets = tutorial.getNode("targets");
props.dump(allTargets);
}Once you start fgfs with the c172p, this snippet of code should load the "Taxiing" tutorial (it having a number of targets) and dump all info to the console:
targets {NONE} = nil
targets/j1 {NONE} = nil
targets/j1/longitude-deg {UNSPECIFIED} = -121.81664
targets/j1/latitude-deg {UNSPECIFIED} = 37.6949
targets/j2 {NONE} = nil
targets/j2/longitude-deg {UNSPECIFIED} = -121.82258
targets/j2/latitude-deg {UNSPECIFIED} = 37.6949
targets/j3 {NONE} = nil
targets/j3/longitude-deg {UNSPECIFIED} = -121.8250
targets/j3/latitude-deg {UNSPECIFIED} = 37.69498
targets/a1 {NONE} = nil
targets/a1/longitude-deg {UNSPECIFIED} = -121.8251
targets/a1/latitude-deg {UNSPECIFIED} = 37.694616
targets/a2 {NONE} = nil
targets/a2/longitude-deg {UNSPECIFIED} = -121.8294
targets/a2/latitude-deg {UNSPECIFIED} = 37.69459
Now, let's turn the whole thing into a searchCmd() function that we can us in our MapStructure layer:
var getTutorialNode = func(name) {
var tutorialN = nil;
foreach (var c; props.globals.getNode("/sim/tutorials").getChildren("tutorial")) {
if (c.getNode("name").getValue() == name) {
tutorialN = c;
return tutorialN;
}
}
if (tutorialN == nil) {
screen.log.write('Unable to find tutorial "' ~ name ~ '"');
return nil;
}
}
var getTargets = func(node) {
var results = [];
foreach (var t; node.getChildren()) {
var lon = t.getNode("longitude-deg");
var lat = t.getNode("latitude-deg");
if (lon == nil or lat == nil) {
die("target coords undefined");
}
var target = geo.Coord.new().set_latlon(lat.getValue(), lon.getValue());
append(results,target);
}
return results;
}
var searchCmd = func() {
var tutorial = getTutorialNode("Taxiing");
var allTargets = nil;
if (tutorial != nil) {
allTargets = tutorial.getNode("targets");
return getTargets(allTargets);
}
}
debug.dump(searchCmd());Next, we need to come up with some boilerplate code for creating a new MapStructure layer, and then populate the searchCmd() method using our two helper fnctions
# See: http://wiki.flightgear.org/MapStructure
# Class things:
var name = 'TUT';
var parents = [SymbolLayer.Controller];
var __self__ = caller(0)[0];
SymbolLayer.Controller.add(name, __self__);
SymbolLayer.add(name, {
parents: [MultiSymbolLayer],
type: name, # Symbol type
df_controller: __self__, # controller to use by default -- this one
df_style: {},
});
var new = func(layer) {
var m = {
parents: [__self__],
layer: layer,
map: layer.map,
listeners: [],
query_type:'dme',
};
# TODO: for non-navaid/FGPositioned layers this needs to be customized to compare objects on the layer
# layer.searcher._equals = func(a,b) 0;
return m;
}; # ctor
var del = func() {
foreach (var l; me.listeners) {
removelistener(l);
}
}; # del
var searchCmd = func {
printlog(_MP_dbg_lvl, "Running query: ", me.query_type);
var range = me.map.controller.query_range();
if (range == nil) {
return;
}
return positioned.findWithinRange(range, me.query_type);
}; # searchCmdNow, here's some code to actually test the new layer (contributed by ludomotico [14]
):
var temp = {};
temp.dlg = canvas.Window.new([600, 400], "dialog");
temp.canvas = temp.dlg.createCanvas().setColorBackground(1, 1, 1, 0.5);
temp.root = temp.canvas.createGroup();
var testMap = temp.root.createChild("map");
testMap.setController("Aircraft position");
testMap.setRange(25);
testMap.setTranslation(
temp.canvas.get("view[0]") / 2,
temp.canvas.get("view[1]") / 2,
);
var r = func(name, vis = true, zindex = nil) {
return caller(0)[0];
};
foreach (var type; [r('TUT'), r('APT'), r('APS')]) {
TestMap.addLayer(
factory: canvas.SymbolLayer,
type_arg: type.name,
visible: type.vis,
priority: type.zindex,
);
}To test the new layer, we need to review the taxiing tutorial to check where all those targets are located, specifically see the presets section below:
<presets>
<airport-id>KLVK</airport-id>
<on-ground>1</on-ground>
<runway>12</runway>
<altitude-ft>-9999</altitude-ft>
<latitude-deg>37.6952</latitude-deg>
<longitude-deg>-121.8167</longitude-deg>
<heading-deg>175.0</heading-deg>
<airspeed-kt>0</airspeed-kt>
<glideslope-deg>0</glideslope-deg>
<offset-azimuth-deg>0</offset-azimuth-deg>
<offset-distance-nm>0</offset-distance-nm>
</presets>
This tells us that we need to start fgfs using --airport=KLVK --runway=12
- ↑ Hooray (Oct 27th, 2016). Re: [SOLVED] How to modify symbols in MapStructure layers .
- ↑ Hooray (Oct 27th, 2016). Re: [SOLVED] How to modify symbols in MapStructure layers .
- ↑ Hooray (Oct 27th, 2016). Re: [SOLVED] How to modify symbols in MapStructure layers .
- ↑ Hooray (Oct 27th, 2016). Re: [SOLVED] How to modify symbols in MapStructure layers .





