Canvas MapStructure

From FlightGear wiki
(Redirected from MapStructure)
Jump to navigation Jump to search
MapStructure
MapStructure-GRID-layer.png
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 as of 11/2013
Topic branches:
fgdata main fgdata repo


F-JJTH's GPSMap196 in the process of being ported to Canvas using Philosopher's MapStructure framework for mapping (LOD/scaling not applied here, but available) - see [1] This is a link to the FlightGear forum.
MapStructure layers integrated in GPSMap196 with some custom styling applied
Canvas-based VSD prototype (Vertical Situation Display) developed by Omega95
MapStructure WXR (weather) layer created by omega95's using a web service API to fetch live online imagery [2] This is a link to the FlightGear forum.


Targeted FlightGear versions: 3.20+

Canvasready.png

Note  This article is intended for people wanting to add custom scripted mapping/charting displays to FlightGear (aircraft, GUI dialogs, HUDs etc) using layered maps- it assumes familiarity with Nasal scripting, Object Oriented Programming with Nasal and Canvas - while an existing chart layer (e.g. to show VORs/NDBs or DMEs) can be easily added and used/customied by non-programmers within a few minutes, creating new MapStructure layers requires additional knowledge which is detailed below. Usually, new layers can be easily created by adapting existing layers. In its simplest form, MapStructure is just a library of a handful of existing layers (navaids, traffic, weather etc) that can be easily -and quickly- reused for all kinds of different purposes.

For people familiar with Nasal coding, MapStructure is also a framework to create new fully reusable layers with integrated support for caching, resource management (listener/timers) and efficient spatial searching/updating. For FlightGear versions >= 3.3+, there will also be a Canvas Widget to easily add MapStructure-based maps to any Canvas dialog and/or to Canvas MFDs that internally use the Canvas GUI framework. Another novelty that we're currently exploring is extending the framework such that GUI-based creation/editing/customization of layers becomes a first-class concept, e.g. to create a weather or tutorial/missions GUI editor: see #Porting the map dialog.

History

Basically, the ND/MapStructure frameworks came to be out of a certain degree of disagreement with other MFD/glass cockpit efforts using the Canvas system. That was primarily because many of those lacked support for:

  • truly independent instances
  • reuse (other aircraft/use-cases, e.g. sharing common logic between aircraft displays and GUI dialogs)
  • customiation (think styling)


Especially, we found it hugely frustrating to see awesome avionics like the Avidyne Entegra R9 being developed for aircraft like the extra500, that were impossible to reuse for other aircraft cockpits, but that also contained features/code that would have been useful elsewhere (think mapping/charting displays). So that more often than not, copy & paste was the only option to "reuse" useful code elsewhere. Thus, the idea was to grow a library of generic building blocks that live in $FG_ROOT and that are sufficiently generic and customizable to basically enable people to collaborate more properly by providing a strong incentive via compelling functionality that can be easily maintained/updated in the future. The thinking was that all aircraft/GUI dialogs using a single back-end would automatically benefit from significant updates to the back-end that way (think performance, features etc). Our strategy was to adopt a MVC (model/view/controller) approach, where the model would represent what is to be rendered, the view would map the Canvas system and its primitives (elements) and underlying APIs, whereas the controller would usually be use-case specific (think cockpit vs. GUI dialog).[1]


Today, just the MapStructure framework is under 1k lines of code, and is making roughly 9k-15k lines of C++ code obsolete in FlightGear (agradar,groundradar,wxradar,map dialog), unifying the whole shebang in the process, which also ensures that more modern OSG/OpenGL can be used, i.e. better performance in the long-term.

This all has taken place in under 24 months actually (MapStructure just being 6 months old now actually) - whereas hard-coded instruments (wxradar, agradar, navdisplay,kln89 etc) are usually 5+ years old, and things like the Map dialog even older - in comparison, these were all "easier" to come up with in the first place, but obviously don't scale as well as a Canvas-based solution. Gijs NavDisplay framework is already better accessible and more feature-rich than the original ND code.

But this kind of work isn't exactly fun, it comprises lots of refactoring and is more about talking and coordinating things than actually coding stuff - because the implementation may very well just be a fraction the size of all the manifestations that are hopefully replaced over time.

