Howto:Parsing 2D Instruments using the Canvas

From FlightGear wiki
Jump to navigation Jump to search
This article is a stub. You can help the wiki by expanding it.

There's also the long-term plan to re-implement our 2D panel manager code in Nasal space through a custom Canvas wrapper - so we could just as well provide a little more than just that stub, I didn't bother looking into supporting the various transformations/actions - but given the flexibility and power of the canvas system, it should actually be fairly straightforward - I cannot currently think of anything "missing" here - the status looks pretty encouraging actually, I think I also remember seeing quite a bit of useful code in Tom's fgdata repo.

Coming up with a Nasal/Canvas wrapper that supports loading our 2D panels should be fairly straightforward, but would involve a fair bit of sophisticated Nasal hacking, i.e. "meta-programming" and familiarity with the canvas system and the 2D panel code obviously

Note All FlightGear disk I/O handled via Nasal scripting and/or fgcommands, is subject to access validation via IOrules. This includes the SGPath bindings in FlightGear 2.99+

However, unlike $FG_ROOT, $FG_HOME is generally accessible for writing, consider this example:

#  come up with a path and filename in $FG_HOME, inside the Export sub folder, file name is test.xml
var filename = getprop("/sim/fg-home") ~ "/Export/test.xml";

# use the write_properties() helper in io.nas, which is a wrapper for the savexml fgcommand (see README.commands)
io.write_properties( path: filename, prop: "/sim" );

This will dump the sub branch of the /sim property tree into $FG_HOME/Export/test.xml

For additional examples, see $FG_ROOT/Nasal/io.nas

To learn more about PropertyList processing via loadxml and savexml, please see $FG_ROOT/Docs/README.commands

Internally, all read/write access is validated via an API called fgValidatePath(), for details please see $FG_SRC/Main/util.cxx

Parsing Instruments and Texture Mapping

Here's another stub, that demonstrates how to process existing 2D panel instruments from $FG_AIRCRAFT/Instruments, load them via read_properties() (to be found in io.nas) and then create canvas groups with images for each layer found in the XML file (lots of stuff still missing, such as transformations, bindings/actions). If you'd like to help with this, see Canvas Wrappers#2D Instruments parser

###
# $FG_ROOT/Nasal/parse2dpanel/demo.nas
#

##
# the 2D instrument we are loading textures from
#
var filename = "/Aircraft/Instruments/gyro.xml";

##
# the function that will be called once all files in 
# the  parse2dpanel folder have been loaded
#
var demo = func {
 
##
# enable menubar hiding
#
setprop("/sim/menubar/autovisibility/enabled",1);
 
##
# get the screen coordinates
var x = getprop("/sim/startup/xsize");
var y = getprop("/sim/startup/ysize");
 
##
# create a new canvas fullscreen window 
#
var dlg = canvas.Window.new([x,y]);
 
##
# set rgba
#
var my_canvas = dlg.createCanvas()
                     .setColorBackground(1,1,1,1);
 
##
# create the toplevel canvas group
#                     
var root = my_canvas.createGroup();


##
# load the PropertyList-encoded XML file
#
var temp=io.read_properties(getprop("/sim/fg-root") ~ filename);
 

##
# can be used for the z-index attribute, i.e. stacking textures
# 
var z =100;

## 
# transparently convert the XML file into a Nasal hash using props.nas/getValues()
#
var layers = temp.getValues().layers.layer;

##
# now, go through all layers, 
#
foreach(var layer; layers ) {
 print("new layer:", layer.name);

 # if it's  not a texture, skip
 if (!contains(layer, "texture")) continue;

 # get a handle to the texture of the layer
 var texture = layer.texture;

 # create an image child for the texture
 root.createChild("image")
			           .setFile( texture.path )
				   .setSourceRect( texture.x1, texture.x2, texture.y1, texture.y2 )
                                   .setSize(layer.w,layer.h)
                                   .setTranslation(100,100)
				   .set("z-index", z +=1 );

# TODO: process transformations etc here

} # foreach layer

##
# add a simple event handler, clicking destroys the window
#
my_canvas.addEventListener("click", 
	func dlg.del() 
);
 
} # end of demo function

