Canvas Map API

From FlightGear wiki
Revision as of 17:07, 27 September 2012 by Hooray (talk | contribs) (→‎Open Issues)
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

  • 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

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

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 {
 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 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).

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: MAP_LAYERS["navaids"], 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>