We've seen some extremeley skilled contributors making sizable contributions without them ever documenting the internals, so that getting up to scratch with things later on may be next to impossible without spending a huge amount of time, that could be equally spent on re-designing certain features/systems.

This is something that we have actively worked to address in the context of Nasal/Canvas and efforts like the ND/MapStructure frameworks, i.e. those are now extensively documented, not just including "roadmaps" and "milestones", but also internal design stuff, including even step-by-step tutorials and coding examples - admittedly, this has taken up quite a bit of spare time, that could have just as well been spent "coding" - but given the wiki stats, we seem to be on the right track here, i.e. most of these articles have seen between 2k-8k views within just ~10-12 weeks, which is kinda impressive (some of our most popular articles "only" have seen 40k views in years!), but this also ensures at the same time that even if some of us were to disappear for a few months, people would still be able to pick up where we (TheTom, Philosopher, Gijs, myself) left off, no matter if this means "maintaining" our existing code - or modernizing/replacing it completely.

In professional software development circles, writing documentation is a necessary evil, as is writing unit tests - in FlightGear, people prefer to spend their time doing "fun" stuff instead for understandable reasons. Then again, some of the main building blocks and key technologies in FlightGear were developed by people who obviously understood that having sufficient docs is at least as important for a feature to survive than the actual code, just look at architectural pillars contributed by people like David Megginson (property tree) or Andy Ross (Nasal) - those are typically the same guys who were responsible for much of the original documentation targeted at core developers, no matter if it's through extensive use of doxygen comments or through dedicated design articles.

Some of our most active contributors spent little to no time ensuring that future developers will be able to continue their work - and that's a problem that will only really become obvious once someone is too busy with other aspects of their life to contribute (or even just back to answer questions).

What is it ?

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.

MapStructure-MVC-Framework

The primary challenge here is, that these different front-ends (e.g. a GUI dialog or cockpit) will typically all have very different means to interact with the map and its rendered layers.

MapStructure MVC separation

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.

MapStructure Controllers

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.

MapStructure-UseCases.png

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.

As of 05/2015, all this still is work in progress and not yet completely finished, but we're hoping to completely replace the old map.nas code by FG 3.2 and provide a pure Canvas-based re-implementation of the Map dialog in the 4.0 release.

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.

MapStructure Internals


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=1,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.

Initial Waypoint support for Philosopher's MapStructure framework

Layers

See Canvas MapStructure Layers

Issues

(link to canvas-navdisplay label in issue tracker)

We currently have quite a few old files doing basically identical stuff, just with different draw routines - such as e.g. "runway-nd", "airports", "airports-nd". It would probably be a good idea to generalize this by implementing LOD and styling support - so that we can use a single APT layer that supports all necessary customizations.

Basically, we could unify things a bit by using LOD support to show APT in different modes, so that taxiways etc would be only shown if necessary. It would still make sense to maintain separate draw/symbol files for these, so that things can be easily reused.

Scale/Ratio Handling

Need to take original canvas texture dimensions into account, and also keep in mind that layers may be shown not in "fullscreen" mode (using the whole texture), but just a sub-area, i.e. a clipped canvas-region, and setTranslation/view/size calls must be adjusted accordingly. The GPSMap196 and Avidyne are instruments that support partial-screen modes.

GPSMap196 with MapStructure layers (currently exhibiting some scaling/LOD issues due to hard-coded assumptions in some of the layers)
Cquote1.png The extra500 developers just posted a few screen shots of the Avidyne Entegra R9 in "moving map" mode, which demonstrates a few use-cases that we do not currently support in MapStructure (the ND being a different matter for now):


Extra_EA-500#Map_:_Moving_Map
IFD FMS-Map FPL.png
IFD MAP-Map+.png