##
# register a submodule listener to ensure that our demo function is called
#
_setlistener("/nasal/parse2dpanel/loaded", demo );

Parsing Transformations

Here's basically the same code as before, this time with some boilerplate to help parse the various transformations supported by 2D panel instruments (rotate, x-shift, y-shift), the next step would be populating the placeholders in the Transformation map, so that they actually do something, the transformation callbacks accept a "params" hash, which contains:

  • handle to the canvas image group that implements that layer, named params.group
  • any optional arguments as members of the params hash, e.g. scale/offset

See $FG_ROOT/Docs/README.xmlpanel for more info about transformations like x-shift, y-shift, rotate and their parameters.

###
# $FG_ROOT/Nasal/parse2dpanel/demo.nas
#

##
# the 2D instrument we are loading textures from
#
var filename = "/Aircraft/Instruments/gyro.xml";

##
# print optional transformation arguments
#
var print_optional = func(hash, key) {
 if(contains(hash,key))
	print(key,':',hash[key] );
}

##
# show any optional arguments
#

var show_optional = func(hash) {
 foreach(var p; ['scale', 'offset', 'property'] )
	print_optional(hash, p);
}

##
# show transformation info
#
var transform_info = func(type, params) {
 show_optional(params);
}

##
# lookup table/map with transformation names and function callbacks
#
var Transformations = {
 'rotation': func(params) {
   transform_info("rotation:", params);
  },
 'x-shift': func(params) {
   transform_info("x-shift", params);
 },
 'y-shift': func(params) {
   transform_info("y-shift", params);
 },
};


##
# extract optional arguments
#

var extract_optional = func(hash, name, dest) {
 if (contains(hash, name))
	dest[name] = hash[name];
}

##
# handle a transformation
#

var handleTransform = func(layer, transformation, instrument_layer) {
  print("Transform type:", transformation.type);
  # wrap all transformation parameters in a hash
  var params = {};
  # save a handle to the canvas group in the params hash, so that callbacks can access the right sub tree
  params.group = instrument_layer;

  # now, add transformation-specific args on demand
  # debug.dump(transformation);
  if (transformation.type == 'rotation')
	params.property = transformation.property;
  else {  
	foreach(var opt; ['offset', 'scale'])
		extract_optional(transformation, opt, params);
	}

  ##
  # register a listener that is invoked once the property changes to update the transformation
  if (transformation.type == 'rotation') # dynamic/property transforms
  	setlistener( transformation.property, func {
			Transformations[transformation.type] (params);
		}, 1		
	);
  else # apply static transformations immediately
   Transformations[transformation.type] (params);
}

 
##
# the function that will be called once all files in 
# the  parse2dpanel folder have been loaded
#
var demo = func {
 
##
# enable menubar hiding
#
setprop("/sim/menubar/autovisibility/enabled",1);
 
##
# get the screen coordinates
var x = getprop("/sim/startup/xsize");
var y = getprop("/sim/startup/ysize");
 
##
# create a new canvas fullscreen window 
#
var dlg = canvas.Window.new([300,240]);
 
##
# set rgba
#
var my_canvas = dlg.createCanvas()
	.setColorBackground(1,1,1,1); 
##
# create the toplevel canvas group
#                     
var root = my_canvas.createGroup();
 
 
##
# load the PropertyList-encoded XML file
#
temp=io.read_properties(getprop("/sim/fg-root") ~ filename);
 
 
##
# can be used for the z-index attribute, i.e. stacking textures (layers)
# 
var z =100;
 
##
# create a sub group for this instrument
#
var instrument = root.createChild("group")
	.set("instrument-name", temp.getValues().name );

## 
# transparently convert the XML file into a Nasal hash using props.nas/getValues()
#
var layers = temp.getValues().layers.layer;

 
##
# now, go through all layers in the file 
#
foreach(var layer; layers ) {
 print("new layer:", layer.name);
 
 # if it's  not a texture, skip
 if (!contains(layer, "texture")) continue;
 
 # get a handle to the texture of the layer
 var texture = layer.texture;
 
 # create an image child for the texture
 var instrument_layer=instrument.createChild("image")
				   .set('layer-name', layer.name) # meta information
			       .setFile( texture.path )
				   .setSourceRect( texture.x1, texture.x2, texture.y1, texture.y2 )
				   .setSize(layer.w,layer.h)
                   .setTranslation(100,100)
				   .set("z-index", z +=1 );
 
# TODO: process transformations etc here, for example:

if (contains(layer, "transformations")) {
  var transformations = layer.transformations.transformation;
  print("Transformations found:", size( transformations ));
  if (typeof(transformations)=='vector')
  	foreach(var t; transformations) {
		handleTransform(layer, t, instrument_layer);	
  	} #foreach transformation
} # have transformations

} # foreach layer

##
# add a simple event handler, clicking destroys the window
#
my_canvas.addEventListener("click", 
	 func dlg.del() 
);
 
} # end of demo function

