Howto:Prototyping a new NavDisplay Style: Difference between revisions

From FlightGear wiki
Jump to navigation Jump to search
 
(31 intermediate revisions by 3 users not shown)
Line 9: Line 9:
|started= 10/2016  
|started= 10/2016  
|description = [[PUI]] dialog to instantiate independent ND instances using different NavDisplay styles  
|description = [[PUI]] dialog to instantiate independent ND instances using different NavDisplay styles  
|status = Under active development as of 10/2016
|status = working proof-of-concept (as of 10/2016)
|developers =  Hooray (since 10/2016)  
|developers =  Hooray (since 10/2016)  
<!--
<!--
Line 18: Line 18:
== Objective ==
== Objective ==
{{See also|FGCanvas}}
{{See also|FGCanvas}}
Demonstrate how a simple PUI/XML dialog can be used to easily prototype new [[NavDisplay]] styles, showing two instances for each pilot, which can be controlled/tested independently (using different settings for range, modes, traffic etc). Once the dialog is closed/reopened, the underlying navdisplay.mfd/styles files are also automatically reloaded from disk, so that you don't need to exist/restart fgfs to test your cchanges.
Demonstrate how a PUI/XML dialog can be used to easily prototype new [[NavDisplay]] styles, showing two (or more) instances for each pilot, which can be controlled/tested independently (using different settings for range, modes, traffic etc). Once the dialog is closed/reopened, the underlying navdisplay.mfd/styles files are also automatically reloaded from disk, so that you don't need to exist/restart fgfs to test your cchanges.


== Gallery ==
== Gallery ==
Line 34: Line 34:
Revamped-NDStyles-dialog.png|Improved layouting for different ND styles
Revamped-NDStyles-dialog.png|Improved layouting for different ND styles
Canvas-ND-RAD.png|ND dialog should the route manager layer (MapStructure RTE)
Canvas-ND-RAD.png|ND dialog should the route manager layer (MapStructure RTE)
ND-dialog-customizable-size-and-instances.png|Screenshot showing a [[PUI]] dialog with two [[NavDisplay]] instances, featuring support for customizable resolution/size of the ND as well as selectable number of NDs to be shown.
ND-Airbus-Style-With-Route.png|Screenshot showing Gijs' [[NavDisplay]] using the Airbus style created by artix in a [[PUI]] dialog with an embedded [[Canvas]] widget to render the ND and the corresponding widgets.
Canvas-ND-resizable.png|Resizable Canvas ND <ref>https://forum.flightgear.org/viewtopic.php?p=298889#p298889</ref>
</gallery>
</gallery>


== Approach ==
== Approach ==


This is a PUI/XML dialog with two embedded CanvasWidget areas that instantiate independent ND instances using a subset of the code commonly found in the aircraft specific ND.nas file, adapted to use the embedded Canvas region for rendering the two NDs and GUI buttons/widgets added to control the whole thing without necessarily requiring a fully developed cockpit.
This is a PUI/XML dialog with multiple embedded CanvasWidget areas that instantiate independent ND instances using a subset of the code commonly found in the aircraft specific ND.nas file, adapted to use the embedded Canvas region for rendering the two NDs and GUI buttons/widgets added to control the whole thing without necessarily requiring a fully developed cockpit.


This is primarily useful for rapid prototyping ("RAD"), i.e. quickly testing additions and changes without having to exit/restart fgfs and without having a full aircraft developed yet.
This is primarily useful for rapid prototyping ("RAD"), i.e. quickly testing additions and changes without having to exit/restart fgfs and without having a full aircraft developed yet.
== Implementation ==
Meanwhile "the dialog" has become more sophisticated than originally planned, people looking at the code/XML markup may wonder how this whole thing is working, so the following parapgraph is going to provide some additional background inforamtion:
In general, all PUI/XML dialogs should be using the markup for the widgets, and layouting directives, detailed in $FG_ROOT/Docs/README.gui and README.layout
In addition to the features mentioned in those files there are some undocumented features, which can be found in the dialog building routines (C++ code).
Internally, a dialog is just a property tree - it is read and loaded into the/a property tree and then further processed (usually by C++ code).
This is accomplished using so called "fgcommands", which is the stuff that you'd typically add to menubar.xml to make a new dialog item known to the GUI.
Some of the more typical GUI related fgcommands are:
* dialog-show
* dialog-close
Now, what is taking place internally is that dialogs may not only contain the known XML markup, but may also contain embedded Nasal code blocks.
There are really only two main ways to embed such code: at the dialog-level using nasal/open tags for code that is executed prior to showing a dialog that is already loaded from disk, and nasal/load blocks loaded per Canvas widget (the black area showing the ND texture).
To be fair, there also is a nasal/close tag to do some housekeeping/cleanup (as well as arbitrary Nasal in bindings).
Anyway, what is so powerful about Nasal embedded in dialog files is that the code is typically executed prior to the creation/display of the dialog, and the Nasal code gets a handle to the property tree representation of the dialog markup (processed aready by the XML parser).
This "handle" is not just read-only, i.e. you get a mutable handle, which means you can traverse the tree and query it (e.g. to locate certain elements) and then freely add/remove or rename/duplicate elements, using the exact same APIs used to manipulate the global proeprty tree (e.g. props.nas).
And that is basically answering your question: The XML file posted in the wiki is incomplete - it would not make any sense at all without the embedded Nasal portions, but it contains a few redundant nodes that are not recognied by the existing GUI engine.
However, nasal/open block will specifically look for these redundant blocks and treat those as "templates" for certain widgets, e.g. those requiring dynamic contents (think a list of known styles, which cannot be hard-coded, think a list of MAP modes, ranges etc).
Internally, the Nasal code will then locate a handful of useful blocks, instantiate (copy/rename, and customize them) and insert them into the appropriate locations in the dialog/property tree.
This is why we may have only a single combo/select or "button" element in the XML markup, despite having possibly dozens of buttons: The code gets a copy to the dialog's property tree, and then duplicates useful/required nodes and customizes/renames them.
What you have now found are leftovers from one of my debugging sessions - basically, all these strings are irrelevant placeholders, because they get overridden anyway. However, at some point, the code wasn't working properly, so that I added strings like "unchanged mode" to the XML - this was so that I could tell immediately which routine would fail replacing those placeholders, because that would show up prominently in a dump of the "procedurally created dialog".
And that sums up the whole approach pretty well: The dialog only contains a subset of the required widgets/data, but it's using ~200 LOC to procedurally reuse those and replace those placeholders with more appropriate contents/variables.
Anyway, none of this should be relevant as long as you really only want to use the dialog. As a matter of fact, I don't think that there are many other dialogs making such elaborate use of this templating technique, but it is this that makes it possible to instantiate an arbitrary number of NDs using different resolutions - because what is really taking place is that the corresponding settings are written into the global property tree, and then the dialog is closed, the GUI subsystem reset and the same dialog opened, this time picking up the defaults you set up using the previously closed dialog.
I realize that this may sound complicated - but once you understand README.gui, README.layout and how embedded Nasal blocks work, and how those can manipulate the dialog before it is shown, it actually all makes sense, and isn't much unlike JavaScript/DOM traversal (akin to jQuery even).<ref>{{cite web
  |url    =  https://forum.flightgear.org/viewtopic.php?p=297505#p297505
  |title  =  <nowiki> Re: Canvas ND on another aircraft than Boeing and Airbus </nowiki>
  |author =  <nowiki> Hooray </nowiki>
  |date  =  Oct 26th, 2016
  |added  =  Oct 26th, 2016
  |script_version = 0.40
  }}</ref>


== Goals ==
== Goals ==
Line 60: Line 106:
* maybe allow the style to be selected/changed at runtime, possibly per ND ? {{Progressbar|90}}
* maybe allow the style to be selected/changed at runtime, possibly per ND ? {{Progressbar|90}}
* add some style specific stats to the dialog (SVG filename, number of elements, listeners, timers etc)  
* add some style specific stats to the dialog (SVG filename, number of elements, listeners, timers etc)  
* add VSD support
* add VSD support <ref>http://www.boeing.com/commercial/aeromagazine/articles/2012_q1/3/img/AERO_2012q1-3_fig3.jpg</ref>
* make some more settings configurable (and persistent using the userarchive attribute):
* make some more settings configurable (and persistent using the userarchive attribute):
** number of NDs to be shown
** number of NDs to be shown
Line 66: Line 112:
** default style
** default style
** settings previously used per ND
** settings previously used per ND
== Ideas ==
* add a fullscreen option ?
* add a layout combo for table layouts (row X column)
* add tooltips, and mouseover events for ND elements - e.g. name of the layer/symbol etc ?
* add a mode to close the dialog and decouple the ND (no PUI overhead) ?
* Use {{Func link|bind()}} to make io.include work within {{Func link|call()}}
* Consider supporting an actual texture map (use one of the aircraft textures) to show a proper MCP instead of those checkboxes (ask Hyde/Gijs) ?
* Use an outer/embedded Canvas for the UI ? (see the Garmin 196) <ref>https://forum.flightgear.org/viewtopic.php?f=4&p=297433#p297424</ref>
* allow multiple NDs to be rendered to a single canvas, which would help clean up all hard-coded assumptions about the size of the Canvas and the clipping region used {{Not done}}
Optionally show some  additional ND/style specific info:
* SVG filename, font-mapper
* number of features
* number of MapStructure layers
* active timers/listeners
* total instances


