Canvas SVG parser

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


Scripted SVG parser for Canvas
svg.nas at work
svg.nas at work
Description Implements a scripted SVG parser in Nasal space for the Canvas system by mapping a subset of SVG to Canvas elements (text, OpenVG, raster images)
Maintainer(s) TheTom
Contributor(s) TheTom
Status Under active development
Changelog /Nasal/canvas/svg.nas



The scripted SVG parser basically just maps the SVG xml structure to the property tree by adding corresponding Canvas elements as child nodes. It is implemented in Nasal on top of the XML parsing facilities already provided by FlightGear/Nasal (see $FG_ROOT/Nasal/io.nas).

There's a tiny Nasal module named svg.nas (see $FG_ROOT/Nasal/Canvas) that will read those SVG/XML files and convert them into Canvas nodes (OpenVG/ShivaVG paths) - in other words, depending on the nature of the differences in the output, it should be possible to patch up the svg parser to also support the new format. To make changes to that module, you need to understand how parsing works and how a stack works (the data structure). [1]

  • a Canvas represents an actual OpenGL texture (2D)
  • is hardware-accelerated
  • is using OSG for most of its features
  • there is an actual scenegraph
  • uses Nasal instead of ECMAScript/JavaScript

A FlightGear Canvas is primarily a property tree in the main property tree, where attributes of the texture, and each element, are mapped to "listeners" (or updated via polling). Internally, this will dispatch events/notifications to the current texture/event. A Canvas in texture is an invisible offscreen rendering context (RTT/FBO) - it is made visible by adding a so called "placement" to the main FlightGear scenegraph, where the static texture will be replaced with one of the dynamic Canvas textures. A FlightGear Canvas supports events for UI purposes, so that listeners can be registered for events like "mouseover" etc. The Canvas scenegraph is a special thing, its root is always a Canvas group - each group can have an arbitrary number of children added, i.e. other elements (or other groups). The primary Canvas elements are 1) raster images, 2) osgText nodes, 3) map, 4) groups and 5) OpenVG paths. The FlightGear Canvas system does not understand SVG images - instead, it is using the OpenVG back-end to translate a subset of SVG/XML to Canvas properties by mapping those to OpenVG primitives. There are many features that are not supported by this SVG parser (svg.nas), but it is written in Nasal and can be easily extended to also support other features, e.g. support for raster images and/or nested SVG images. Apart from OpenVG, there's no lower level drawing support (think pixels). [2]

for the supported commands, refer to $FG_ROOT/Nasal/canvas/api.nas and the segments listed in the canvas.Path hash (OpenVG should support arc/circle paths, not sure if those are currently made available or not) For reference, you can look up the corresponding ShivaVG/OpenVG examples on the web, all primitives listed in api.nas should also be supported by the Canvas/ShivaVG implementation. BTW: Once you know how to draw a circle using OpenVG paths, you can also extend svg.nas to parse the <circle> tag [3]

Vector Image Support

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.

If you find some SVG feature not working, please check the console for errors - our SVG parser is hand-written and only supports a limited subset of SVG instructions, so may need to be extended, or simply use supported inkscape primitives instead (see svg.nas). $FG_ROOT/Nasal/canvas/svg.nas is our existing example on populating a canvas procedurally by parsing an XML file.

Basically, it looks for supported SVG primitives, and turns those into Canvas/OpenVG primitives by setting properties.

The SVG standard explicitly supports referencing other images (including raster images and SVGs) via the "image" tag - even recursively, so I'd be surprised if inkscape didn't support that. However, our svg parser doesn't currently support this IIRC - but it's "just" Nasal code, so would probably just require ~10-15 lines of code to add support for the image/object tags.

the svg parser is hand-written and can be found in $FG_ROOT/Nasal/canvas/svg.nas, it can be easily extended to support additional primitives/tags, you just need to map them to the corresponding OpenVG equivalents, I think supporting shapes would make sense and shouldn't be too difficult.

our svg.nas parser could also be extended to support external SVGs directly, i.e. using the <use> and <image> tags. The other concern here is performance - once instruments are self-contained and can be treated as such, we can also update them more selectively, i.e. we don't necessarily have to use timers for each individual element, but can instead update instruments in a holistic fashion.


Hello World