Specifically, this probably means that we may need to revamp/resurrect symbol controllers and maybe accept an optional callback to pre-/post-transform individual symbols (they're rotated apparently according to runway orientation), and allow them to be overridden when instantiating the layer (i.e. via the ctor if SVG-based). And we may also want to explore SVG styling by patching svg.nas to run a "transform" callback to customize/colorize certain elements of the SVG. The instrument itself is meanwhile making fairly aggressive use of texture-map based caching for basically ALL symbols.

The other thing is that translation (centered/off center mode) should always be based on the parent group's dimensions (bounding box) and that we really shouldn't be using the top-level canvas, because a MFD very well be split into several screen areas (which also applies to the GPSMap196 and most other modern avionics).


Cquote2.png

Performance

MapStructure/NavDisplay testbed with integrated profiling support for each layer

the performance issue related to the map-canvas.xml dialog pointed out by another user (whenooming) has nothing to do with Canvas or Nasal - it is C++ code that is causing this, and this is no surprise - it's a known issue actually. As a matter of fact, the hard-coded NavDisplay, as well as the hard-coded Map dialog both suffered from the exact same issue, until Gijs solved it by rewriting the projection handling code. However, the original Canvas projection code is still using the old implementation, i.e. the update got never back-ported: Canvas MapStructure#Performance We're talking here about roughly 50 lines of C++ code, which exist already and which would need to be turned into a Canvas::Map::Projection Note that this would benefit any, and all, Canvas-based NavDisplay/Map applications, too - also note that this information is readily available in various places, so there is no need to draw any wrong conclusions or spread any misinformation here.[2]


Cquote1.png We've already fixed that in the (old) map dialog, by using an azimuthal equidistant projection (see screenshot). Porting the projection to Canvas is on my todo list. Such a projection is much much better for navigational use.

Curves in routes are not calculated by Canvas, nor by the ND though. It's the route manager that splits up a route in segments in order to get smooth transitions.


Cquote2.png
Cquote1.png The new ND uses the actual route-manager paths, which allows it to draw holdings, flyby waypoints (thanks to James recent work) etc. But we'll need the azimuthal projection anyway, so I'll bump my todo list
Cquote2.png
Cquote1.png I do agree that it would make sense to sub-class the Canvas projection class and implement Gijs' changes there, like we originally discussed in the merge request: https://gitorious.org/fg/flightgear?p=fg:flightgear.git;a=commit;h=3f433e2c35ef533a847138e6ae10a5cb398323d7


https://gitorious.org/fg/flightgear?p=fg:flightgear.git;a=blob;f=src/GUI/MapWidget.cxx;hb=96a2673dd8a08b70396e2be1e567c0e89d8cf6e3#l1573

Ideally, we would expose the projection as a property for each Map so that it can be changed dynamically.


Cquote2.png

These are the most likely jobs ahead to improve MapStructure performance:

  • adopt our caching scheme (i.e. referencing precreated raster images rather than having hundreds of openVG paths) 80}% completed
    • port existing MapStructure layers to make use of the cache (VOR, NDB, DME etc)
  • optimize our use of navcache queries as per ticket #1320
  • use cppbind to turn props.nas APIs into native code ?
  • investigate exposing systime() stats for each loop/callback per layer ?
  • expose canvas-specific stats via SGTimeStamp to each canvas element ?

Porting the map dialog

80}% completed

Cquote1.png is anyone working on replacing the map dialog based upon this code? Because that seems like the most important use case to demonstrate that MVC pattern can support, aside from the NavDisplays.
— zakalawe (Mon Nov 04). Re: How to display Airport Chart?.
(powered by Instant-Cquotes)
Cquote2.png
Cquote1.png I was just reviewing some UI changes, and realised we have three map options, which seems a little excessive. I’d like to deprecate my ‘manual' OpenGL map in favour of the canvas variant, but the Canvas variant is missing some features:
  • can’t be panned with the mouse
  • dialog doesn’t resize nicely
  • no course + distance computation (although this feature also seems to be broken in the OpenGL version)

Is anyone actively working on the Canvas map? I’d be happy to collaborate to address remaining pieces so we can drop the old map (probably after 3.4 I guess, given the timeframe)


