Canvas Map API: Difference between revisions

From FlightGear wiki
Jump to navigation Jump to search
 
(47 intermediate revisions by 2 users not shown)
Line 1: Line 1:
{{Affected by Canvas|MapStructure}}
{{Canvas Navigation}}
{{Canvas Navigation}}


== Background ==
== 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.
'''Update 11/2013: please see [[MapStructure]]'''
 
As of FlightGear 2.9, the base package contains additional [[Canvas]] helpers to help with the creation of navigational displays (map dialogs or map instruments). 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 ==
== 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.
Heavily reduce the amount of specialized Canvas/Nasal code in XML dialogs and instruments related to mapping/charting (airport-selection, map widget, navdisplay etc) and ensure that a maximum degree of code sharing is accomplished, regardless of the canvas placement mode that is used - bottom line being, it really shouldn't matter if a map is shown as part of an airport selection dialog, as part of the moving map dialog, as part of the navdisplay etc.
 
== 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 ==
== Design ==
Line 28: Line 21:


== Supported Layers ==
== Supported Layers ==
As of 10/2012, the following "layers" are supported:
As of 10/2012, the following "layers" are supported (not yet using [[MapStructure]]):


* runways
* runways
Line 34: Line 27:
* parking
* parking
* tower
* 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:
<syntaxhighlight lang="php">
## "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
</syntaxhighlight>
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
<syntaxhighlight lang="xml">
  <!-- 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>
</syntaxhighlight>
Add this to the "Nasal/close" tag to clean up all resources automatically:
<syntaxhighlight lang="php">
</syntaxhighlight>
== 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:
<syntaxhighlight lang="php">
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);
}
</syntaxhighlight>
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:
<syntaxhighlight lang="php">
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);
}
</syntaxhighlight>
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:
See the forum for details: http://forum.flightgear.org/viewtopic.php?f=71&t=21139


<syntaxhighlight lang="php">
== ToDo ==
var NavaidModel = {};
NavaidModel.new = func make(LayerModel, NavaidModel);
NavaidModel.init = func {
var navaids = findNavaidsWithinRange(50);
foreach(var n; navaids)
        me.push(n);
me.notifyView();
}
</syntaxhighlight>


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).
Originally, the idea was that we want to avoid the ongoing degree of copy/paste programming that could be seen in the airport selection dialog - next, the 744 ND duplicates lots of code used in the airport dialog, but also in the underlying framework - all without using inheritance or delegates/predicates to customize things. So over time, we would end up with tons of custom code that is highly specific to each aircraft/display and GUI dialog.


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:
From a low-level point of view, it shouldn't matter to the draw routines if they're called by an aircraft instrument or by a GUI dialog. However, once they contain use-case specific logic, such as getprop() calls and other conditionals, things do get messy pretty quickly. Thus, what I have been doing is simply copying things to separate *.draw files for different scenarios, i.e. I now ended up with airport.draw and airports-nd.draw for example - that isn't all that elegant, but at least it solves the problem of having to implement full models and controllers for each scenario (well for now), because all the embedded logic can stay, and only needs to be refactored later on.


<syntaxhighlight lang="php">
Thus, the idea is basically this:


var NavLayer =  {};
*.draw files contain the low-level logic to draw arbitrary symbols (runway, waypoint, taxiways, airports, traffic) - without knowing anything about the frontend/user - so that such details needs to be separately encapsulated. The *.model files merely determine what is to be drawn, data-wise, as in NasalPositioned queries and their results - all stored in a single Nasal vector. The layer files turn everything into a dedicated canvas group that can be separately toggled/redrawn/updated - the currently missing bits are controllers that tie everything together, so that each frontend (instrument, MFD, GUI dialog, widget) can separately specify and customize behavior by registering their own callbacks.
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);
Most of this is already working here (with very minor regressions so far that Gijs won't be too happy about ...), but otherwise I managed to turn Gijs code into layers/models that can be directly used by the old framework, i.e. I can now add a handful of new ND layers (fixes, vor, MP or AI traffic, route etc) to the airport selection dialog (or any other dialog actually), and things do show up properly.
</syntaxhighlight>


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).
What's still missing is the controller part, and styling for different aircraft/GUI purposes - also, Gijs' ND routines are currently used directly, without any LOD applied - but I think that can be easily solved by using the canvas's scale() method.


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.
Just to clarify: stuff like getprop("instrumentation/nav[0]/frequencies/selected-mhz") doesn't belong into the visualization part of the code (i.e. the draw/view stuff) - simply because that's the sort of stuff that would "break" once we're using a different use-case, e.g. the map dialog - which is such conditions would need to be either specified in some form of condition hash, as part of the draw* signature, or via a dedicated Drawable class that's able to evaluate such conditions.


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:
To be honest, that was the whole point of moving stuff to different *.layer and *.model files - so that these *.draw files can be shared, without requiring any modifications - your current example would still require using differrent *.draw files for each purpose.


<syntaxhighlight lang="xml">
* Implementing proper LOD support  for *.draw callbacks (i.e. using canvas/scaling)
<layer>
* Implementing support for different styles
                <name>navaids</name>
* Supporting multiple NDs per aircraft
                <init-property>selected-airport/id</init-property>
* Supporting multiple maps per dialog
                <property>display-navaids</property>