Demonstrate how to preview SVG Images using Canvas

Paste this into your Nasal Console and click run:

var CanvasApplication = {
 ##
 # constructor
 new: func(x=300,y=200,file="/gui/dialogs/images/ndb_symbol.svg") {

  var m = { parents: [CanvasApplication] };
  m.file=file;
  m.dlg = canvas.Window.new([x,y],"dialog");

  # you can change the background color here
  var color = {WHITE:[1,1,1,1], BLACK:[0,0,0,1]};
  m.canvas = m.dlg.createCanvas().setColorBackground(color.BLACK);
  m.root = m.canvas.createGroup();
  m.update();
  return m;
 },
  
update: func() {
# create a group for the image
var svg_symbols = me.root.createChild("group", "svgfile");

# parse the SVG file
canvas.parsesvg(svg_symbols, me.file);


# resize the image so that it can be  fully seen (35%)
svg_symbols.setScale(0.35); 

},
}; # end of CanvasApplication
 
 
var SVGMapPreview= {
 new: func(x,y,svg) {
  var m = CanvasApplication.new(x:x, y:y, file:svg);
  return m;
 },
 
};
 
var preview= SVGMapPreview.new(x:400, y:400, svg:"Nasal/canvas/map/Images/boeingND.svg");
 
print("SVGPreviewer loaded ...!");

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!");
msg.setColor(1,0,0);

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  Support for SVG circle and ellipse has been added to "next" branch in 12/2018 and is expected to be officially available in release 2019.1

Known Limitations

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 (see the implementation for SVG text->Canvas.Text handling for details).

Shapes are also currently not exposed to Nasal, you'll typically want to use Inkscape's "simplify" option. Basic shapes like Line, Polygon, Rect, RoundedRect, Ellipse and Arc are available through the VGU Utility Library and could be implemented in Nasal directly by extending svg.nas to map SVG/XML primitives to the corresponding OpenVG primitives supported by Shiva/ShaderVG. In the meantime, we should at the very least, look for unsupported primitives like these and show a corresponding error/hint, telling people to simplify their SVG files.

Cquote1.png Learnt the hard way that SVG parsing (for svg_path in AA.symbol) in FG doesn't recognize <circle>, <ellipse>, etc., so I opened my hand-coded SVG in Inkscape and used Path->Simplify (which ostensibly converts shapes to paths) to convert it to a code compatible with FG.
— RevHardt (Tue Jun 24). Re: Get objects to show up on Map/Radar.
(powered by Instant-Cquotes)
Cquote2.png
Cquote1.png Referring to the gitorious discussion about having a styleable SVGSymbol class - that is something that will be hard to support/integrate with pre-defined styling unless we patch svg.nas to accept a list of optional SVG attributes that are mapped to a transformation/rewrite-callback so that SVG attributes can be dynamically "rewritten" by the parser based on looking up a certain id (e.g. "background-color") and changing CSS stuff there.
— Hooray (Sun May 11). MapStructure styling.
(powered by Instant-Cquotes)
Cquote2.png
Cquote1.png This could then use id settings like "glideslope" and "localize" to make those elements accessible, the parser would check its lookup map if the current element matches any key, and if so applies the callback to rewrite the corresponding tag to transparently re-style things without having to modify the actual file. People wanting to change the representation would then merely need to copy the SVG file and ensure that they use the same IDs that are required by the layer's style
— Hooray (Sun May 11). MapStructure styling.
(powered by Instant-Cquotes)
Cquote2.png

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...
canvas.parsesvg
(
  eicas,
  "Aircraft/C-130J/Instruments/EICAS.svg",
  # ... 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(group) {
  me.pfd = group;
  me.symbols = {};
  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",
				      "hdg","dmeLDist","dmeRDist","vorLId","vorRId",
				      "eta","range","taOnly","status.wxr","status.wpt",
				      "status.sta","status.arpt"]) 
  me.symbols[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",
				      "curHdgPtr","staFromL","staToL",
				      "staFromR","staToR","compass"] )
		  me.symbols[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
  1. Hooray (Mar 28th, 2016). Re: Strange thing with font mapper.
  2. Hooray (Feb 27th, 2016). Re: Canvas documentation.
  3. Hooray (Mar 20th, 2016). Re: Drawing a circle.