— James Turner (2014-12-06). [Flightgear-devel] Canvas map dialog status.
(powered by Instant-Cquotes)
Cquote2.png
Cquote1.png I noticed a couple of bugs in the Canvas-in-PUI map in 3.3 git:


  1. The VOR radial is always true. It doesn't listen to the "Magnetic headings" checkbutton. In northern part of Canada true heading is used (because of the extreme magnetic variation), but anywhere else magnetic.
  2. When I click "Aircraft heading up", the radial of the VOR doesn't get rotated.
  3. I cannot click and drag the map.
  4. The blue "flight history" line is very thick.
  5. When I zoom in, the map moves and display certain symbols to the edges for a split second, but then hides them. It should instead hide them before doing the translation of the symbol.
  6. The map hides too much symbols at the edges in any case. It should be more like the PUI map IMHO, which displays a symbol if it could be visible in the GUI widget.
  7. When I zoom in or out, the relative location of various symbols changes. For example, go to CYRB and zoom in on the map. The VOR seems to be between the 2 NDB's.
  8. If you zoom all the way out, the VOR moves to the left of the 2 NDB's. The old PUI map doesn't have this problem.
  9. Sometimes labels of waypoints are difficult to read.

— onox (Mon Dec 15). Re: Phasing out MapWidget post 3.2.
(powered by Instant-Cquotes)
Cquote2.png