* Stress Test: render a dialog with 10 NDs, for 10 different MP/AI aircraft
                <description>Show Navaids</description>
* clean up map.nas, get rid of stubs and old code
                <default>enabled</default>
* get rid of the PUI helpers in map.nas, and use real canvas dialogs instead
</layer>
* Optimize (Nasal/Canvas and C++ space)
* come up with a framework for MFDs
* use the MFD framework for creating a stylable ND class


</syntaxhighlight>
== Also see ==
* http://forum.flightgear.org/viewtopic.php?f=71&t=21139

Latest revision as of 14:33, 29 June 2014

IMPORTANT: Some, and possibly most, of the features/ideas discussed here are likely to be affected, and possibly even deprecated, by the ongoing work on providing a property tree-based 2D drawing API accessible from Nasal using the new Canvas system available since FlightGear 2.80 (08/2012). Please see: MapStructure for further information

You are advised not to start working on anything directly related to this without first discussing/coordinating your ideas with other FlightGear contributors using the FlightGear developers mailing list or the Canvas subforum This is a link to the FlightGear forum.. Anything related to Canvas Core Development should be discussed first of all with TheTom and Zakalawe. Nasal-space frameworks are being maintained by Philosopher and Hooray currently. talk page.

Canvasready.png


Background

Update 11/2013: please see MapStructure

As of FlightGear 2.9, the base package contains additional Canvas helpers to help with the creation of navigational displays (map dialogs or map instruments). 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

Heavily reduce the amount of specialized Canvas/Nasal code in XML dialogs and instruments related to mapping/charting (airport-selection, map widget, navdisplay etc) and ensure that a maximum degree of code sharing is accomplished, regardless of the canvas placement mode that is used - bottom line being, it really shouldn't matter if a map is shown as part of an airport selection dialog, as part of the moving map dialog, as part of the navdisplay etc.

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 (not yet using MapStructure):

  • runways
  • taxiways
  • parking
  • tower

See the forum for details: http://forum.flightgear.org/viewtopic.php?f=71&t=21139

ToDo

Originally, the idea was that we want to avoid the ongoing degree of copy/paste programming that could be seen in the airport selection dialog - next, the 744 ND duplicates lots of code used in the airport dialog, but also in the underlying framework - all without using inheritance or delegates/predicates to customize things. So over time, we would end up with tons of custom code that is highly specific to each aircraft/display and GUI dialog.

From a low-level point of view, it shouldn't matter to the draw routines if they're called by an aircraft instrument or by a GUI dialog. However, once they contain use-case specific logic, such as getprop() calls and other conditionals, things do get messy pretty quickly. Thus, what I have been doing is simply copying things to separate *.draw files for different scenarios, i.e. I now ended up with airport.draw and airports-nd.draw for example - that isn't all that elegant, but at least it solves the problem of having to implement full models and controllers for each scenario (well for now), because all the embedded logic can stay, and only needs to be refactored later on.

Thus, the idea is basically this:

  • .draw files contain the low-level logic to draw arbitrary symbols (runway, waypoint, taxiways, airports, traffic) - without knowing anything about the frontend/user - so that such details needs to be separately encapsulated. The *.model files merely determine what is to be drawn, data-wise, as in NasalPositioned queries and their results - all stored in a single Nasal vector. The layer files turn everything into a dedicated canvas group that can be separately toggled/redrawn/updated - the currently missing bits are controllers that tie everything together, so that each frontend (instrument, MFD, GUI dialog, widget) can separately specify and customize behavior by registering their own callbacks.

Most of this is already working here (with very minor regressions so far that Gijs won't be too happy about ...), but otherwise I managed to turn Gijs code into layers/models that can be directly used by the old framework, i.e. I can now add a handful of new ND layers (fixes, vor, MP or AI traffic, route etc) to the airport selection dialog (or any other dialog actually), and things do show up properly.

What's still missing is the controller part, and styling for different aircraft/GUI purposes - also, Gijs' ND routines are currently used directly, without any LOD applied - but I think that can be easily solved by using the canvas's scale() method.

Just to clarify: stuff like getprop("instrumentation/nav[0]/frequencies/selected-mhz") doesn't belong into the visualization part of the code (i.e. the draw/view stuff) - simply because that's the sort of stuff that would "break" once we're using a different use-case, e.g. the map dialog - which is such conditions would need to be either specified in some form of condition hash, as part of the draw* signature, or via a dedicated Drawable class that's able to evaluate such conditions.

To be honest, that was the whole point of moving stuff to different *.layer and *.model files - so that these *.draw files can be shared, without requiring any modifications - your current example would still require using differrent *.draw files for each purpose.

  • Implementing proper LOD support for *.draw callbacks (i.e. using canvas/scaling)
  • Implementing support for different styles
  • Supporting multiple NDs per aircraft
  • Supporting multiple maps per dialog
  • Stress Test: render a dialog with 10 NDs, for 10 different MP/AI aircraft
  • clean up map.nas, get rid of stubs and old code
  • get rid of the PUI helpers in map.nas, and use real canvas dialogs instead
  • Optimize (Nasal/Canvas and C++ space)
  • come up with a framework for MFDs
  • use the MFD framework for creating a stylable ND class

Also see