##
# register a submodule listener to ensure that our demo function 
# is called once all submodule files are loaded
#
_setlistener("/nasal/parse2dpanel/loaded", demo );

Compass Widget

Inspired by a a recent forum discussion, we thought that creating a compass widget would be a neat little project for someone interested in learning a bit more about Nasal and Canvas coding, and it should be possible to accomplish with less than 50-80 lines of code actually - there's a ton of stuff that you could reuse (SVG compass rose or an existing 2D panel texture), we have quite a few tutorials on Nasal, Canvas and Image processing (check the wiki). Furthermore, we have the long-term plan to eventually phase out the old 2D panel code and used a modernized Canvas-based version to help with Unifying the 2D rendering backend via canvas.


A Nasal/Canvas based compass widget

Paste this into your Nasal Console and click Execute:

var CanvasApplication = {
 ##
 # constructor
 new: func(x=300,y=200) {
  var m = { parents: [CanvasApplication] };
  m.dlg = canvas.Window.new([x,y],"dialog");
  m.canvas = m.dlg.createCanvas().setColorBackground(1,1,1,0.5);
  m.root = m.canvas.createGroup();
  m.timer = maketimer(0.1, func m.update() );
  m.init();
  return m;
 },

del: func me.timer.stop(),

update: func() {
 var hdg=getprop("/orientation/heading-deg");
 me.compass.setRotation(-hdg*D2R);
 
},

init: func() {
 var filename = "/Aircraft/Instruments/gyro.xml";
 var temp= io.read_properties(getprop("/sim/fg-root") ~ filename); 
 var layers = temp.getValues().layers.layer;

var z=100;
foreach(var layer; layers ) {
 print("new layer:", layer.name);
 
 # if it's  not a texture, skip
 if (!contains(layer, "texture")) continue;
 
 # get a handle to the texture of the layer
 var texture = layer.texture;
 
 # create an image child for the texture
 var child=me.root.createChild("image")
			           .setFile( texture.path )
				   .setSourceRect( texture.x1, texture.x2, texture.y1, texture.y2 )
                                   .setSize(layer.w,layer.h)
                                   .setTranslation(20,20)
				   .set("z-index", z +=1 )
				   .setScale(2.5);
 
if (layer.w != nil and layer.h!=nil)
  child.setCenter(layer.w/2, layer.h/2);

 if (layer.name=="compass rose") {
  print("Found compass layer");
  # child.setCenter(55,55);
  me.compass = child;
 }
} # foreach


 
 me.timer.start();
 },
}; # end of CanvasApplication


var InstrumentWidget = {
 new: func(x,y) {
  var m = CanvasApplication.new(x:x,y:y);
 },

};

var compass = InstrumentWidget.new(x:300, y:300);

print("Compass Loaded...!");