Note  The dialog as shown below will be part of FlightGear 3.2, future changes (mostly additional/refined layers and controllers) are planned for the subsequent release cycle, i.e. post 3.2
Cquote1.png the recent GUI work committed by TheTom means that we can phase out the PUI map-canvas.xml dialog and create the whole dialog procedurally using a native canvas window, because we really only need two types of widgets: buttons and checkboxes, which are both supported now, thanks to the new "Aircraft Center":
Native Canvas MapWidget dialog using just Canvas Widgets, the Map has been turned into a Canvas Widget itself, so that it can be easily embedded in other dialogs (obviously the whole thing still needs to be cleaned up, layouting isn't currently used yet). See [3] This is a link to the FlightGear forum.
More Canvas/MapWidget prototyping. See [4] This is a link to the FlightGear forum.
Screen shot showing TheTom's modified demo dialog with a MapStructure based map that has clipping and event handling applied, i.e. can respond to common mouse events in order to either display tooltips or support drag&drop-style GUI dialogs for editing map-like displays, e.g. for creating an Advanced weather GUI [5] This is a link to the FlightGear forum.. Particular care must be taken to formalize z-index handling (rendering priority) for each MapStructure layer, which is something we still need to work out, especially for possibly overlapping symbols...
This screen shot shows an experimental MapStructure-based Weather Editor Widget for Canvas using interactive and editable lcontroller files that respond to GUI events so that map symbols can be interactively placed on a map.

Cquote2.png

Porting the airports.xml dialog

Cquote1.png The rationale being that we're in the process of turning our map-canvas.xml dialog into a generic, and reusable, widget for $FG_ROOT/Nasal/canvas/gui/widgets - the widget is using the new Layout engine to align other widgets (checkboxes & buttons for now). And one of our goals is to also modernize airports.xml (the airport selection dialog) such that it can use MapStructure. The simplest way would be to simply turn the PUI CanvasWidget into a Canvas.Window() so that layouting etc works as expected, and so that we can simply show the new scripted MapWidget there.
— Hooray (Thu Jun 26). Using a PUI CanvasWidget as a Canvas.Window ?.
(powered by Instant-Cquotes)
Cquote2.png
This is a screen shot showing Stuart's PUI/XML based airport selection dialog with an embedded Canvas MapWidget that is driven by Philosopher's MapStructure framework using TheTom's latest GUI/Layouting changes (e.g. checkbox widgets).

For runways.draw it would make sense to split up the function to have a helper function that draws a single runway. Runway-drawing is fairly modular already, so we should be able to populate a SymbolCache with a handful of building blocks for any runway, and then scale/transform it as needed, while custom stuff would be rendered on top. That should help speed up things rather significantly. Also, we could then have a shared AirportCache that keeps a FIFO of 4x4 airports by using a single 1024x1024 texture. Multiple ND/dialog instances could then access the same cache to speed up things. Taxiways are a different beast, would need to be rendered and cached in full, because they're too custom drawing-wise.

Regarding generic-canvas-map.xml: it would be awesome to remove this, but doing so will break Stuart's airport selection dialog ... We need to get rid of map.nas first of all, i.e. port airport-selection to use MapStructure - because airport-select includes and parameterizes the corresponding file.

I suppose we should continue this discussion in public, either on the forum or via the wiki - where we have a section on porting this dialog, and getting rid of map.nas

I'd love to see all this stuff removed obviously - but there's still some work ahead AFAIU But it should be perfectly doable within the upcoming release cycle. It's not exactly a lot of work either. Mostly refactoring and generalization. The whole workaround is documented at: http://wiki.flightgear.org/Canvas_Map_API#Full_Example:_Creating_dialogs_with_embedded_Canvas_Maps

The first step would be porting the missing layers to MapStructure *.symbol files, i.e. stuff like towers, airports (parking, runways, taxiways). Then, we'll need to add another map controller for panning support. The XML file itself will also procedurally add "toggle" checkboxes for each requested layer, so that's why there's so much code in it. map-canvas.xml can probably become 100% Canvas based pretty soon. But the airport-selection dialog contains a ton of PUI widgets for which do not yet have Canvas equivalents. So I am more inclined to "just" port the 3-4 missing layers, add a map controller for panning and then adopt MapStructure there, at which point the original map.nas stuff can be deleted, it will have served its purpose by then, i.e. establishing the MVC separation.

Also, some missing layers may benefit from caching to some extent, and styling should probably be supported by all of them.

TFC

  • generalize aircraft position controller to support AI/MP traffic
  • investigate adopting our caching scheme to make the layer faster

Hooray's Todo List

  • 06/2014 MapStructure.nas:
    • CENTERED/HDG/MAG TRK view modes (navdisplay.mfd) ?
    • ILS & WIND layers are missing (see Map dialog, see navdisplay.mfd for WIND code)
    • add StationaryObjectLayerController helper where update semantics are wrapper properly (i.e. used by the new NavaidSymbolLayer)
    • introduce a LegSymbolLayer for LineSymbols (RTE/FLT)  ?
    • instead of directly accessing properties like AP/RM/RADIO, encapsulate in driver hash (conceptually, we could read/display an AI flight plan for example)?
    • encapsulate main-only layers (RTE,WPT) and depending features (AP/RM/RADIO stuff in VOR/DME etc) that won't work for AI/MP traffic (driver hash)
    • LayerController: provide a method to enable/disable layers when paused (/sim/freeze), e.g. FLT doesn't make much sense to constantly redraw here ?
    • the SAT (fetched image overlay) stuff is too hacky, need to introduce an OverlayLayer helper that wraps remotely fetched raster images
    • displaying TCAS/AIS feeds can now be done via Nasal http bindings and running JSON queries, better sub-class MultiSymbolLayer to support feeds where the fetching method is the equivalent of searchCmd, but can be re-implemented for different URL schemes using Philosoper's/TheTom's demo code
    • interactive stuff is not yet mature enough to work, unless it's just tooltips for each symbol, but could help us implement DATA ?
  • layers should probably encode information about being "safe" for non-main aircraft, i.e. a few layers don't make much sense for AI/MP traffic due to references to properties/APIs that are n/a, such as flight plan/routing, autopilot, instrumentation properties - those lookups should be disabled by the "driver" hash (or position controller).
  • SymbolLayer should probably be sub-classed for "static" layers (navaids) and "volatile" layers with movign objects as per Philosopher's comments
  • LOD handling needs to be refined, cannot be just setScale() in complex cases like taxiways/runways - we better add an interface that receives notifications once the range is changed (MapController) and use different callbacks for certain ranges ? 60}% completed
  • work out a good way to integrate caching and styling, where each style variation would be a separate cache entry 40}% completed
  • once that works, we can easily implement animations on top of cached entries with different styles and use a timer to change/hide/show groups
  • extend the SymbolCache class to integrate the dialog for inspection purposes, i.e. via a method so that each cache can be quickly visualized/inspected
    • maybe maintain a refcount for each referenced cache entry ?
    • provide a method freeSpots() ?
    • introduce a CacheManager class as the super class managing different SymbolCache objects ?
  • need to extend the svg parser to provide hooks for registering callbacks so that we can also support styling for SVG images, based on looking up elements by ID and override colors
  • Symbol.Controller.getpos() is doing two things at once 1) checking if the obj is supported, 2) extracting lat/lon - it would be better to split this, so that we can reuse the is_supported check in other places, such as validating searchCmd() results
  • MapController: need to expose some flags to determine if a map is used inside a GUI dialog or aircraft: This is because we do not typically need to update most map layers shown in an aircraft when paused - but we may very well want to update layers when used inside a GUI dialog, i.e. check maketimer() use here.
  • make logging a part of the framework, overload print/printlog
  • make profiling via systime() a part of the framework, for each controller/model/view (optional)
  • investigate hardening, i.e.
    • type-checking (typeof)
    • e.g. searchCmd should always return a vector, and isa(type) should be supported by getpos 60}% completed
    • checking available symbols (and typeof) for scontroller/lcontroller and symbol files
  • clean up existing files and add more comments 20}% completed
    • including a MapStructure URL (wiki) to each file so that people know where to look for further info Done Done
  • add a new section: Porting Layers (i.e. from the old format to the new one)
  • provide an option to suspend/restart MapStructure
  • provide an option to reload MapStructure (layers) from disk (RAD)
  • styling - will involve: 30}% completed (currently being prototyped inside the DME layer)
    • allow colors/fonts to be overridden, i.e. any draw .symbol callback would use a lookup hash that can be customized (instead of hard-coding things)
    • allow size to be overridden (overlapping with LOD support)
    • allow custom file names or callbacks to be provided for symbols (draw routines)
    • i.e. come up with a "Styleable" class that exposes an interfaces that is implemented by StyleableColor, StyleableFont, StyleableSymbol
    • this means that style-specific things should never be directly part of the draw routine itself, but need to be encapsulated, i.e. setColor/setColorFill/setSize/setText/setFont/setFontSize etc - so that these methods can be afterwards called on the canvas group - the styleable class should probably just sub-class Symbol.Controller to make this work