== Issues ==
== Issues ==
* Currently, the code will re-instantiate a new NavDisplay when the style is changed - however, it makes more sense to simply set a corresponding property and close/reopen the dialog, as that would then also allow us to procedurally add style-specific GUI widgets (the Airbus style supports a handful of features that the Boeing one does not know anything about), besides this would make the whole reload button obsolete, too. {{Progressbar|90}}


== Status ==
== Status ==
Line 78: Line 141:
{{Note|The following patch contains changes that move the definition of aircraft specific switches back into the navdisplay.styles file, adding GUI related fields to the hash, so that these can be used to procedurally create a UI without requiring an actual cockpit (including an optional values vector). In addition, this makes deletion of the Canvas by the ND optional, so that PUI based CanvasWidgets can reuse the ND without having to reallocate a new Canvas. Also, changes references to properly resolve to canvas.Path.
{{Note|The following patch contains changes that move the definition of aircraft specific switches back into the navdisplay.styles file, adding GUI related fields to the hash, so that these can be used to procedurally create a UI without requiring an actual cockpit (including an optional values vector). In addition, this makes deletion of the Canvas by the ND optional, so that PUI based CanvasWidgets can reuse the ND without having to reallocate a new Canvas. Also, changes references to properly resolve to canvas.Path.
}}
}}
<syntaxhighlight lang="diff">diff --git a/Nasal/canvas/api.nas b/Nasal/canvas/api.nas
<syntaxhighlight lang="diff">diff --git a/Nasal/canvas/map/navdisplay.mfd b/Nasal/canvas/map/navdisplay.mfd
index ea67217..8fd77e5 100644
index 89996f4..43080cb 100644
--- a/Nasal/canvas/api.nas
+++ b/Nasal/canvas/api.nas
@@ -462,7 +462,7 @@ var Map = {
  },
  del: func()
  {
-    #print("canvas.Map.del()");
+    # print("canvas.Map.del()");
    if (me.controller != nil)
      me.controller.del(me);
    foreach (var k; keys(me.layers)) {
diff --git a/Nasal/canvas/map/navdisplay.mfd b/Nasal/canvas/map/navdisplay.mfd
index 89996f4..7f8f4f8 100644
--- a/Nasal/canvas/map/navdisplay.mfd
--- a/Nasal/canvas/map/navdisplay.mfd
+++ b/Nasal/canvas/map/navdisplay.mfd
+++ b/Nasal/canvas/map/navdisplay.mfd
Line 140: Line 190:
  foreach(var t; me.timers)
  foreach(var t; me.timers)
  t.stop();
  t.stop();
foreach(var l; me.listeners)
- foreach(var l; me.listeners)
- removelistener(l);
- removelistener(l);
+ foreach(var l; me.listeners)
+ # removelistener(l);
+ # removelistener(l);
+ call(removelistener, [l]);
+ call(removelistener, [l]);
Line 357: Line 408:
  }
  }
  },
  },
</syntaxhighlight>
{{Note|navdisplay.mfd around line 270, the clip options must be embedded into 'rect()' }}
<syntaxhighlight lang="diff">
me.map = me.nd.createChild("map","map")
.set("clip", "rect("~map_rect~")")
.set("screen-range", 700);
</syntaxhighlight>
== Menubar ==
[[File:Canvas-nd-menubar-entry.png|thumb|Screenshot showing  [[PUI]] menubar with added '''Canvas NavDisplay''' entry for the Canvas ND dialog.]]
<syntaxhighlight lang="diff">
diff --git a/Translations/en/menu.xml b/Translations/en/menu.xml
index a5411d3..7144ba8 100644
--- a/Translations/en/menu.xml
+++ b/Translations/en/menu.xml
@@ -66,6 +66,7 @@
        <random-failures>Random Failures</random-failures>
        <system-failures>System Failures</system-failures>
        <instrument-failures>Instrument Failures</instrument-failures>
+      <canvas-nd>Canvas NavDisplay</canvas-nd>
        <!-- AI menu -->
        <ai>AI</ai>
diff --git a/gui/menubar.xml b/gui/menubar.xml
index afb7ff3..45803e5 100644
--- a/gui/menubar.xml
+++ b/gui/menubar.xml
@@ -412,6 +412,15 @@
                </item>
                <item>
+                      <name>canvas-nd</name>
+                      <binding>
+                              <command>dialog-show</command>
+                              <dialog-name>canvas-nd</dialog-name>
+                      </binding>
+              </item>
+
+
+              <item>
                        <name>failure-submenu</name>
                        <enabled>false</enabled>
</syntaxhighlight>
</syntaxhighlight>


Line 368: Line 463:
}}
}}


<syntaxhighlight lang="xml"><?xml version="1.0"?><?xml version="1.0"?>
{{Note|The following can be split into a XML file and a nasal file - makes editing and syntax highlighting easier. However, variables used in the close block must be declared in the open block before the include of the nasal file.
}}
 
<syntaxhighlight lang="xml"><?xml version="1.0"?>
<!--
 
GNU GPL v2.0 (c) FlightGear.org 2016
For details, see: http://wiki.flightgear.org/Howto:Prototyping_a_new_NavDisplay_Style
 
-->
<PropertyList>
<PropertyList>
   <name>canvas-nd</name>
   <name>canvas-nd</name>
Line 374: Line 478:
   <layout>vbox</layout>
   <layout>vbox</layout>
   <text>
   <text>
     <label>Canvas ND</label>
     <label>Canvas NavDisplay</label>
   </text>
   </text>


Line 385: Line 489:


<checkbox>
<checkbox>
<pref-width>30</pref-width>
<pref-width>40</pref-width>
<pref-height>22</pref-height>
<pref-height>22</pref-height>


Line 428: Line 532:
       <frame>
       <frame>
       <layout>vbox</layout>
       <layout>vbox</layout>
       <padding>30</padding>
       <padding>15</padding>
       <stretch>true</stretch>
       <stretch>true</stretch>


Line 447: Line 551:
         <script>
         <script>
           fgcommand("dialog-close", props.Node.new({"dialog-name": "canvas-nd"}));
           fgcommand("dialog-close", props.Node.new({"dialog-name": "canvas-nd"}));
  fgcommand("reinit", props.Node.new({subsystem:'gui'}));
           settimer(func fgcommand("dialog-show", props.Node.new({"dialog-name": "canvas-nd"})), 0);
           settimer(func fgcommand("dialog-show", props.Node.new({"dialog-name": "canvas-nd"})), 0);
         </script>
         </script>
Line 457: Line 562:
       <group>
       <group>
       <layout>hbox</layout>
       <layout>hbox</layout>
      <padding>5</padding>
       <name>mfd-controls</name>
       <name>mfd-controls</name>
       </group>
       </group>
Line 468: Line 574:
    -->
    -->
    <!-- reducing the dimensions even more will look weird due to layouting issues, given the number of hbox aligned checkbox widgets -->
    <!-- reducing the dimensions even more will look weird due to layouting issues, given the number of hbox aligned checkbox widgets -->
                     <pref-width>400</pref-width>
                     <pref-width>256</pref-width>
                     <pref-height>400</pref-height>
                     <pref-height>256</pref-height>
    <view>1024</view>
    <view>1024</view>
    <view>1024</view>
    <view>1024</view>
