Canvas SVG Parser

From FlightGear wiki
Revision as of 19:38, 7 November 2013 by Hooray (Talk | contribs) (Complex Instruments)

Jump to: navigation, search

Vector Image Support

The SVG parser basically just maps the xml structure to the property tree. It is implemented in Nasal on top of the XML parsing facilities already provided by FlightGear. Using separate canvas elements instead of a single image will always be slower as every little piece of the canvas hat to be triangulated and afterwards rendered every time the canvas gets updated instead of just copying an image. On the other hand you if you use the canvas you can dynamically update the contents of the image and also get (theoretically) unlimited resolution, even changeable at runtime. For SVG, already existing tools (Inkscape) can be used to create images and then just load them via Nasal and add some dynamic features to them.

Basic example

For loading a SVG file onto a Canvas we first need to create a Canvas instance (See Howto:Add_a_2D_canvas_instrument_to_your_aircraft). Afterwards we can load a SVG by just using the function canvas.parsesvg from the Canvas API, also the API now supports retrieving elements by id which enables the following simple code snippet for changing the text and color in an instrument:

# Create a group for the parsed elements
var eicas = my_canvas.createGroup();

# Parse an SVG file and add the parsed elements to the given group
canvas.parsesvg(eicas, "Aircraft/C-130J/Instruments/EICAS.svg");

# Get a handle to the element called "ACAWS_10" inside the parsed
# SVG file...
var msg = eicas.getElementById("ACAWS_10");

# ... and change it's text and color
msg.setText("THE NEW API IS COOL!");

Also the API now supports retrieving elements by id which enables the following simple code snippet for changing the text and color in an instrument: You can lookup any type of element you want and modify them how you want (Add transformations, change colors/texts/coordinates etc.). You can also lookup an parent element and afterwards some of is child elements. By this you can use the same id multiple times but are still able to get access to every element (eg. Engine 1/Dial N1, Engine 2/Dial N1, etc.).

The result will look somehow like in the following image. The screen on the left side has been created by using the code snippet above and the screen on the right side is just a statically rendered version of the EICAS:

Simple EICAS example (Notice our warning message)

Supported SVG features

The SVG file used for this demo has been created using Inkscape. Using paths (also with linestipple/dasharray), text, groups and cloning is supported, but don't try to use more advanced features like gradients, as the SVG parser doesn't interpret every part of the SVG standard. (You can always have a look at the implementation and also improve it if you want ;-) )

NOTE: As of 08/2012, the Canvas system also provides support for raster images, however the SVG parser has not yet been extended to also support raster images via the "image" tag.

Advanced usage

Font settings

By default every text element uses "LiberationFonts/LiberationMono-Bold.ttf" for rendering. If you want to use another font you can pass a function as an additional option to canvas.parsesvg:

# There are two arguments passed to the function. The first contains
# the font-family and the second one the font-weight attribute value
var font_mapper = func(family, weight)
  if( family == "Ubuntu Mono" and weight == "bold" )
    # We have to return the name of the font file, which has to be
    # inside either the global Font directory inside fgdata or a
    # Font directory inside the current aircraft directory.
    return "UbuntuMono-B.ttf";

  # If we don't return anything the default font is used

# Same as before...
  # ... but additionally with our font mapping function
  {'font-mapper': font_mapper}

Complex Instruments

Complex instruments like an airliner's PFD, ND or EICAS display will typically need to retrieve dozens of handles to SVG elements, store them and animate them independently. Typically, you'll see code like the following snippet being used:

        var speedText = pfd.getElementById("speedText");
        var markerBeacon = pfd.getElementById("markerBeacon");
        var markerBeaconText = pfd.getElementById("markerBeaconText");
        var machText = pfd.getElementById("machText");
        var altText = pfd.getElementById("altText");
        var selHdgText = pfd.getElementById("selHdgText");
        var selAltPtr = pfd.getElementById("selAltPtr");
        var fdX = pfd.getElementById("fdX");
        var fdY = pfd.getElementById("fdY");
        var curAlt1 = pfd.getElementById("curAlt1");
        var curAlt2 = pfd.getElementById("curAlt2");
        var curAlt3 = pfd.getElementById("curAlt3");
... and so on

As you can see, it is generally a good idea to use identical names, for both, SVG elements and Nasal variables.

However, there's one important consideration here: Code like that is generally not suitable to work for multiple instances of an instruments. This may not seem important in the beginning, but normally each pilot will have their own PFD/ND screens, and their own set of switches to control each display. In addition, keeping such requirements in mind, helps to generalize and improve the design of your code.

So rather than having possibly dozens of free-standing Nasal variables dozens of getElementById() calls, you can save lots of time, typing and energy by using a Nasal hash (class) as your instrument container:

var myPFD777 = {
 new: func {
  return {parents:[myPFD777] };
 init: func(groupp) {
  me.pfd = group;
  canvas.parsesvg(me.pfd, "Aircraft/747-400/Models/Cockpit/Instruments/ND/ND.svg", {'font-mapper': font_mapper});

  foreach(var svg_element; ["wpActiveId","wpActiveDist","wind","gs","tas",
  me[svg_element] = me.nd.getElementById(element);



For a single instrument, this will be identical - but with the added advantage that all elements and canvas groups will not be saved as singleton/global variable, but as members of your myPFD777 class - that way, you will be easily able to support multiple instances of the same instrument.

If you have SVG elements that need some special processing, such as calling the updateCenter() during initialization, you can simply put those inside a separate vector:

# 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
		# but do watch our for naming conflicts with other member fields, simply rename SVG IDs if necessary
		foreach(var element; ["rotateComp","windArrow","selHdg",
				      "staFromR","staToR","compass"] )
		  me[element] = me.nd.getElementById(element).updateCenter();

Bottom line being: 1) less code, 2) less typing, 3) better maintainable, 4) more future-proof

To port existing code (such as the 744 EICAS/PFD) accordingly, here's what you'll want to do:

  • make sure that the SVG element's ID is indeed unique, also within the namespace of your hash/class, to avoid naming conflicts (if the ID is not unique, either introduce a sub namespace/hash, or simply open the SVG file and rename it)
  • search/replace occurences of the old variable name and prepend a me. prefix, e.g. altitudeFt becomes me.altitudeFt
  • remove/comment the free-standing var altitudeFt = {}; line and replace it by adding the SVG element ID to the vector of the foreach loop