Howto:Prototyping a new NavDisplay Style

From FlightGear wiki
Revision as of 19:30, 22 October 2016 by Hooray (talk | contribs) (→‎Approach)
Jump to navigation Jump to search
This article is a stub. You can help the wiki by expanding it.
Screenshot showing a PUI dialog with two embedded Canvas regions for displaying two independent NavDisplay instances.

Objective

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.

Approach

Screenshot showing two independent NavDisplay instances rendered in a PUI widget via Canvas
PUI dialog with 3 embedded NavDisplay instances controlled via checkboxes (i.e. no property browser needed)
Screenshot showing a PUI dialog with 3 NDs and different settings applied for each instance
ND dialog with WXR layer shown
ND dialog showing the Airbus style of the NavDisplay module
PUI dialog showing 3 independent NavDisplay instances with styling being customizable at runtime.
PUI dialog showing two independent NavDisplay instances with different styles applied and support for toggling display modes separately
Updated PUI/XML dialog showing different ND styles in use.

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

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 ? 30}% completed
  • add VSD support

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. Not done Not done

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 mainly take care of two things:
  • properly referencing the canvas namespace, so that the ND module can be separately included via io.include()
  • moving the initialization of aircraft specific SVG elements into the navdisplay.styles file, and the corresponding Boeing/Airbus entries there (see initialize_elements()

As of 10/2016, these should be included of the base package, i.e. available in 2016.4 [1])