Line 530: Line 636:
   <nasal>
   <nasal>
   <open><![CDATA[
   <open><![CDATA[
# to be used for shutting down each created instance upon closing the dialog (see the close block below)
# variable not visible in close block, if defined in included nasal file, so defined here
var MFDInstances = {};
#io.include("canvas-nd-dlg-open.nas");
### optional: move everything below this line up to end of CDATA to a separate nas file and include it (uncomment the line above)
var getWidgetTemplate = func(root, identifier) {
var getWidgetTemplate = func(root, identifier) {
var target = globals.gui.findElementByName(root,  identifier );
var target = globals.gui.findElementByName(root,  identifier );
if(target == nil) die("Target node not found for identifier:"~identifier);
if(target == nil) die("Target node not found for identifier:"~identifier);
return target;
return target;
}
}
Line 577: Line 687:


var initialize_nd = func(index) {
var initialize_nd = func(index) {
  # print("running init nd");
  var my_canvas = canvas.get( cmdarg() );
  my_canvas = canvas.get( cmdarg() );


  # FIXME: use the proper root here
  # FIXME: use the proper root here
  var myND = setupND(mfd_root: "/instrumentation/efis["~index~"]", my_canvas: my_canvas, index:index);
  var myND = setupND(mfd_root: "/instrumentation/efis["~index~"]", my_canvas: my_canvas, index:index);
};
};


Line 589: Line 696:
var errors = [];
var errors = [];


# NOTE: this requires changes to navdisplay.mfd
# NOTE: this requires changes to navdisplay.mfd (see the wiki article for details)
# call(func[, args[, me[, locals[, error]]]);
# call(func[, args[, me[, locals[, error]]]);
# call(
# call(
Line 623: Line 730:
var getSwitchesForND = func(index) {
var getSwitchesForND = func(index) {
    
    
   var style_property = "/gui/dialogs/canvas-nd/nd["~index~"]/selected-style";
   var style_property = "/fgcanvas/nd["~index~"]/selected-style";
   var style = getprop(style_property);
   var style = getprop(style_property);


   # make sure that the style  is exposed via the property tree
   # make sure that the style  is exposed via the property tree
   if (style == nil) {
   if (style == nil) {
print("Ooops, style was undefined, using default");
style = 'Boeing'; # our default style
style = 'Boeing'; # our default style
setprop(style_property, style);
setprop(style_property, style);
Line 633: Line 741:


   var switches = NDStyles[style].default_switches;
   var switches = NDStyles[style].default_switches;
   print("Using ND style/switches:", style);
   # print("Using ND style/switches:", style);


   if (switches == nil) print("Unknown ND style: ", style);
   if (switches == nil) print("Unknown ND style: ", style);
Line 642: Line 750:
var setupND = func(mfd_root, my_canvas, index) {
var setupND = func(mfd_root, my_canvas, index) {


   var style = getprop("/gui/dialogs/canvas-nd/nd["~index~"]/selected-style") or 'Boeing';
   var style = getprop("/fgcanvas/nd["~index~"]/selected-style") or 'Boeing';


     ##
     ##
Line 655: Line 763:
     var handle = "ND["~index~"]";
     var handle = "ND["~index~"]";
     MFDInstances[handle] = myND;  
     MFDInstances[handle] = myND;  
     # print("ND setup completed");
     # return {nd: myND, property_root: mfd_root};
    return {nd: myND, property_root: mfd_root};
} # setupND()
} # setupND()


# this determines how many NDs will be added to the dialog, and where their controls live in the property tree
# this determines how many NDs will be added to the dialog, and where their controls live in the property tree
# TODO: support default style per ND, different dimensions ?
# TODO: support default style per ND, different dimensions ?
# persistent config/profiles
var canvas_areas = [
var canvas_areas = [
{name: 'captain.ND', property_root:'/instrumentation/efis[0]',},
{name: 'captain.ND', property_root:'/instrumentation/efis[0]',},
{name: 'copilot.ND', property_root:'/instrumentation/efis[1]',},
#{name: 'copilot.ND', property_root:'/instrumentation/efis[1]',},
# you can add more entries below, for example:  
# you can add more entries below, for example:  
# {name: 'engineer.ND', property_root:'/instrumentation/efis[2]',},
        #{name: 'engineer.ND', property_root:'/instrumentation/efis[2]',},
];
];


# procedurally add one canvas for each ND to be shown (requires less code/maintenance, aka DRY)
# procedurally add one canvas for each ND to be shown (requires less code/maintenance, aka DRY)


var index=0;
var totalNDs = getprop("/fgcanvas/total-nd-instances") or 1;
foreach(var c; canvas_areas) {
 
# var index=0;
for(var index=0;index<totalNDs;index+=1) {
var c = {name: 'ND #'~index, property_root:'/instrumentation/efis['~index~']'};
print("Setting up ND:", c.name);
# foreach(var c; canvas_areas) {
var switches = getSwitchesForND(index);
var switches = getSwitchesForND(index);


Line 682: Line 795:
canvasWidget.getNode("text/label",1).setValue(c.name);
canvasWidget.getNode("text/label",1).setValue(c.name);
canvasWidget.getNode("canvas/name").setValue(c.name);
canvasWidget.getNode("canvas/name").setValue(c.name);
var r = getprop("/fgcanvas/nd-resolution") or 420;
canvasWidget.getNode("canvas/pref-width").setValue(r);
canvasWidget.getNode("canvas/pref-height").setValue(r);


# instantiate and populate combo widgets
# instantiate and populate combo widgets
var selectWidgets= [
var selectWidgets= [
{node: 'combo', label:'Style', attribute: 'Style', property:'/gui/dialogs/canvas-nd/nd['~index~']/selected-style', values:keys(NDStyles) },
{node: 'combo', label:'Style', attribute: 'Style', property:'/fgcanvas/nd['~index~']/selected-style', values:keys(NDStyles) },
{node: 'group[1]/combo[2]', label:'nm', attribute: 'RangeNm', property:c.property_root~switches['toggle_range'].path, values:switches['toggle_range'].values },
{node: 'group[1]/combo[2]', label:'nm', attribute: 'RangeNm', property:c.property_root~switches['toggle_range'].path, values:switches['toggle_range'].values },
{node: 'group[1]/combo', label:'', attribute: 'ndMode', property:c.property_root~switches['toggle_display_mode'].path, values:switches['toggle_display_mode'].values },
{node: 'group[1]/combo', label:'', attribute: 'ndMode', property:c.property_root~switches['toggle_display_mode'].path, values:switches['toggle_display_mode'].values },
Line 702: Line 820:


# FIXME: shouldn't hard-code this here ...
# FIXME: shouldn't hard-code this here ...
var keyValueMap = [1,0,-1]; # switches['toggle_lh_vor_adf'].values; # myCockpit_switches['toggle_lh_vor_adf'].values;
var keyValueMap = [1,0,-1]; # switches['toggle_lh_vor_adf'].values;  


# FIXME look up the proper lh/rh values here
# FIXME look up the proper lh/rh values here
# and use the proper root
populateSelectWidget(leftVORADFSelector, "", "VOR/ADF(l)", index, "/instrumentation/efis["~index~"]/inputs/lh-vor-adf", keyValueMap);   
populateSelectWidget(leftVORADFSelector, "", "VOR/ADF(l)", index, "/instrumentation/efis["~index~"]/inputs/lh-vor-adf", keyValueMap);   
var rightVORADFSelector = canvasWidget.getNode("group[1]/combo[3]");
var rightVORADFSelector = canvasWidget.getNode("group[1]/combo[3]");
Line 713: Line 832:
var cb_index = 0;
var cb_index = 0;
# add checkboxes for each boolean switch
# add checkboxes for each boolean switch
#  TODO: customize this for style-specific switches !


# HACK: get rid of this, it's just an alias for now
# HACK: get rid of this, it's just an alias for now
Line 733: Line 851:
} # add checkboxes for each boolean ND switch
} # add checkboxes for each boolean ND switch


index += 1;
# index += 1;
} # foreach ND instance
} # foreach ND instance


#print("Complete dialog is:");
#print("Complete dialog is:");
#props.dump( WidgetTemplates['canvas-placeholder'] );
#props.dump( WidgetTemplates['canvas-placeholder'] );
### end of nasal code to be moved
   ]]>
   ]]>
   </open>
   </open>
Line 767: Line 888:
       </binding>
       </binding>
     </button>
     </button>
<combo>
  <label>Instances</label>
  <name>TotalNDInstances</name>
  <property>/fgcanvas/total-nd-instances</property>
  <value>1</value>
  <value>2</value>
  <value>3</value>
  <value>4</value>
  <value>5</value>
  <binding>
      <command>dialog-apply</command>
      <object-name>TotalNDInstances</object-name>
  </binding> 
  <binding>
        <command>nasal</command>
        <script>
          fgcommand("dialog-close", props.Node.new({"dialog-name": "canvas-nd"}));
  fgcommand("reinit", props.Node.new({subsystem:'gui'}));
          settimer(func fgcommand("dialog-show", props.Node.new({"dialog-name": "canvas-nd"})), 0);
        </script>
      </binding>
  </combo>
  <combo>
  <label>Size</label>
  <name>NDResolution</name>
  <property>/fgcanvas/nd-resolution</property>
  <value>256</value>
  <value>384</value>
  <value>512</value>
  <value>768</value>
  <binding>
      <command>dialog-apply</command>
      <object-name>NDResolution</object-name>
  </binding> 
    <binding>
        <command>nasal</command>
        <script>
          fgcommand("dialog-close", props.Node.new({"dialog-name": "canvas-nd"}));
  fgcommand("reinit", props.Node.new({subsystem:'gui'}));
          settimer(func fgcommand("dialog-show", props.Node.new({"dialog-name": "canvas-nd"})), 0);
        </script>
      </binding>
  </combo>
</group>
</group>



Latest revision as of 14:29, 24 February 2017

This article is a stub. You can help the wiki by expanding it.
NavDisplay testing/prototyping environment
CanvasND development via PUI.png
Started in 10/2016
Description PUI dialog to instantiate independent ND instances using different NavDisplay styles
Contributor(s) Hooray (since 10/2016)
Status working proof-of-concept (as of 10/2016)

Objective

Demonstrate how a PUI/XML dialog can be used to easily prototype new NavDisplay styles, showing two (or more) instances for each pilot, which can be controlled/tested independently (using different settings for range, modes, traffic etc). Once the dialog is closed/reopened, the underlying navdisplay.mfd/styles files are also automatically reloaded from disk, so that you don't need to exist/restart fgfs to test your cchanges.

Gallery

Approach

This is a PUI/XML dialog with multiple embedded CanvasWidget areas that instantiate independent ND instances using a subset of the code commonly found in the aircraft specific ND.nas file, adapted to use the embedded Canvas region for rendering the two NDs and GUI buttons/widgets added to control the whole thing without necessarily requiring a fully developed cockpit.

This is primarily useful for rapid prototyping ("RAD"), i.e. quickly testing additions and changes without having to exit/restart fgfs and without having a full aircraft developed yet.

Implementation

Meanwhile "the dialog" has become more sophisticated than originally planned, people looking at the code/XML markup may wonder how this whole thing is working, so the following parapgraph is going to provide some additional background inforamtion:

In general, all PUI/XML dialogs should be using the markup for the widgets, and layouting directives, detailed in $FG_ROOT/Docs/README.gui and README.layout

In addition to the features mentioned in those files there are some undocumented features, which can be found in the dialog building routines (C++ code).

Internally, a dialog is just a property tree - it is read and loaded into the/a property tree and then further processed (usually by C++ code).

This is accomplished using so called "fgcommands", which is the stuff that you'd typically add to menubar.xml to make a new dialog item known to the GUI. Some of the more typical GUI related fgcommands are:

  • dialog-show
  • dialog-close

Now, what is taking place internally is that dialogs may not only contain the known XML markup, but may also contain embedded Nasal code blocks. There are really only two main ways to embed such code: at the dialog-level using nasal/open tags for code that is executed prior to showing a dialog that is already loaded from disk, and nasal/load blocks loaded per Canvas widget (the black area showing the ND texture). To be fair, there also is a nasal/close tag to do some housekeeping/cleanup (as well as arbitrary Nasal in bindings). Anyway, what is so powerful about Nasal embedded in dialog files is that the code is typically executed prior to the creation/display of the dialog, and the Nasal code gets a handle to the property tree representation of the dialog markup (processed aready by the XML parser).


This "handle" is not just read-only, i.e. you get a mutable handle, which means you can traverse the tree and query it (e.g. to locate certain elements) and then freely add/remove or rename/duplicate elements, using the exact same APIs used to manipulate the global proeprty tree (e.g. props.nas). And that is basically answering your question: The XML file posted in the wiki is incomplete - it would not make any sense at all without the embedded Nasal portions, but it contains a few redundant nodes that are not recognied by the existing GUI engine.

However, nasal/open block will specifically look for these redundant blocks and treat those as "templates" for certain widgets, e.g. those requiring dynamic contents (think a list of known styles, which cannot be hard-coded, think a list of MAP modes, ranges etc). Internally, the Nasal code will then locate a handful of useful blocks, instantiate (copy/rename, and customize them) and insert them into the appropriate locations in the dialog/property tree.

This is why we may have only a single combo/select or "button" element in the XML markup, despite having possibly dozens of buttons: The code gets a copy to the dialog's property tree, and then duplicates useful/required nodes and customizes/renames them.

What you have now found are leftovers from one of my debugging sessions - basically, all these strings are irrelevant placeholders, because they get overridden anyway. However, at some point, the code wasn't working properly, so that I added strings like "unchanged mode" to the XML - this was so that I could tell immediately which routine would fail replacing those placeholders, because that would show up prominently in a dump of the "procedurally created dialog". And that sums up the whole approach pretty well: The dialog only contains a subset of the required widgets/data, but it's using ~200 LOC to procedurally reuse those and replace those placeholders with more appropriate contents/variables.

Anyway, none of this should be relevant as long as you really only want to use the dialog. As a matter of fact, I don't think that there are many other dialogs making such elaborate use of this templating technique, but it is this that makes it possible to instantiate an arbitrary number of NDs using different resolutions - because what is really taking place is that the corresponding settings are written into the global property tree, and then the dialog is closed, the GUI subsystem reset and the same dialog opened, this time picking up the defaults you set up using the previously closed dialog.

I realize that this may sound complicated - but once you understand README.gui, README.layout and how embedded Nasal blocks work, and how those can manipulate the dialog before it is shown, it actually all makes sense, and isn't much unlike JavaScript/DOM traversal (akin to jQuery even).[2]

Goals

Primarily, this dialog is intended to help with:

  • regression testing
  • refactoring of the ND/PFD code
  • decoupling of styling related stuff and aircraft specific code (hard-coded assumptions)
  • integration with Richard's MFD framework
  • testing of independent instances
  • benchmarking/profiling and stress-testing the ND/Canvas code respectively
  • hardening the code

Roadmap

  • add buttons for controlling each ND instance (range, zoom, centering) 80}% completed
  • use io.include to directly reload the styles stuff (for rapid prototyping) (this will require changes to navdisplay.mfd which should be reviewed/tested by Gijs and Hyde) 60}% completed
  • procedurally add buttons for each switch by enhancing the myCockpit_switches hash with legends/labels for the UI use-case (use checkboxes for boolean props) Done Done
  • add a dropdown menu to select the position source (main aircraft vs. AI/MP traffic)
  • maybe allow the style to be selected/changed at runtime, possibly per ND ? 90}% completed
  • add some style specific stats to the dialog (SVG filename, number of elements, listeners, timers etc)
  • add VSD support [3]
  • make some more settings configurable (and persistent using the userarchive attribute):
    • number of NDs to be shown
    • resolution/size of the ND/Canvas
    • default style
    • settings previously used per ND

Ideas

  • add a fullscreen option ?
  • add a layout combo for table layouts (row X column)
  • add tooltips, and mouseover events for ND elements - e.g. name of the layer/symbol etc ?
  • add a mode to close the dialog and decouple the ND (no PUI overhead) ?
  • Use bind() to make io.include work within call()
  • Consider supporting an actual texture map (use one of the aircraft textures) to show a proper MCP instead of those checkboxes (ask Hyde/Gijs) ?
  • Use an outer/embedded Canvas for the UI ? (see the Garmin 196) [4]
  • allow multiple NDs to be rendered to a single canvas, which would help clean up all hard-coded assumptions about the size of the Canvas and the clipping region used Not done Not done

Optionally show some additional ND/style specific info:

  • SVG filename, font-mapper
  • number of features
  • number of MapStructure layers
  • active timers/listeners
  • total instances

Issues

Status

For now this is heavily based on ideas, and code, borrowed from the original MapStructure Debugger (code still available at [1])

The canvas widget is procedurally added to the dialog using the techniques discussed at Aircraft_Generation_Wizard#Under_the_Hood.

Base Package changes

Note  The following patch contains changes that move the definition of aircraft specific switches back into the navdisplay.styles file, adding GUI related fields to the hash, so that these can be used to procedurally create a UI without requiring an actual cockpit (including an optional values vector). In addition, this makes deletion of the Canvas by the ND optional, so that PUI based CanvasWidgets can reuse the ND without having to reallocate a new Canvas. Also, changes references to properly resolve to canvas.Path.
diff --git a/Nasal/canvas/map/navdisplay.mfd b/Nasal/canvas/map/navdisplay.mfd
index 89996f4..43080cb 100644
--- a/Nasal/canvas/map/navdisplay.mfd
+++ b/Nasal/canvas/map/navdisplay.mfd
@@ -60,33 +60,6 @@ NDSourceDriver.new = func {
 # To get started implementing your own ND, just copy the switches hash to your
 # ND.nas file and map the keys to your cockpit properties - and things will just work.
 
-# TODO: switches are ND specific, so move to the NDStyle hash!
-
-var default_switches = {
-	'toggle_range':        {path: '/inputs/range-nm', value:40, type:'INT'},
-	'toggle_weather':      {path: '/inputs/wxr', value:0, type:'BOOL'},
-	'toggle_airports':     {path: '/inputs/arpt', value:0, type:'BOOL'},
-	'toggle_stations':     {path: '/inputs/sta', value:0, type:'BOOL'},
-	'toggle_waypoints':    {path: '/inputs/wpt', value:0, type:'BOOL'},
-	'toggle_position':     {path: '/inputs/pos', value:0, type:'BOOL'},
-	'toggle_data':         {path: '/inputs/data',value:0, type:'BOOL'},
-	'toggle_terrain':      {path: '/inputs/terr',value:0, type:'BOOL'},
-	'toggle_traffic':      {path: '/inputs/tfc',value:0, type:'BOOL'},
-	'toggle_centered':     {path: '/inputs/nd-centered',value:0, type:'BOOL'},
-	'toggle_lh_vor_adf':   {path: '/inputs/lh-vor-adf',value:0, type:'INT'},
-	'toggle_rh_vor_adf':   {path: '/inputs/rh-vor-adf',value:0, type:'INT'},
-	'toggle_display_mode': {path: '/mfd/display-mode', value:'MAP', type:'STRING'}, # valid values are: APP, MAP, PLAN or VOR
-	'toggle_display_type': {path: '/mfd/display-type', value:'CRT', type:'STRING'}, # valid values are: CRT or LCD
-	'toggle_true_north':   {path: '/mfd/true-north', value:0, type:'BOOL'},
-	'toggle_rangearc':     {path: '/mfd/rangearc', value:0, type:'BOOL'},
-	'toggle_track_heading':{path: '/trk-selected', value:0, type:'BOOL'},
-	'toggle_weather_live': {path: '/mfd/wxr-live-enabled', value: 0, type: 'BOOL'},
-	'toggle_chrono': {path: '/inputs/CHRONO', value: 0, type: 'INT'},
-	'toggle_xtrk_error': {path: '/mfd/xtrk-error', value: 0, type: 'BOOL'},
-	'toggle_trk_line': {path: '/mfd/trk-line', value: 0, type: 'BOOL'},
-	'toggle_hdg_bug_only': {path: '/mfd/hdg-bug-only', value: 0, type: 'BOOL'},
-};
-
 ##
 # TODO:
 # - introduce a MFD class (use it also for PFD/EICAS)
@@ -96,20 +69,26 @@ var NavDisplay = {
 	# static
 	id:0,
 
-	del: func {
+	del: func(destroy_canvas=1) {
 		print("Cleaning up NavDisplay");
 		# shut down all timers and other loops here
 		me.update_timer.stop();
 		foreach(var t; me.timers)
 			t.stop();
-		foreach(var l; me.listeners)
-			removelistener(l);
+		foreach(var l; me.listeners) 
+			# removelistener(l);
+			call(removelistener, [l]);
 		# clean up MapStructure
 		me.map.del();
 		# call(canvas.Map.del, [], me.map);
-		# destroy the canvas
-		if (me.canvas_handle != nil)
+
+		me.nd.del(); # delete symbols (compass rose etc, needed in case we keep the underlying canvas)
+
+		# destroy the canvas (now optional, we may not always want to delete the whole canvas)
+		if (destroy_canvas and me.canvas_handle != nil) {
+			print("ND: destroying Canvas!");
 			me.canvas_handle.del();
+		}
 		me.inited = 0;
 		NavDisplay.id -= 1;
 	},
@@ -127,7 +106,7 @@ var NavDisplay = {
 	listen_switch: func(s,c) {
 		# print("event setup for: ", id(c));
 		if (!contains(me.efis_switches, s)) {
-			print('EFIS Switch not defined: '~ s);
+			print('cannot set up listener, EFIS Switch not defined in style/switches: '~ s);
 			return;
 		}
 		me.listen( me.get_full_switch_path(s), func {
@@ -178,14 +157,26 @@ var NavDisplay = {
 
 	# TODO: the ctor should allow customization, for different aircraft
 	# especially properties and SVG files/handles (747, 757, 777 etc)
-	new : func(prop1, switches=default_switches, style='Boeing') {
+	new : func(prop1, switches, style='Boeing') {
+
+		# if no custom switches specified, use default switches
+		if (switches==nil) {
+			print("ND: Using ND specific default switches");
+			switches = NDStyles[styles].default_switches;
+			}
+
 		NavDisplay.id +=1;
 		var m = { parents : [NavDisplay]};
 
-		var df_toggles = keys(default_switches);
+		m.nd_style = NDStyles[style]; # look up ND specific stuff (file names etc)
+
+		var df_toggles = keys(m.nd_style.default_switches);
+		print("ND specific default switches:", size(df_toggles));
 		foreach(var toggle_name; df_toggles){
-			if(!contains(switches, toggle_name))
-			switches[toggle_name] = default_switches[toggle_name];
+			if(!contains(switches, toggle_name)) {
+			print("Undefined ND switch, using default mapping for:", toggle_name);
+			switches[toggle_name] = m.nd_style.default_switches[toggle_name];
+			}
 		}
 		
 		m.inited = 0;
@@ -194,11 +185,11 @@ var NavDisplay = {
 		m.listeners=[]; # for cleanup handling
 		m.aircraft_source = NDSourceDriver.new(); # uses the main aircraft as the driver/source (speeds, position, heading)
 
-		m.nd_style = NDStyles[style]; # look up ND specific stuff (file names etc)
 		m.style_name = style;
 
 		m.radio_list=["instrumentation/comm/frequencies","instrumentation/comm[1]/frequencies",
 		              "instrumentation/nav/frequencies", "instrumentation/nav[1]/frequencies"];
+		# FIXME: this is redundant, must be moved to the style/Switches list
 		m.mfd_mode_list=["APP","VOR","MAP","PLAN"];
 
 		m.efis_path = prop1;
@@ -251,7 +242,7 @@ var NavDisplay = {
 		me.update_timer = maketimer(update_time, func me.update() );
 		me.nd = canvas_group;
 		me.canvas_handle = parent;
-		me.df_options = nil;
+		me.df_options = { map:{width:1024, height:1024} };
 		if (contains(me.nd_style, 'options'))
 			me.df_options = me.nd_style.options;
 		nd_options = canvas.default_hash(nd_options, me.df_options);
@@ -278,7 +269,8 @@ var NavDisplay = {
 
 		me.nd_style.initialize_elements(me);
 
-
+	
+		# var map_rect = [124, me.options.map.width, me.options.map.height, 0];
 		var map_rect = [124, 1024, 1024, 0];
 		var map_opts = me.options['map'];
 		if (map_opts == nil) map_opts = {};
@@ -582,7 +574,7 @@ var NavDisplay = {
 			me.map.setTranslation(trsl.x, trsl.y);
 		} else {
 			if(me.in_mode('toggle_display_mode', ['PLAN']))
-				me.map.setTranslation(512,512);
+				me.map.setTranslation(512,512); # FIXME use options hash to look up actual texture dimensions here
 			elsif(me.get_switch('toggle_centered'))
 				me.map.setTranslation(512,565);
 			else
diff --git a/Nasal/canvas/map/navdisplay.styles b/Nasal/canvas/map/navdisplay.styles
index c2196c8..6539d63 100644
--- a/Nasal/canvas/map/navdisplay.styles
+++ b/Nasal/canvas/map/navdisplay.styles
@@ -29,6 +29,37 @@ var NDStyles = {
 				return "LiberationFonts/LiberationSans-Regular.ttf";
 		},
 
+		# to be used for validating new/modified styles, this can be used to encode a list of switches that 
+		# should be configured by the aircraft developer, lest the ND may not work completely
+
+# switches are ND specific, TODO: use UI version with legends/labels
+
+default_switches: {
+    # symbolic alias : GUI legend/tooltip, relative property (as used in bindings), initial value, valid values (vector), property type
+    # TODO: should support a vector of valid_values() (ranges)
+    'toggle_range':         {legend:'rng', path: '/inputs/range-nm', value:40, values:[10,20,40,80,160,320], type:'INT'},
+    'toggle_weather':       {legend:'wxr', path: '/inputs/wxr', value:0, type:'BOOL'},
+    'toggle_airports':      {legend:'apt', path: '/inputs/arpt', value:0, type:'BOOL'},
+    'toggle_stations':      {legend:'sta', path: '/inputs/sta', value:0, type:'BOOL'},
+    'toggle_waypoints':     {legend:'wpt', path: '/inputs/wpt', value:0, type:'BOOL'},
+    'toggle_position':      {legend:'pos', path: '/inputs/pos', value:0, type:'BOOL'},
+    'toggle_data':          {legend:'dat', path: '/inputs/data',value:0, type:'BOOL'},
+    'toggle_terrain':       {legend:'terr', path: '/inputs/terr',value:0, type:'BOOL'},
+    'toggle_traffic':       {legend:'tfc', path: '/inputs/tfc',value:0, type:'BOOL'},
+    'toggle_centered':      {legend:'ctr', path: '/inputs/nd-centered',value:0, type:'BOOL'},
+    'toggle_lh_vor_adf':    {legend:'vor/adf (l)', path: '/inputs/lh-vor-adf',value:0, values:{'VOR':1, 'OFF':0, 'ADF':-1 }, type:'INT'},
+    'toggle_rh_vor_adf':    {legend:'vor/adf (r)', path: '/inputs/rh-vor-adf',value:0, values:{'VOR':1, 'OFF':0, 'ADF':-1 }, type:'INT'},
+    'toggle_display_mode':  {legend:'map',path: '/mfd/display-mode', value:'MAP', values:['APP', 'MAP', 'PLAN', 'VOR' ], type:'STRING'},
+    'toggle_display_type':  {legend:'lcd',path: '/mfd/display-type', value:'LCD', values:['CRT', 'LCD' ], type:'STRING'},
+    'toggle_true_north':    {legend:'tru',path: '/mfd/true-north', value:0, type:'BOOL'},
+    'toggle_rangearc':      {legend:'rng',path: '/mfd/rangearc', value:0, type:'BOOL'},
+    'toggle_track_heading': {legend:'trk',path: '/hdg-trk-selected', value:0, type:'BOOL'},
+    'toggle_hdg_bug_only':  {legend:'hdg',path: '/hdg-bug-only', value:0, type:'BOOL'},
+    # add any new switches here (and update navdisplay.styles as needed)
+ 
+}, # end of aircraft-specific default switches
+
+
 		# where all the symbols are stored
 		# TODO: SVG elements should be renamed to use boeing/airbus prefix
 		# aircraft developers should all be editing the same ND.svg image
@@ -52,7 +83,7 @@ var NDStyles = {
 		                      "HdgBugCRT2","TrkBugLCD2","HdgBugLCD2","hdgBug2","selHdgLine","selHdgLine2","curHdgPtr2",
 		                      "staArrowL","staArrowR","staToL","staFromL","staToR","staFromR"] )
 			me.symbols[element] = me.nd.getElementById(element).updateCenter();
-
+	
 		}, # initialize_elements
 
 		##
@@ -1147,6 +1178,43 @@ var NDStyles = {
 				if( family == "Liberation Sans" and weight == "normal" )
 						return "LiberationFonts/LiberationSans-Regular.ttf";
 			},
+# switches are ND specific, TODO: see actual Airbus ND.nas (artix/omega95)
+
+# switches are ND specific, TODO: use UI version with legends/labels
+# https://github.com/artix75/A320neo/blob/master/Models/Instruments/ND/canvas/ND.nas#L16
+
+default_switches: {
+		# symbolic alias : relative property (as used in bindings), initial value, type
+		'toggle_range': 	{legend: "range", path: '/inputs/range-nm', value:40, values:[10,20,40,80,160,320],type:'INT'},
+		'toggle_weather': 	{legend: "wxr", path: '/inputs/wxr', value:0, type:'BOOL'},
+		'toggle_airports': 	{legend: "apt", path: '/inputs/arpt', value:0, type:'BOOL'},
+		'toggle_ndb': 	{legend: "ndb", path: '/inputs/NDB', value:0, type:'BOOL'},
+		'toggle_stations':     {legend: "sta", path: '/inputs/sta', value:0, type:'BOOL'},
+		'toggle_vor': 	{legend:"vor", path: '/inputs/VORD', value:0, type:'BOOL'},
+		'toggle_dme': 	{legend: "dme", path: '/inputs/DME', value:0, type:'BOOL'},
+		'toggle_cstr': 	{legend: "cstr", path: '/inputs/CSTR', value:0, type:'BOOL'},
+		'toggle_waypoints': 	{legend:"wpt", path: '/inputs/wpt', value:0, type:'BOOL'},
+		'toggle_position': 	{legend:"pos", path: '/inputs/pos', value:0, type:'BOOL'},
+		'toggle_data': 		{legend:"dat", path: '/inputs/data',value:0, type:'BOOL'},
+		'toggle_terrain': 	{legend:"terr", path: '/inputs/terr',value:0, type:'BOOL'},
+		'toggle_traffic': 	{legend:"tfc",path: '/inputs/tfc',value:0, type:'BOOL'},
+		'toggle_centered': 		{legend:"ctr",path: '/inputs/nd-centered',value:0, type:'BOOL'},
+		'toggle_lh_vor_adf':	{legend:"vor/adf(l)",path: '/input/lh-vor-adf',value:0, values:{'VOR':1, 'OFF':0, 'ADF':-1 }, type:'INT'},
+		'toggle_rh_vor_adf':	{legend:"vor/adf(r)", path: '/input/rh-vor-adf',value:0,values:{'VOR':1, 'OFF':0, 'ADF':-1 }, type:'INT'},
+		# FIXME: this should be reviewed by one of the airbus developers, not sure if these values make sense for the Airbus style (will only affect the GUI dialog though)
+		'toggle_display_mode': 	{legend: "mode", path: '/nd/canvas-display-mode', value:'NAV', values:['NAV','APP','MAP','VOR','PLAN'],type:'STRING'},
+		'toggle_display_type': 	{legend: "LCD/CRT", path: '/mfd/display-type', value:'LCD', values:['LCD','CRT'],type:'STRING'},
+		'toggle_true_north': 	{legend: "tru", path: '/mfd/true-north', value:1, type:'BOOL'},
+		'toggle_track_heading': 	{legend:"trk", path: '/trk-selected', value:0, type:'BOOL'},
+		'toggle_wpt_idx': {legend:"wpt idx", path: '/inputs/plan-wpt-index', value: -1,type: 'INT'},
+		'toggle_plan_loop': {legend:"plan loop", path: '/nd/plan-mode-loop', value: 0,type: 'INT'},
+		'toggle_weather_live': {legend: "live wxr", path: '/mfd/wxr-live-enabled', value: 0, type: 'BOOL'},
+		'toggle_chrono': {legend:"chrono", path: '/inputs/CHRONO', value: 0, values:[],type: 'INT'},
+		'toggle_xtrk_error': {legend: "xte", path: '/mfd/xtrk-error', value: 0,type: 'BOOL'},
+		'toggle_trk_line': {legend:"trk line", path: '/mfd/trk-line', value: 0, type: 'BOOL'},
+		# add new switches here
+}, # end of aircraft-specific default switches
+
 
 			# where all the symbols are stored
 			# TODO: SVG elements should be renamed to use boeing/airbus prefix
@@ -1171,7 +1239,7 @@ var NDStyles = {
 		                      "HdgBugCRT2","TrkBugLCD2","HdgBugLCD2","hdgBug2","selHdgLine","selHdgLine2","curHdgPtr2",
 		                      "staArrowL","staArrowR","staToL","staFromL","staToR","staFromR"] )
 			me.symbols[element] = me.nd.getElementById(element).updateCenter();
-
+	
 		}, # initialize_elements
 
 
@@ -1530,9 +1598,9 @@ var NDStyles = {
 								var adf1_frq = getprop(me.options.adf1_frq);
 								var adf2_frq = getprop(me.options.adf2_frq);
 								if(adf1_frq == frq or adf2_frq == frq){
-									me.element.setColor(tuned_color, [Path]);
+									me.element.setColor(tuned_color, [canvas.Path]);
 								} else {
-									me.element.setColor(dfcolor, [Path]);
+									me.element.setColor(dfcolor, [canvas.Path]);
 								}
 							}
 						},
Note  navdisplay.mfd around line 270, the clip options must be embedded into 'rect()'
me.map = me.nd.createChild("map","map")
.set("clip", "rect("~map_rect~")")
.set("screen-range", 700);

Menubar

Screenshot showing PUI menubar with added Canvas NavDisplay entry for the Canvas ND dialog.
diff --git a/Translations/en/menu.xml b/Translations/en/menu.xml
index a5411d3..7144ba8 100644
--- a/Translations/en/menu.xml
+++ b/Translations/en/menu.xml
@@ -66,6 +66,7 @@
        <random-failures>Random Failures</random-failures>
        <system-failures>System Failures</system-failures>
        <instrument-failures>Instrument Failures</instrument-failures>
+       <canvas-nd>Canvas NavDisplay</canvas-nd>
 
        <!-- AI menu -->
        <ai>AI</ai>

diff --git a/gui/menubar.xml b/gui/menubar.xml
index afb7ff3..45803e5 100644
--- a/gui/menubar.xml
+++ b/gui/menubar.xml
@@ -412,6 +412,15 @@
                </item>
 
                <item>
+                       <name>canvas-nd</name>
+                       <binding>
+                               <command>dialog-show</command>
+                               <dialog-name>canvas-nd</dialog-name>
+                       </binding>
+               </item>
+
+
+               <item>
                        <name>failure-submenu</name>
                        <enabled>false</enabled>

Code

Note  The following code will no longer work without the changes shown above, if in doubt, see the history of this article for the previous version. For testing purposes, put the following dialog into $FG_ROOT/gui/dialogs/canvas-nd.xml and use the Nasal Console to run the dialog (or extend the Menubar accordingly):
fgcommand('dialog-show', props.Node.new({'dialog-name':'canvas-nd'}) );
Note  The following can be split into a XML file and a nasal file - makes editing and syntax highlighting easier. However, variables used in the close block must be declared in the open block before the include of the nasal file.
<?xml version="1.0"?>
<!--

 GNU GPL v2.0 (c) FlightGear.org 2016
 For details, see: http://wiki.flightgear.org/Howto:Prototyping_a_new_NavDisplay_Style

-->
<PropertyList>
  <name>canvas-nd</name>
  <modal>false</modal>
  <layout>vbox</layout>
  <text>
    <label>Canvas NavDisplay</label>
  </text>

  <widget-templates>
  <!--
  The following markup is processed by the dialog's nasal/open block to instantiate independent ND instances, including widgets to control them
  -->
<nd-checkbox>
<name>checkbox-template</name>

<checkbox>
<pref-width>40</pref-width>
<pref-height>22</pref-height>

  <label>nd-checkbox</label>
  <halign>left</halign>
  <property></property>
  <live>true</live>

  <binding>
  <command>dialog-apply</command>
  </binding>
<!--
  <binding>
  <command>dialog-toggle</command>
  </binding>
-->
</checkbox>

</nd-checkbox>

    <canvas-widget>
      <name>canvas-mfd</name>

<!--
      <group>
      <layout>vbox</layout>
      <name>mfd-controls2</name>

<checkbox>
<pref-width>30</pref-width>
<pref-height>22</pref-height>

  <label>Test</label>
  <halign>left</halign>
  <property>/sim/none/foo</property>
</checkbox>


      </group>
-->

      <frame>
      <layout>vbox</layout>
      <padding>15</padding>
      <stretch>true</stretch>

  <!-- style selector (property will be overridden) -->
  <combo>
   <label>Style</label>
   <name>unchanged</name>
   <property>WILL BE FILLED IN PROCEDURALLY</property>

   <binding>
    <command>dialog-apply</command>
    <object-name>unchanged</object-name>
   </binding>
  
<!-- This actually works but triggers a Canvas related segfault if we don't use the timer -->
    <binding>
        <command>nasal</command>
        <script>
          fgcommand("dialog-close", props.Node.new({"dialog-name": "canvas-nd"}));
	  fgcommand("reinit", props.Node.new({subsystem:'gui'}));
          settimer(func fgcommand("dialog-show", props.Node.new({"dialog-name": "canvas-nd"})), 0);
        </script>
      </binding>

  </combo>


      <!-- this is where our checkboxes end up TODO: should be moved somewhere else and become a vbox-->
      <group>
      <layout>hbox</layout>
      <padding>5</padding>
      <name>mfd-controls</name>
      </group>

      <canvas>
		    <name></name>
		    <!--
		    <valign>fill</valign>
                    <halign>fill</halign>
                    <stretch>false</stretch>
		    -->
		    <!-- reducing the dimensions even more will look weird due to layouting issues, given the number of hbox aligned checkbox widgets -->
                    <pref-width>256</pref-width>
                    <pref-height>256</pref-height>
		    <view>1024</view>
		    <view>1024</view>
      <nasal>      
        <load>
       </load>
      </nasal>
    </canvas>
    <!-- ND/Canvas specific controls placed at the bottom of the display (e.g. range) -->
    <group> <!-- move this down for better layout -->
    <layout>hbox</layout>
  
 <combo>
   <label>Mode</label>
   <name>unchanged mode!!!</name>
   <property>WILL BE FILLED IN PROCEDURALLY</property>

   <binding>
      <command>dialog-apply</command>
      <object-name>unchanged mode !!!</object-name>
   </binding>   
  </combo>
  
<combo>
   <label></label>
   <name></name>
   <property></property>
   <binding>
      <command>dialog-apply</command>
      <object-name></object-name>
      </binding>   
  </combo>

    <combo>
      <name>unchanged range</name>
      <label>nm</label>
      <property>WILL BE FILLED IN PROCEDURALLY</property>
    <binding>
      <command>dialog-apply</command>
      <object-name>unchanged range</object-name>
    </binding>   
    </combo>

  <combo>
   <label></label>
   <name></name>
   <property></property>
   <binding>
      <command>dialog-apply</command>
      <object-name></object-name>
      </binding>   
  </combo>

    </group>
    </frame>

    </canvas-widget>

  </widget-templates>
  <nasal>
  <open><![CDATA[
	# to be used for shutting down each created instance upon closing the dialog (see the close block below)
	# variable not visible in close block, if defined in included nasal file, so defined here
	var MFDInstances = {};
	#io.include("canvas-nd-dlg-open.nas");
### optional: move everything below this line up to end of CDATA to a separate nas file and include it (uncomment the line above)
var getWidgetTemplate = func(root, identifier) {
var target = globals.gui.findElementByName(root,  identifier );
if(target == nil) die("Target node not found for identifier:"~identifier);
return target;
}

var populateSelectWidget = func(widget, label, attribute, index, property, values)  {
# make up an identifier to be used for the object-name (fgcommands dialog-apply and -update)
# format: ND[x].attribute
var objectName = "ND["~index~"]."~attribute; # attribute~index;

widget.getNode("label",1).setValue(label);
widget.getNode("name",1).setValue(objectName);
widget.getNode("binding/object-name",1).setValue(objectName);

var list = nil;

if (typeof(values) == 'hash') {
# we have a hash with key/value pairs
list = keys(values);
print("FIXME: key/value mapping missing for value hash map:", attribute);
# widget.getNode("property",1).setValue(property~"value-to-key");

}
else {
list = values;
widget.getNode("property",1).setValue(property);
}
# for vectors with values
forindex(var c; list) {
 widget.getChild("value",c,1).setValue(list[c]);
}
};

###
# locate required templates
var WidgetTemplates = {};

# populate a hash with templates that we will need later on
foreach(var t; ['canvas-placeholder', 'canvas-mfd', 'checkbox-template']) {
WidgetTemplates[t]=getWidgetTemplate(root:cmdarg(), identifier: t);
#print("Dump:\n");
#props.dump( WidgetTemplates[t] );
}

var initialize_nd = func(index) {
		   var my_canvas = canvas.get( cmdarg() );

		   # FIXME: use the proper root here
		   var myND = setupND(mfd_root: "/instrumentation/efis["~index~"]", my_canvas: my_canvas, index:index);
};

# not currently used, but could be used for validating the mfd/styles files before using them
var errors = [];

# NOTE: this requires changes to navdisplay.mfd (see the wiki article for details)
# call(func[, args[, me[, locals[, error]]]);
# call(
# actually include the navdisplay.mfd file so that this gets reloaded whenever the dialog is closed/reopened
io.include('Nasal/canvas/map/navdisplay.mfd');
#, nil, closure(initialize_nd), var errors=[]);

if (size(errors)) {
canvas.MessageBox.critical(
  "$FG_ROOT/Nasal/canvas/map/navdisplay.mfd",
  "Error reloading navdisplay.mfd and/or navdisplay.styles:\n",
  cb = nil,
  buttons = canvas.MessageBox.Ok
);
# TODO: close dialog on error
}

var ND = NavDisplay;

# TODO: this info could also be added to the GUI dialog
print("Number of ND Styles found:", size(keys(NDStyles)));

# to be used for shutting down each created instance upon closing the dialog (see the close block below)
var MFDInstances = {};

# http://wiki.flightgear.org/Canvas_ND_Framework#Cockpit_switches
var resolve_adf_vor_mode = func(num) {
if (num == -1) return 'ADF';
if (num == 1) return 'VOR';
return 'OFF';
}

var getSwitchesForND = func(index) {
   
   var style_property = "/fgcanvas/nd["~index~"]/selected-style";
   var style = getprop(style_property);

   # make sure that the style  is exposed via the property tree
   if (style == nil) {
	print("Ooops, style was undefined, using default");
	style = 'Boeing'; # our default style
	setprop(style_property, style);
	}

   var switches = NDStyles[style].default_switches;
   # print("Using ND style/switches:", style);

   if (switches == nil) print("Unknown ND style: ", style);
   return switches;

}

var setupND = func(mfd_root, my_canvas, index) {

   var style = getprop("/fgcanvas/nd["~index~"]/selected-style") or 'Boeing';

    ##
    # set up a  new ND instance, under mfd_root and use the
    # myCockpit_switches hash to map ND specific control properties
    var switches = getSwitchesForND(index);
    var myND= ND.new(mfd_root, switches, style);
    var group = my_canvas.createGroup();
    myND.newMFD(group, my_canvas);
    myND.update();
    # store the new instance for later cleanup
    var handle = "ND["~index~"]";
    MFDInstances[handle] = myND; 
    # return {nd: myND, property_root: mfd_root};
} # setupND()

# this determines how many NDs will be added to the dialog, and where their controls live in the property tree
# TODO: support default style per ND, different dimensions ?
# persistent config/profiles
var canvas_areas = [
	{name: 'captain.ND', property_root:'/instrumentation/efis[0]',},
	#{name: 'copilot.ND', property_root:'/instrumentation/efis[1]',},
	# you can add more entries below, for example: 
        #{name: 'engineer.ND', property_root:'/instrumentation/efis[2]',},
];

# procedurally add one canvas for each ND to be shown (requires less code/maintenance, aka DRY)

var totalNDs = getprop("/fgcanvas/total-nd-instances") or 1;

# var index=0;
for(var index=0;index<totalNDs;index+=1) {
var c = {name: 'ND #'~index, property_root:'/instrumentation/efis['~index~']'};
print("Setting up ND:", c.name);
# foreach(var c; canvas_areas) {
var switches = getSwitchesForND(index);

# next, create a new symbol named canvasWidget, create child in target, with the index specified (idx)
var canvasWidget = WidgetTemplates['canvas-placeholder'].getChild("frame", index, 1);
# now, copy our template stuff into the new tree 
props.copy(WidgetTemplates['canvas-mfd'].getChild("frame"), canvasWidget);

# customize the subtree and override a few things
canvasWidget.getNode("text/label",1).setValue(c.name);
canvasWidget.getNode("canvas/name").setValue(c.name);

var r = getprop("/fgcanvas/nd-resolution") or 420;

canvasWidget.getNode("canvas/pref-width").setValue(r);
canvasWidget.getNode("canvas/pref-height").setValue(r);

# instantiate and populate combo widgets
var selectWidgets= [
	{node: 'combo', label:'Style', attribute: 'Style', property:'/fgcanvas/nd['~index~']/selected-style', values:keys(NDStyles) },
	{node: 'group[1]/combo[2]', label:'nm', attribute: 'RangeNm', property:c.property_root~switches['toggle_range'].path, values:switches['toggle_range'].values },
	{node: 'group[1]/combo', label:'', attribute: 'ndMode', property:c.property_root~switches['toggle_display_mode'].path, values:switches['toggle_display_mode'].values },
];

foreach(var s; selectWidgets) {
 populateSelectWidget(canvasWidget.getNode(s.node), s.label, s.attribute, index, s.property, s.values);  
}

# add a single line of code to each canvas/nasal section setting up the ND instance 
canvasWidget.getNode("canvas/nasal/load").setValue("initialize_nd(index:"~index~");");

# --------------- This whole thing can be simplified by putting it into the previous foreach loop
# TODO: this should be using VOR/OFF/ADF instead of the numerical values ...
var leftVORADFSelector = canvasWidget.getNode("group[1]/combo[1]");

# FIXME: shouldn't hard-code this here ...
var keyValueMap = [1,0,-1]; # switches['toggle_lh_vor_adf'].values; 

# FIXME look up the proper lh/rh values here
# and use the proper root 
populateSelectWidget(leftVORADFSelector, "", "VOR/ADF(l)", index, "/instrumentation/efis["~index~"]/inputs/lh-vor-adf", keyValueMap);  
var rightVORADFSelector = canvasWidget.getNode("group[1]/combo[3]");
populateSelectWidget(rightVORADFSelector, "", "VOR/ADF(r)",index, "/instrumentation/efis["~index~"]/inputs/rh-vor-adf", keyValueMap);  
# ---------------

var checkboxArea = getWidgetTemplate(root:canvasWidget, identifier:'mfd-controls'); 
var cb_index = 0;
# add checkboxes for each boolean switch

# HACK: get rid of this, it's just an alias for now
var myCockpit_switches = getSwitchesForND(index); # FIXME: should be using index instead of 0
foreach(var s; keys(myCockpit_switches)) {
var switch = s;
if (myCockpit_switches[switch].type != 'BOOL') continue; # skip non boolean switches for now 

var checkbox = checkboxArea.getChild("checkbox",cb_index, 1);
props.copy(WidgetTemplates['checkbox-template'].getChild("checkbox"), checkbox);
cb_index+=1;
checkbox.getNode("label").setValue(myCockpit_switches[switch].legend);
checkbox.getNode("property").setValue(c.property_root ~ myCockpit_switches[switch].path);
# FIXME: use notation ND[x].attribute.toggle
var object_name = "checkbox["~cb_index~"]("~myCockpit_switches[switch].legend~")";
checkbox.getNode("name",1).setValue(object_name);
checkbox.getNode("binding/object-name",1).setValue(object_name);

} # add checkboxes for each boolean ND switch

# index += 1;
} # foreach ND instance

#print("Complete dialog is:");
#props.dump( WidgetTemplates['canvas-placeholder'] );

### end of nasal code to be moved

  ]]>
  </open>

  <close><![CDATA[
	print("invoking nasal/close block canvas-nd.xml");
	foreach(var mfd; keys(MFDInstances) ) {
		MFDInstances[mfd].del();
	}
  ]]>
  </close>

  </nasal>
 
    <group>
    <layout>hbox</layout>
    <name>canvas-placeholder</name>
    <!-- this will be populated dynamically when the dialog is opened -->
    </group>

<group>
<layout>hbox</layout>
    <button>
      <legend>Exit</legend>
      <equal>true</equal>
      <key>Esc</key>
      <binding>
        <command>dialog-close</command>
      </binding>
    </button>

 <combo>
   <label>Instances</label>
   <name>TotalNDInstances</name>
   <property>/fgcanvas/total-nd-instances</property>

   <value>1</value>
   <value>2</value>
   <value>3</value>
   <value>4</value>
   <value>5</value>

   <binding>
      <command>dialog-apply</command>
      <object-name>TotalNDInstances</object-name>
   </binding>   

  <binding>
        <command>nasal</command>
        <script>
          fgcommand("dialog-close", props.Node.new({"dialog-name": "canvas-nd"}));
	  fgcommand("reinit", props.Node.new({subsystem:'gui'}));
          settimer(func fgcommand("dialog-show", props.Node.new({"dialog-name": "canvas-nd"})), 0);
        </script>
      </binding>



  </combo>
 
  <combo>
   <label>Size</label>
   <name>NDResolution</name>
   <property>/fgcanvas/nd-resolution</property>

   <value>256</value>
   <value>384</value>
   <value>512</value>
   <value>768</value>

   <binding>
      <command>dialog-apply</command>
      <object-name>NDResolution</object-name>
   </binding>   

    <binding>
        <command>nasal</command>
        <script>
          fgcommand("dialog-close", props.Node.new({"dialog-name": "canvas-nd"}));
	  fgcommand("reinit", props.Node.new({subsystem:'gui'}));
          settimer(func fgcommand("dialog-show", props.Node.new({"dialog-name": "canvas-nd"})), 0);
        </script>
      </binding>
  </combo>
 


</group>

</PropertyList>

References

References