Canvas Map API

From FlightGear wiki
Jump to navigation Jump to search


Background

As of FlightGear 2.9, the base package contains additional Canvas helpers to help with the creation of navigational displays. These are currently under development and will change rapidly during the next weeks and months, so that we can come up with a generic and re-usable design for different dialogs/instruments.

Objective

The Nasal module map.nas in $FG_ROOT/Nasal/canvas will serve as the shared backend for all sorts of mapping/charting purposes in FlightGear. So that GUI dialogs and instruments can use the same code. As of 10/2012 the module is still being designed, so nothing written here is set in stone. This page is just intended to document the whole process.

Open Issues

  • Supporting multi-symbol instancing at the core canvas/C++ level, i.e. using "pointers" to point to another group, instead of replicating it over and over again
  • re-implementing object "marking" (runways/parking) in the airports dialog
  • Refining the model API
  • Using the system for different dialogs & instruments
  • generalizing the currently required dialog "prologue" in the Nasal/open block (using caller() and closure())
  • supporting/addressing multiple canvases per dialog
  • implementing a real MVC controller and porting the code to use it
  • improving the MVC separation further
  • extending the Layer API to support the z-index (rendering ordering)
  • resource cleanup (listeners & timers)

Design

The design follows the basic MVC (Model/View/Controller) principle, i.e.:

  • the Model contains the data to be shown
  • the View contains the Layer (Canvas group)
  • the Controller contains the interface to update the model and the view (using timers and/or listeners)


The basic idea is such that each layer is linked to a "control" property to easily toggle its visibility (this can be a GUI property for a checkbox or a cockpit hotspot), drawables (symbols) can be put in different layers and easily toggled on/off.

Supported Layers

As of 10/2012, the following "layers" are supported:

  • runways
  • taxiways
  • parking
  • tower
  • navaids


At the moment, we are working on additional layers - in order to port the Map and Navigation display display to Canvas. This will probably require support for:

  • Routing (waypoints)
  • Fixes
  • multiplayer traffic
  • AI traffic

Full Example: Creating dialogs with embedded Canvas Maps

At the moment, the system (and its design) is still evolving - so nothing is set in stone, and most things written here should be considered a "draft". This is also why the system makes currently some assumptions and requires certain variables/functions to be specified in the dialog XML.