diff --git a/Nasal/canvas/api.nas b/Nasal/canvas/api.nas
index ea67217..8fd77e5 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 d2d2ef5..f811f16 100644
--- a/Nasal/canvas/map/navdisplay.mfd
+++ b/Nasal/canvas/map/navdisplay.mfd
@@ -106,6 +106,7 @@ var NavDisplay = {
 			removelistener(l);
 		# clean up MapStructure
 		me.map.del();
+		# call(canvas.Map.del, [], me.map);
 		# destroy the canvas
 		if (me.canvas_handle != nil)
 			me.canvas_handle.del();
@@ -253,7 +254,7 @@ var NavDisplay = {
 		me.df_options = nil;
 		if (contains(me.nd_style, 'options'))
 			me.df_options = me.nd_style.options;
-		nd_options = default_hash(nd_options, me.df_options);
+		nd_options = canvas.default_hash(nd_options, me.df_options);
 		me.options = nd_options;
 		me.route_driver = nil;
 		if (me.options == nil) me.options = {};
@@ -275,22 +276,9 @@ var NavDisplay = {
 			if(contains(feature.impl,'init')) feature.impl.init(me.nd, feature); # call The element's init code (i.e. updateCenter)
 		}
 
-		### this is the "old" method that's less flexible, we want to use the style hash instead (see above)
-		# because things are much better configurable that way
-		# now look up all required SVG elements and initialize member fields using the same name  to have a convenient handle
-		foreach(var element; ["dmeLDist","dmeRDist","dmeL","dmeR","vorL","vorR","vorLId","vorRId",
-		                      "status.wxr","status.wpt","status.sta","status.arpt"])
-			me.symbols[element] = me.nd.getElementById(element);
+		me.nd_style.initialize_elements(me);
 
-		# load elements from vector image, and create instance variables using identical names, and call updateCenter() on each
-		# anything that needs updatecenter called, should be added to the vector here
-		#
-		foreach(var element; ["staArrowL2","staArrowR2","staFromL2","staToL2","staFromR2","staToR2",
-		                      "hdgTrk","trkInd","hdgBug","HdgBugCRT","TrkBugLCD","HdgBugLCD","curHdgPtr",
-		                      "HdgBugCRT2","TrkBugLCD2","HdgBugLCD2","hdgBug2","selHdgLine","selHdgLine2","curHdgPtr2",
-		                      "staArrowL","staArrowR","staToL","staFromL","staToR","staFromR"] )
-			me.symbols[element] = me.nd.getElementById(element).updateCenter();
-		
+	
 		var map_rect = [124, 1024, 1024, 0];
 		var map_opts = me.options['map'];
 		if (map_opts == nil) map_opts = {};
@@ -342,6 +330,7 @@ var NavDisplay = {
 			get_tuned_course:get_course_by_freq,
 			get_position: get_current_position,
 			new: func(map) return { parents:[controller], map:map },
+			del: func() {print("cleaning up nd controller");},
 			should_update_all: func {
 				# TODO: this is just copied from aircraftpos.controller,
 				# it really should be moved to somewhere common and reused
@@ -386,12 +375,12 @@ var NavDisplay = {
 			if(!layer['isMapStructure']) # set up an old INEFFICIENT and SLOW layer
 				the_layer = me.layers[layer.name] = canvas.MAP_LAYERS[layer.name].new( me.map, layer.name, controller );
 			else {
-				printlog(_MP_dbg_lvl, "Setting up MapStructure-based layer for ND, name:", layer.name);
+				printlog(canvas._MP_dbg_lvl, "Setting up MapStructure-based layer for ND, name:", layer.name);
 				var opt = me.options != nil and me.options[layer.name] != nil ? me.options[layer.name] : nil;
 				if (opt == nil and contains(layer, 'options'))
 					opt = layer.options;
 				if (opt != nil and default_opts != nil)
-					opt = default_hash(opt, default_opts);
+					opt = canvas.default_hash(opt, default_opts);
 				#elsif(default_opts != nil)
 				#    opt = default_opts;
 				var style = nil;
@@ -420,7 +409,7 @@ var NavDisplay = {
 					l.predicate = func {
 						var t = systime();
 						call(_predicate, arg, me);
-						printlog(_MP_dbg_lvl, "Took "~((systime()-t)*1000)~"ms to update layer "~l.name);
+						printlog(canvas._MP_dbg_lvl, "Took "~((systime()-t)*1000)~"ms to update layer "~l.name);
 					}
 				})();
 			}
@@ -805,7 +794,7 @@ var NavDisplay = {
 		me.symbols['status.arpt'].setVisible( me.get_switch('toggle_airports') and me.in_mode('toggle_display_mode', ['MAP']));
 		me.symbols['status.sta'].setVisible( me.get_switch('toggle_stations') and  me.in_mode('toggle_display_mode', ['MAP']));
 		# Okay, _how_ do we hook this up with FGPlot?
-		printlog(_MP_dbg_lvl, "Total ND update took "~((systime()-_time)*100)~"ms");
+		printlog(canvas._MP_dbg_lvl, "Total ND update took "~((systime()-_time)*100)~"ms");
 		setprop("/instrumentation/navdisplay["~ NavDisplay.id ~"]/update-ms", systime() - _time);
 	} # of update() method (50% of our file ...seriously?)
 };
diff --git a/Nasal/canvas/map/navdisplay.styles b/Nasal/canvas/map/navdisplay.styles
index 7712727..52751a2 100644
--- a/Nasal/canvas/map/navdisplay.styles
+++ b/Nasal/canvas/map/navdisplay.styles
@@ -34,6 +34,27 @@ var NDStyles = {
 		# aircraft developers should all be editing the same ND.svg image
 		# the code can deal with the differences now
 		svg_filename: "Nasal/canvas/map/Images/boeingND.svg",
+
+		initialize_elements: func(me) {
+
+		### this is the "old" method that's less flexible, we want to use the style hash instead (see above)
+		# because things are much better configurable that way
+		# now look up all required SVG elements and initialize member fields using the same name  to have a convenient handle
+		foreach(var element; ["dmeLDist","dmeRDist","dmeL","dmeR","vorL","vorR","vorLId","vorRId",
+		                      "status.wxr","status.wpt","status.sta","status.arpt"])
+			me.symbols[element] = me.nd.getElementById(element);
+
+		# load elements from vector image, and create instance variables using identical names, and call updateCenter() on each
+		# anything that needs updatecenter called, should be added to the vector here
+		#
+		foreach(var element; ["staArrowL2","staArrowR2","staFromL2","staToL2","staFromR2","staToR2",
+		                      "hdgTrk","trkInd","hdgBug","HdgBugCRT","TrkBugLCD","HdgBugLCD","curHdgPtr",
+		                      "HdgBugCRT2","TrkBugLCD2","HdgBugLCD2","hdgBug2","selHdgLine","selHdgLine2","curHdgPtr2",
+		                      "staArrowL","staArrowR","staToL","staFromL","staToR","staFromR"] )
+			me.symbols[element] = me.nd.getElementById(element).updateCenter();
+	
+		}, # initialize_elements
+
 		##
 		## this loads and configures existing layers (currently, *.layer files in Nasal/canvas/map)
 		##
@@ -1132,6 +1153,29 @@ var NDStyles = {
 			# aircraft developers should all be editing the same ND.svg image
 			# the code can deal with the differences now
 			svg_filename: "Nasal/canvas/map/Airbus/Images/airbusND.svg",
+
+		initialize_elements: func(me) {
+
+		### this is the "old" method that's less flexible, we want to use the style hash instead (see above)
+		# because things are much better configurable that way
+		# now look up all required SVG elements and initialize member fields using the same name  to have a convenient handle
+		foreach(var element; ["dmeLDist","dmeRDist","dmeL","dmeR","vorL","vorR","vorLId","vorRId",
+		                      "status.wxr","status.wpt","status.sta","status.arpt"])
+			me.symbols[element] = me.nd.getElementById(element);
+
+		# load elements from vector image, and create instance variables using identical names, and call updateCenter() on each
+		# anything that needs updatecenter called, should be added to the vector here
+		#
+		foreach(var element; ["staArrowL2","staArrowR2","staFromL2","staToL2","staFromR2","staToR2",
+		                      "hdgTrk","trkInd","hdgBug","HdgBugCRT","TrkBugLCD","HdgBugLCD","curHdgPtr",
+		                      "HdgBugCRT2","TrkBugLCD2","HdgBugLCD2","hdgBug2","selHdgLine","selHdgLine2","curHdgPtr2",
+		                      "staArrowL","staArrowR","staToL","staFromL","staToR","staFromR"] )
+			me.symbols[element] = me.nd.getElementById(element).updateCenter();
+	
+		}, # initialize_elements
+
+
+
 			##
 			## this loads and configures existing layers (currently, *.layer files in Nasal/canvas/map)
 			##

Code

Note  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'}) );
<?xml version="1.0"?><?xml version="1.0"?>
<PropertyList>
  <name>canvas-nd</name>
  <modal>false</modal>
  <layout>vbox</layout>
  <text>
    <label>Canvas ND</label>
  </text>

  <widget-templates>

<nd-checkbox>
<name>checkbox-template</name>

<checkbox>
<pref-width>30</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>property-toggle</command>
 <property>this will be customized</property>
</binding>
-->

</checkbox>

</nd-checkbox>

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

      <group>
      <layout>vbox</layout>
      <group>
      <layout>hbox</layout>
      <name>mfd-controls</name>
      </group>
      <canvas>
		    <name></name>
		    <valign>fill</valign>
                    <halign>fill</halign>
                    <stretch>true</stretch>
                    <pref-width>400</pref-width>
                    <pref-height>400</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>
    <layout>hbox</layout>

    <button>
      <legend>Cycle range</legend>
      <equal>true</equal>
    <binding>
      <command>property-cycle</command>
      <property>WILL BE FILLED IN PROCEDURALLY</property>
      <!-- TODO populate from values vector-->
      <value>10</value>
      <value>20</value>
      <value>40</value>
      <value>80</value>
      <value>160</value>
      <value>320</value>
    </binding>   

    </button>

   <text>
    <format>Range: %s nm</format>
    <property>WILL BE FILLED IN PROCEDURALLY</property>
    <live>true</live>
   </text>

    </group>
    </group>

    </canvas-widget>

  </widget-templates>

  <nasal>
  <open><![CDATA[
# print("Nasal/open");
var getWidgetTemplate = func(identifier) {
var target = globals.gui.findElementByName(root,  identifier );
if(target == nil) die("Target node not found for type:"~identifier);

return target;
}

###
# locate required templates
var target = getWidgetTemplate(root:cmdarg(), identifier:'canvas-placeholder');
var template = getWidgetTemplate(root:cmdarg(), identifier:'canvas-mfd');
var checkboxTemplate = getWidgetTemplate(root:cmdarg(), identifier:'checkbox-template');

var initialize_nd = func(index) {
                   var my_canvas = canvas.get( cmdarg() ); # this will get a handle to the parent canvas:
		   # show_canvas_id(my_canvas); # this is for debugging only
		   setupND(mfd_root: "/instrumentation/efis["~index~"]", my_canvas: my_canvas);
  
};

var errors = [];

# NOTE: this requires changes to navdisplay.mfd
# call(func[, args[, me[, locals[, error]]]);
# call(
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
}

# debug.dump( NDStyles );
print("ND Styles found:", size(keys(NDStyles)));

	var show_canvas_id = func(c) {
		print("Canvas is:", c.getPath());
	};

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

  #### 
  ## an adapted version of the setup logic found in ND.nas
  ##
var myCockpit_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:[1, 0, 1 ], type:'INT'},
    'toggle_rh_vor_adf':    {legend:'vor/adf (r)', path: '/inputs/rh-vor-adf',value:0, values: [1, 0, 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.mfd as needed)
      };


# 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 setupND = func(mfd_root, my_canvas, style='Airbus') {

   ###
    # entry point, this will set up an ND instance

    # get a handle to the NavDisplay in canvas namespace (for now), see $FG_ROOT/Nasal/canvas/map/navdisplay.mfd
    var ND = NavDisplay;

    ##
    # set up a  new ND instance, under mfd_root and use the
    # myCockpit_switches hash to map ND specific control properties
    var myND= ND.new(mfd_root, myCockpit_switches, style);

    var group = my_canvas.createGroup();
    myND.newMFD(group, my_canvas);
    myND.update();
    # store the instance for later cleanup
    append(MFDInstances, 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
var canvas_areas = [
	{name: 'captain.ND', property_root:'/instrumentation/efis[0]',},
	{name: 'copilot.ND', property_root:'/instrumentation/efis[1]',},
	{name: 'engineer.ND', property_root:'/instrumentation/efis[2]',},
];

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

var index=0;
foreach(var c; canvas_areas) {
# print("Adding Canvas widget to GUI dialog procedurally");

# next, create a new symbol named canvasWidget, create child in target, with the index specified (idx)
var canvasWidget = target.getChild("group", index, 1);

# now, copy our template stuff into the new tree 
props.copy(template.getChild("group"), canvasWidget);

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

# 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~");");

var range_property = c.property_root ~ '/inputs/range-nm';
canvasWidget.getNode("group[1]/button/binding/property").setValue(range_property);
canvasWidget.getNode("group[1]/text/property").setValue(range_property);
# TODO: populate values dynamically with values vector from cockpit switches

var checkboxArea = getWidgetTemplate(root:canvasWidget, identifier:'mfd-controls'); # globals.gui.findElementByName(canvasWidget, 'mfd-controls');
# if(checkboxArea == nil) die("Checkbox area node not found for mfd controls");

var cb_index = 0;
# add checkboxes for each boolean switch
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(checkboxTemplate.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);
} # add checkboxes for each boolean ND switch

index += 1;

} # foreach ND instance

  ]]>
  </open>

  <close><![CDATA[
	print("nasal/closing block canvas-nd.xml");
	foreach(var mfd; 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>Reload</legend>
 	<enable>
        <property>/do-not-enable/reloading</property>
        </enable>


         <border>2</border>
         <binding>
             <command>reinit</command>
              <subsystem>gui</subsystem>
          </binding>
<!--
         <binding>
             <command>dialog-close</command>
              <dialog-name>canvas-nd</dialog-name>
          </binding>
          <binding>
             <command>dialog-show</command>
              <dialog-name>canvas-nd</dialog-name>
          </binding>
-->
      </button>

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

</PropertyList>

References

References