Roadmap post 3.2

Cquote1.png Well, we've been talking about the lack of current navdata in FG recently. On the MapStructure side of things we really only need to encapsulate any NasalPositioned calls, to support arbitrary data - including even fetched (XML), or manually entered navaids/fixes. Such data could then be centrally served or based on NaviGraph. So we wouldn't have to touch any of the existing NavDB stuff to work around its limitations.


So I am thinking about moving all NasalPositioned queries in MapStructure into a "driver" hash, but maybe more in line with the aircraftpos.controller stuff that Philosopher developed, just specific to some kind of "NavaidSource".

That would allow us to easily work around such limitations, so that I-NEMO, Hyde, Gijs etc can easily use the latest data on their EFB/ND. The other motivation here is that I experimented with interactive layers, i.e. where objects could be visually placed on a map-these could be scenery objects, but also navaids. And once we move such assumptions out of the lcontroller files, we can trivially support other cool use-cases, not just GUI-based editing, but even simulated radio navigation or other "learning" tools:

(see the linked image) (see the linked image)


— Hooray (Mon Jun 23). Re: 777 EFB: initial feedback.
(powered by Instant-Cquotes)
Cquote2.png


  • port missing layers 70}% completed
  • the granularity of the maketimer()-created update timers should be configurable/exposed when creating a layer, i.e. to allow aircraft developers to adjust timing as required
  • handle signals to trigger controller updates for radio, autopilot, route manager or AI/MP and weather changes 60}% completed
  • adapt NavDisplay framework to use MapStructure internally 80}% completed
  • look into extending the controller framework for different use cases (ND/airport selection dialog) 30}% completed
  • look into porting the Map dialog 20}% completed
  • look into updating the airport-selection dialog Not done Not done
  • add a MapStructure-based chart to the route manager dialog Not done Not done
  • integrate the ND into other airliner cockpits (757,767 and Airbus series) to ensure that things remain generic 20}% completed