(For the latest information, you'll want to refer to $FG_ROOT/gui/dialogs/airports.xml as the "de facto" example of how to use the system)

Add this to the "Nasal/open" tag of your XML dialog and customize it for your dialog:

 ## "prologue" currently required by the canvas-generic-map 
       var dialog_name ="airports"; #TODO: use substr() and cmdarg() to get this dynamically
       var dialog_property = func(p) return "/sim/gui/dialogs/airports/"~p; #TODO: generalize using cmdarg      
       var DIALOG_CANVAS = gui.findElementByName(cmdarg(), "airport-selection");
       canvas.GenericMap.setupGUI(DIALOG_CANVAS, "canvas-control"); #TODO: this is not a method!
      ## end of canvas-generic-map prologue

Note the string "airport-selection" which should match the name of of the subsequent canvas section.

Add this to a group in the dialog where you want the Canvas to appear

  <!-- Instantiate a generic canvas map and parametrize it via inclusion -->
      <!-- TODO: use params and aliasing -->
      <canvas include="/Nasal/canvas/generic-canvas-map.xml">

	<name>airport-selection</name>
        <valign>fill</valign>
        <halign>fill</halign>
        <stretch>true</stretch>
        <pref-width>600</pref-width>
        <pref-height>400</pref-height>
        <view n="0">600</view>
        <view n="1">400</view>


       <features>
	<!-- TODO: use params and aliases to make this shorter -->
	<!-- TODO: support styling, i.e. image sets/fonts and colors to be used -->
	<!-- this will set up individual "layers" and map them to boolean "toggle" properties -->
	<!-- providing an optional "description" tag here allows us to create all checkboxes procedurally -->
	<dialog-root>/sim/gui/dialogs/airports</dialog-root>
	<range-property>zoom</range-property>

	<!-- These are the ranges available for the map:       var ranges = [0.1, 0.25, 0.5, 1, 2.5, 5] -->

	<ranges>
		<range>0.1</range>
		<range>0.25</range>
		<range>0.5</range>
		<range>1</range>
		<range>2.5</range>
		<range>5</range>
	</ranges>

	<!-- available layers and their toggle property (appended to dialog-root specified above) -->

 	<layer>
                <name>airport_test</name>
                <init-property>selected-airport/id</init-property>      <!-- the init/input property that re-inits the layer -->
                <property>display-test</property>                    	<!-- property switch to toggle the layer on/off -->
                <description>Show TestLayer</description>               <!-- GUI label for the checkbox -->
                <default>disabled</default>                              <!-- default checkbox/layer state -->
                <hide-checkbox>true</hide-checkbox>                     <!-- for default layers so that the checkbox is hidden -->
        </layer>

	<layer>
		<name>runways</name>
		<init-property>selected-airport/id</init-property>	
		<property>display-runways</property>			
		<description>Show Runways</description> 		
		<default>enabled</default>				
		<hide-checkbox>true</hide-checkbox>  			
	</layer>
 	<layer>
                <name>taxiways</name>
		<init-property>selected-airport/id</init-property>
                <property>display-taxiways</property>
		<description>Show Taxiways</description>
		<default>disabled</default>
        </layer>

 	<layer>
                <name>parkings</name>
		<init-property>selected-airport/id</init-property>
                <property>display-parking</property>
		<description>Show Parking</description>
		<default>disabled</default>
        </layer>

 	<layer>
                <name>towers</name>
		<init-property>selected-airport/id</init-property>
                <property>display-tower</property>
		<description>Show Tower</description>
		<default>enabled</default>
        </layer>
<!--
 	<layer>
                <name>navaid_test</name>
		<init-property>selected-airport/id</init-property>
                <property>display-navaids</property>
		<default>disabled</default>
        </layer>
-->	

       </features>
      </canvas>

Add this to the "Nasal/close" tag to clean up all resources automatically:

Adding support for new Layer Types

You only need to read this section if you are interested in understanding the underlying design, in order to extend existing layer types or create new ones from scratch.

Required:

  • callback to draw a single layer element (aircraft, waypoint, navaid, runway)
  • A new class (Nasal hash) that derives from the "Layer" class and implements its interface
  • A "data source" (provider) hash that provides the data to be rendered (i.e. populates the model)


The callbacks to draw a particular layer element are to be found in $FG_ROOT/Nasal/canvas/map, at the moment, we have these modules (listed in ascending complexity):

  • Nasal/canvas/map/tower.draw
  • Nasal/canvas/map/navaid.draw
  • Nasal/canvas/map/parking.draw
  • Nasal/canvas/map/runways.draw
  • Nasal/canvas/map/taxiways.draw

Each "*.draw" file contains a single Nasal function, named "draw_FOO" - where FOO is simply chosen based on what is drawn, so you can make up your own name, like "draw_route" for example.

When implementing support for new routines, it is recommended to take an existing file, such as the tower.draw or navaid.draw files and just copy/paste and customize things as needed.

This is what the tower.draw file looks like:

var draw_tower = func (group, apt,lod) {
      var group = group.createChild("group", "tower");
      var icon_tower =
              group.createChild("path", "tower")
                 .setStrokeLineWidth(1)
                 .setScale(1.5)
                 .setColor(0.2,0.2,1.0)
                 .moveTo(-3, 0)
                 .vert(-10)
                 .line(-3, -10)
                 .horiz(12)
                 .line(-3, 10)
                 .vert(10);

      icon_tower.setGeoPosition(apt.lat, apt.lon);
}

As you can see, the draw* callback takes three arguments:

  • the canvas group/layer to be used
  • the layer-specific "model" information (airport/apt in this case)
  • an LOD argument (currently not yet used).

The airport.draw file demonstrates how to create paths procedurally. But you can just as well load the vector image from an SVG file. For an example, please refer to navaid.draw, which is shown below:

var draw_navaid = func (group, navaid, lod) {
      #var group = group.createChild("group", "navaid");

      var symbols = {NDB:"/gui/dialogs/images/ndb_symbol.svg"}; # TODO: add more navaid symbols here
      if (symbols[navaid.type] == nil) return print("Missing svg image for navaid:", navaid.type);

      var symbol_navaid = group.createChild("group", "navaid");
      canvas.parsesvg(symbol_navaid, symbols[navaid.type]);
      symbol_navaid.setGeoPosition(navaid.lat, navaid.lon);
}

Once you have created your own "draw" implementation, you still need to have a data source or "provider" that determines for what data the callback needs to be invoked. This is currently still a little "hackish" and still work in progress. So we have just a very simple MVC model at the moment, whose interface needs to be implemented.

For example, the various airport-specific "layers" all use the same "AirportModel" to be found in airport.model. In the future, other models can be found in *.model files. For a simple example of how to populate the model, just refer to the file "navaid.model", which is shown here:

var NavaidModel = {};
 NavaidModel.new = func make(LayerModel, NavaidModel);
 NavaidModel.init = func {
 me.clear(); # empty the model's vector
 var navaids = findNavaidsWithinRange(50);
 foreach(var n; navaids)
        me.push(n);
 me.notifyView();
}

As can be seen, each "Model" needs to derive from the "LayerModel" class. At the moment, the only method that needs to be implemented is the "init" method, which is invoked once the Layer is re-initialized.

In this case, the init() method merely clears the internal vector using the "me.clear()" call, runs findNavaidsWithinRange(50); and then populates the model by appending each navaid to the MVC model in the top-level "LayerModel". Afterwards, the "notifyView" method is invoked to update the view (this will probably change pretty soon, once a real MVC controller abstraction is added).

Note that the model merely stores the data - only the draw* callback will try to access it, i.e. the lat/lon/id fields.

Also, the LayerModel class is currently just an extremely simple wrapper, all of its fields/methods are directly available to you:

##
# A layer model is just a wrapper for a vector with elements
# either updated via a timer or via a listener

var LayerModel = {_elements:[] };
 LayerModel.new = func make(LayerModel);
 LayerModel.clear = func me._elements = [];
 LayerModel.push = func (e) append(me._elements, e);
 LayerModel.get = func me._elements;
 LayerModel.update = func;
 LayerModel.hasData = func size(me. _elements);

That is, you have the following methods available:

  • new() - to create a new LayerModel instance
  • clear() - to clear the internal _elements vector
  • push() - to append new data to the internal _elements vector
  • get() - to get a handle to the internal _elements vector
  • update() - empty placeholder for the time being
  • hasData() - to get the size of the internal _elements vector (beginning at 0)

In addition, a bunch of additional fields are currently exposed to the model, because the design is currently not a full MVC implementation, so that we need to work around the lack for a proper MVC separation, and a real controller interface, by making handles to the enclosing layer and map available to the model, these are:

  • _view_handle - a handle to the layer (canvas group)
  • _map_handle - a handle to the layer's map (map group)

These are properly initialized during construction of the layer, but will probably be phased out, once the design has improved a little and becomes more stable - so please just consider them "helpers" for now, because we don't have all of the API in place yet.

Now, to actually make a new layer known to the system, we need to add another file that registers the layer. For an example of how to do this, please see navaids.layer:

var NavLayer =  {};
 NavLayer.new = func(group,name) {
  var m=Layer.new(group, name, NavaidModel);
  m.setDraw (func draw_layer(layer:m, callback: draw_navaid, lod:0) );
  return m;
}

register_layer("navaids", NavLayer);

A new layer hash is created by returning a new Layer object via "Layer.new" which derives from the corresponding model (NavaidModel in this case), and setting the draw callback to the draw routine that we created earlier, in this case using the MAP_LAYERS hash - which, currently, needs to be extended in map.nas (but which will soon be changed such that it automatically loads all *.layer files).

The layer is registered at the end of the file using the "register_layer" call and passing a symbolic/lookup name, and the name of the hash that implements the layer.

Finally, to actually load the new layer, you'll want to edit your XML dialog file and add it to your XML file. For example, this is how the navaids layer is enabled:

<layer>
                <name>navaids</name>
                <init-property>selected-airport/id</init-property>
                <property>display-navaids</property>
                <description>Show Navaids</description>
                <default>enabled</default>
</layer>

How the whole thing works

This is targeted at people who are interested in helping improve the whole thing, so that they get a better understanding of how the whole thing hangs together at the moment.

For the time being, the implementation is specific to GUI dialogs - in that it will largely simplify (and even automate) the creation of GUI dialogs with an embedded canvas "map". This is accomplished using a handful of fixed assumptions, most of which will need to be addressed in order to support additional dialogs, and eventually, also non-GUI, i.e. instrument-use. Namely, that means:

  • that people can simply instantiate a "GenericMap" by including an existing XML file
  • dialog-specific settings can be overridden and customized (see above, or $FG_ROOT/gui/dialogs/airports.xml) - i.e. just copied/pasted
  • the GenericMap is a LayeredMap where layers are created for different features and linked to toggle properties
  • there are a handful of variables/functions that need to be declared in the body of the dialog's Nasal/open block
  • in addition, 1-2 function calls need to be made during initialization, in the Nasal/OPEN section
  • these are later on used by the system to instantiate all requested layers dynamically
  • first of all, this is the "setupGUI" function, which will create the requested layers, and add GUI checkboxes and zoom buttons to the dialog automatically