Framework

  • we should add some helper functions for people to more easily do RAD when developing new layers/controllers, this would include supporting reloading from disk, and creating a simple canvas window to show the corresponding layers, maybe with a "reload" button that suspends and restarts everything after reloading - this would probably even help us Not done Not done
  • investigate adding a dedicated "Filter" class for positionedSearches, so that things like custom object filters (think ATC/RADAR etc) can be more easily implemented by implementing a corresponding interface (mirage2000) [9] This is a link to the FlightGear forum. Not done Not done
  • logging should probably be handled by the framework directly, so that messages can be redirected or stored Not done Not done
  • support benchmarking/profiling of layers via systime() - as mentioned by Philosopher on the forum Not done Not done
  • a simple form of unit testing (sanity checks) would make sense for regression testing purposes, i.e. to ensure that the number of drawables never grows beyond the total number of elements (ditto for layers) Not done Not done
  • properly implement (separate) init/update at the framework level Not done Not done
  • consider supporting loops that are split across frames Not done Not done
  • add LOD support 20}% completed
  • add styling support (colors, different SVG images or draw callbacks for symbols) Not done Not done
  • many features would greatly benefit from having a simple animation framework, i.e. for changing/scaling symbols etc (not specific to positioned objects) Not done Not done
  • implement a caching scheme 70}% completed

Using Layers

Using 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


Adding-new-MapStructure-Layers.png

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] [10] [11] [12]
name = Name to use for referencing (via Symbol[SymbolLayer][.Controller].get(name), etc.) and console messages. 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

MapStructure-Symbol-UML.png

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.

Slightly extended Property Browser to show a preview for each Canvas for troubleshooting/debugging purposes, as per [6] This is a link to the FlightGear forum..

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.

MapStructure/SymbolCache experiments for caching symbols and symbol instancing [7] This is a link to the FlightGear forum.

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.

WIP.png 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

MapStructure-Controller-UML.png

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: [13]).

Symbol Controller

MapStructure-SymbolController-UML.png

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. [14].

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, [15], 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

A simple MapStructure layer called DEMO that renders a circle of NDB symbols (SVGs) in the vicinity of the aircraft using geo.nas and its helpers like geo.aircraft_position() and the geo.Coord.apply_course_distance(course, dist) method as per [8] This is a link to the FlightGear forum.. See Canvas Radar to learn more.


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 [16]). 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"
Cquote1.png the real reason behind the _equals is because it's a design hack: the geo difference search function (whatever it's called) originally supported almost any object through Nasal, but since Tom extended it in C++ space it doesn't really work with non-positioned objects (he uses cppbind typecasting into positioned objects), so I retained the old capability by allowing the user to specify a custom equals function to use the old Nasal-space difference engine. (For non-positioned layers this allows us to use roughly the same mechanism with Nasal objects/classes.)


Setting _equals to a "return 0" function simply means that each object will be taken off and recreated on each search. But, when there's a better id to use, MapStructure can remove only the old ones and add only the new ones. How that is done depends on both the model object and the drawing – for instance, if a *.symbol file says to completely redraw the Canvas group based on the model, that means it potentially could be used with different models during its lifetime, whereas if the symbol is only set up once on initialization, it would not reflect changes in it's model object and thus should be removed when it is gone.


— Philosopher (Tue Jun 24). Re: Get objects to show up on Map/Radar.
(powered by Instant-Cquotes)
Cquote2.png
Cquote1.png 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.
— Philosopher (Tue Jun 24). Re: Get objects to show up on Map/Radar.
(powered by Instant-Cquotes)
Cquote2.png
Cquote1.png 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)
Cquote2.png

Canvas Map Controller

This has the most interesting jobs: it manages how the whole map is positioned and re-rendered. Example: [17]. 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.[3]

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.[4]



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[5]


In the drawVOR callback it is then using those variables (as per the hash).

[6]

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.

Screenshot showing the tutorials node in the property browser
Screen shot showing the tutorials dialog with the c172p taxiing tutorial selected

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) {
        node != nil or 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");
 #props.dump(allTargets);
 return getTargets(allTargets);
}

} # searchCmd

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);
}; # searchCmd

Now, here's some code to actually test the new layer (contributed by ludomotico [18] This is a link to the FlightGear forum.):

    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=1,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>


Custom MapStructure layer for displaying Tutorials targets - showing the c172p taxiing tutorial at KLVK

This tells us that we need to start fgfs using --airport=KLVK --runway=12