Canvas: Improve API and SVG parser.
[fg:fgdata.git] / Nasal / canvas / api.nas
1 # Helper function to create a node with the first available index for the given
2 # path relative to the given node
3 #
4 var _createNodeWithIndex = func(node, path, min_index = 0)
5 {
6   # TODO do we need an upper limit? (50000 seems already seems unreachable)
7   for(var i = min_index; i < 50000; i += 1)
8   {
9     var p = path ~ "[" ~ i ~ "]";
10     if( node.getNode(p) == nil )
11       return node.getNode(p, 1);
12   }
13   
14   debug.warn("Unable to get child (already 50000 exist)");
15
16   return nil;
17 };
18
19 # Internal helper
20 var _createColorNodes = func(parent, name)
21 {
22   var node = parent.getNode(name, 1);
23   return [ node.getNode("red", 1),
24            node.getNode("green", 1),
25            node.getNode("blue", 1),
26            node.getNode("alpha", 1) ];
27 };
28
29 var _setColorNodes = func(nodes, color)
30 {
31   if( typeof(nodes) != "vector" )
32   {
33     debug.warn("This element doesn't support setting color");
34     return;
35   }
36   
37   if( size(color) == 1 )
38     color = color[0];
39
40   if( typeof(color) != "vector" )
41     return debug.warn("Wrong type for color");
42   
43   if( size(color) < 3 or size(color) > 4 )
44     return debug.warn("Color needs 3 or 4 values (RGB or RGBA)");
45
46   for(var i = 0; i < size(color); i += 1)
47     nodes[i].setDoubleValue( color[i] );
48
49   if( size(color) == 3 )
50     # default alpha is 1
51     nodes[3].setDoubleValue(1);
52 };
53
54 var _arg2valarray = func
55 {
56   var ret = arg;
57   while (    typeof(ret) == "vector"
58             and size(ret) == 1 and typeof(ret[0]) == "vector" )
59       ret = ret[0];
60   return ret;
61 }
62
63 # Transform
64 # ==============================================================================
65 # A transformation matrix which is used to transform an #Element on the canvas.
66 # The dimensions of the matrix are 3x3 where the last row is always 0 0 1:
67 #
68 #  a c e
69 #  b d f
70 #  0 0 1
71 #
72 # See http://www.w3.org/TR/SVG/coords.html#TransformMatrixDefined for details.
73 #
74 var Transform = {
75   new: func(node, vals = nil)
76   {
77     var m = {
78       parents: [Transform],
79       _node: node,
80       a: node.getNode("m[0]", 1),
81       b: node.getNode("m[1]", 1),
82       c: node.getNode("m[2]", 1),
83       d: node.getNode("m[3]", 1),
84       e: node.getNode("m[4]", 1),
85       f: node.getNode("m[5]", 1)
86     };
87     
88     var use_vals = typeof(vals) == 'vector' and size(vals) == 6;
89     
90     # initialize to identity matrix
91     m.a.setDoubleValue(use_vals ? vals[0] : 1);
92     m.b.setDoubleValue(use_vals ? vals[1] : 0);
93     m.c.setDoubleValue(use_vals ? vals[2] : 0);
94     m.d.setDoubleValue(use_vals ? vals[3] : 1);
95     m.e.setDoubleValue(use_vals ? vals[4] : 0);
96     m.f.setDoubleValue(use_vals ? vals[5] : 0);
97     
98     return m;
99   },
100   setTranslation: func
101   {
102     var trans = _arg2valarray(arg);
103
104     me.e.setDoubleValue(trans[0]);
105     me.f.setDoubleValue(trans[1]);
106     
107     return me;
108   },
109   # Set rotation (Optionally around a specified point instead of (0,0))
110   #
111   #  setRotation(rot)
112   #  setRotation(rot, cx, cy)
113   #
114   # @note If using with rotation center different to (0,0) don't use
115   #       #setTranslation as it would interfere with the rotation.
116   setRotation: func(angle)
117   {
118     var center = _arg2valarray(arg);
119
120     var s = math.sin(angle);
121     var c = math.cos(angle);
122
123     me.a.setDoubleValue(c);
124     me.b.setDoubleValue(s);
125     me.c.setDoubleValue(-s);
126     me.d.setDoubleValue(c);
127
128     if( size(center) == 2 )
129     {
130       me.e.setDoubleValue( (-center[0] * c) + (center[1] * s) + center[0] );
131       me.f.setDoubleValue( (-center[0] * s) - (center[1] * c) + center[1] );
132     }
133     
134     return me;
135   },
136   # Set scale (either as parameters or array)
137   #
138   # If only one parameter is given its value is used for both x and y
139   #  setScale(x, y)
140   #  setScale([x, y])
141   setScale: func
142   {
143     var scale = _arg2valarray(arg);
144
145     me.a.setDoubleValue(scale[0]);
146     me.d.setDoubleValue(size(scale) >= 2 ? scale[1] : scale[0]);
147     
148     return me;
149   },
150   getScale: func()
151   {
152     # TODO handle rotation
153     return [me.a.getValue(), me.d.getValue()];
154   }
155 };
156
157 # Element
158 # ==============================================================================
159 # Baseclass for all elements on a canvas
160 #
161 var Element = {
162   # Constructor
163   #
164   # @param parent   Parent node (In the property tree)
165   # @param type     Type string (Used as node name)
166   # @param id       ID/Name (Should be unique)
167   new: func(parent, type, id)
168   {
169     # arg can contain the node to be used instead of creating a new one
170     var args = _arg2valarray(arg);
171     if( size(args) == 1 )
172     {
173       var node = args[0];
174       if( !isa(node, props.Node) )
175         return debug.warn("Not a props.Node!");
176     }
177     else
178       var node = _createNodeWithIndex(parent, type);
179
180     var m = {
181       parents: [Element],
182       _node: node,
183       _center: [
184         node.getNode("center[0]"),
185         node.getNode("center[1]")
186       ]
187     };
188     
189     if( id != nil )
190       m._node.getNode("id", 1).setValue(id);
191
192     return m;
193   },
194   # Destructor (has to be called manually!)
195   del: func()
196   {
197     me._node.remove();
198   },
199   set: func(key, value)
200   {
201     me._node.getNode(key, 1).setValue(value);
202     return me;
203   },
204   setBool: func(key, value)
205   {
206     me._node.getNode(key, 1).setBoolValue(value);
207     return me;
208   },
209   setDouble: func(key, value)
210   {
211     me._node.getNode(key, 1).setDoubleValue(value);
212     return me;
213   },
214   setInt: func(key, value)
215   {
216     me._node.getNode(key, 1).setIntValue(value);
217     return me;
218   },
219   # Trigger an update of the element
220   # 
221   # Elements are automatically updated once a frame, with a delay of one frame.
222   # If you wan't to get an element updated in the current frame you have to use
223   # this method.
224   update: func()
225   {
226     me.setInt("update", 1);
227   },
228   # Hide/Show element
229   #
230   # @param visible  Whether the element should be visible
231   setVisible: func(visible = 1)
232   {
233     me.setBool("visible", visible);
234   },
235   # Hide element (Shortcut for setVisible(0))
236   hide: func me.setVisible(0),
237   # Show element (Shortcut for setVisible(1))
238   show: func me.setVisible(1),
239   #
240   setGeoPosition: func(lat, lon)
241   {
242     me._getTf()._node.getNode("m-geo[4]", 1).setValue("N" ~ lat);
243     me._getTf()._node.getNode("m-geo[5]", 1).setValue("E" ~ lon);
244     return me;
245   },
246   # Create a new transformation matrix
247   #
248   # @param vals Default values (Vector of 6 elements)
249   createTransform: func(vals = nil)
250   {
251     var node = _createNodeWithIndex(me._node, "tf", 1); # tf[0] is reserved for
252                                                         # setRotation
253     return Transform.new(node, vals);
254   },
255   # Shortcut for setting translation
256   setTranslation: func { me._getTf().setTranslation(arg); return me; },
257   # Set rotation around transformation center (see #setCenter).
258   #
259   # @note This replaces the the existing transformation. For additional scale or
260   #       translation use additional transforms (see #createTransform).
261   setRotation: func(rot)
262   {
263     if( me['_tf_rot'] == nil )
264       # always use the first matrix slot to ensure correct rotation
265       # around transformation center.
266       me['_tf_rot'] = Transform.new(me._node.getNode("tf[0]", 1));
267
268     me._tf_rot.setRotation(rot, me.getCenter());
269     return me;
270   },
271   # Shortcut for setting scale
272   setScale: func { me._getTf().setScale(arg); return me; },
273   # Shortcut for getting scale
274   getScale: func me._getTf().getScale(),
275   # Set the line/text color
276   #
277   # @param color  Vector of 3 or 4 values in [0, 1]
278   setColor: func { _setColorNodes(me.color, arg); return me; },
279   # Set the fill/background/boundingbox color
280   #
281   # @param color  Vector of 3 or 4 values in [0, 1]
282   setColorFill: func { _setColorNodes(me.color_fill, arg); return me; },
283   #
284   getBoundingBox: func()
285   {
286     var bb = me._node.getNode("bounding-box");
287     var min_x = bb.getNode("min-x").getValue();
288     
289     if( min_x != nil )
290       return [ min_x,
291                 bb.getNode("min-y").getValue(),
292                 bb.getNode("max-x").getValue(),
293                 bb.getNode("max-y").getValue() ];
294     else
295       return [0, 0, 0, 0];
296   },
297   # Set transformation center (currently only used for rotation)
298   setCenter: func()
299   {
300     var center = _arg2valarray(arg);
301     if( size(center) != 2 )
302       return debug.warn("invalid arg");
303       
304     if( me._center[0] == nil )
305       me._center[0] = me._node.getNode("center[0]", 1);
306     if( me._center[1] == nil )
307       me._center[1] = me._node.getNode("center[1]", 1);
308
309     me._center[0].setDoubleValue(center[0] or 0);
310     me._center[1].setDoubleValue(center[1] or 0);
311     
312     return me;
313   },
314   # Get transformation center
315   getCenter: func()
316   {
317     var bb = me.getBoundingBox();
318     var center = [0, 0];
319     
320     if( me._center[0] != nil )
321       center[0] = me._center[0].getValue() or 0;
322     if( me._center[1] != nil )
323       center[1] = me._center[1].getValue() or 0;
324
325     if( bb[0] >= bb[2] or bb[1] >= bb[3] )
326       return center;
327     
328     return [ 0.5 * (bb[0] + bb[2]) + center[0],
329               0.5 * (bb[1] + bb[3]) + center[1] ];
330   },
331   # Internal Transform for convenience transform functions
332   _getTf: func
333   {
334     if( me['_tf'] == nil )
335       me['_tf'] = me.createTransform();
336     return me._tf;
337   }
338 };
339
340 # Group
341 # ==============================================================================
342 # Class for a group element on a canvas
343 #
344 var Group = {
345   new: func(parent, id, type = "group")
346   {
347     # special case: if called from #getElementById the third argument is the
348     # existing node so we need to rearange the variables a bit.
349     if( typeof(type) != "scalar" )
350     {
351       var arg = [type];
352       var type = "group";
353     }
354
355     return { parents: [Group, Element.new(parent, type, id, arg)] };
356   },
357   # Create a child of given type with specified id.
358   # type can be group, text
359   createChild: func(type, id = nil)
360   {
361     var factory = me._element_factories[type];
362     
363     if( factory == nil )
364     {
365       debug.dump("canvas.Group.createChild(): unknown type (" ~ type ~ ")");
366       return nil;
367     }
368     
369     return factory(me._node, id);
370   },
371   # Get first child with given id (breadth-first search)
372   #
373   # @note Use with care as it can take several miliseconds (for me eg. ~2ms).
374   getElementById: func(id)
375   {
376     # TODO can we improve the queue or better port this to C++ or use some kind
377     # of lookup hash? Searching is really slow now...
378     var stack = [me._node];
379     var index = 0;
380     
381     while( index < size(stack) )
382     {
383       var node = stack[index];
384       index += 1;
385
386       if( node != me._node )
387       {
388         var node_id = node.getNode("id");
389         if( node_id != nil and node_id.getValue() == id )
390           return me._element_factories[ node.getName() ]
391           (
392             nil,
393             nil,
394             # use the existing node
395             node
396           );
397       }
398         
399       foreach(var c; node.getChildren())
400         # element nodes have type NONE and valid element names (those in the the
401         # factor list)
402         if(     c.getType() == "NONE"
403             and me._element_factories[ c.getName() ] != nil )
404           append(stack, c);
405     }
406   },
407   # Remove all children
408   removeAllChildren: func()
409   {
410     foreach(var type; keys(me._element_factories))
411       me._node.removeChildren(type, 0);
412     return me;
413   }
414 };
415
416 # Map
417 # ==============================================================================
418 # Class for a group element on a canvas with possibly geopgraphic positions
419 # which automatically get projected according to the specified projection.
420 #
421 var Map = {
422   new: func(parent, id)
423   {
424     return { parents: [Map, Group.new(parent, id, "map", arg)] };
425   }
426   # TODO
427 };
428
429 # Text
430 # ==============================================================================
431 # Class for a text element on a canvas
432 #
433 var Text = {
434   new: func(parent, id)
435   {
436     var m = {
437       parents: [Text, Element.new(parent, "text", id, arg)]
438     };
439     m.color = _createColorNodes(m._node, "color");
440     m.color_fill = _createColorNodes(m._node, "color-fill");
441     return m;
442   },
443   # Set the text
444   setText: func(text)
445   {
446     # add space because osg seems to remove last character if its a space
447     me.set("text", typeof(text) == 'scalar' ? text ~ ' ' : "");
448   },
449   # Set alignment
450   #
451   #  @param algin String, one of:
452   #   left-top
453   #   left-center
454   #   left-bottom
455   #   center-top
456   #   center-center
457   #   center-bottom
458   #   right-top
459   #   right-center
460   #   right-bottom
461   #   left-baseline
462   #   center-baseline
463   #   right-baseline
464   #   left-bottom-baseline
465   #   center-bottom-baseline
466   #   right-bottom-baseline
467   #
468   setAlignment: func(align)
469   {
470     me.set("alignment", align);
471   },
472   # Set the font size
473   setFontSize: func(size, aspect = 1)
474   {
475     me.setDouble("character-size", size);
476     me.setDouble("character-aspect-ratio", aspect);
477   },
478   # Set font (by name of font file)
479   setFont: func(name)
480   {
481     me.set("font", name);
482   },
483   # Enumeration of values for drawing mode:
484   TEXT:               1, # The text itself
485   BOUNDINGBOX:        2, # A bounding box (only lines)
486   FILLEDBOUNDINGBOX:  4, # A filled bounding box
487   ALIGNMENT:          8, # Draw a marker (cross) at the position of the text
488   # Set draw mode. Binary combination of the values above. Since I haven't found
489   # a bitwise or we have to use a + instead.
490   #
491   #  eg. my_text.setDrawMode(Text.TEXT + Text.BOUNDINGBOX);
492   setDrawMode: func(mode)
493   {
494     me.setInt("draw-mode", mode);
495   },
496   # Set bounding box padding
497   setPadding: func(pad)
498   {
499     me.setDouble("padding", pad);
500   },
501   setMaxWidth: func(w)
502   {
503     me.setDouble("max-width", w);
504   }
505 };
506
507 # Path
508 # ==============================================================================
509 # Class for an (OpenVG) path element on a canvas
510 #
511 var Path = {
512   # Path segment commands (VGPathCommand)
513   VG_CLOSE_PATH:     0,
514   VG_MOVE_TO:        2,
515   VG_MOVE_TO_ABS:    2,
516   VG_MOVE_TO_REL:    3,
517   VG_LINE_TO:        4,
518   VG_LINE_TO_ABS:    4,
519   VG_LINE_TO_REL:    5,
520   VG_HLINE_TO:       6,
521   VG_HLINE_TO_ABS:   6,
522   VG_HLINE_TO_REL:   7,
523   VG_VLINE_TO:       8,
524   VG_VLINE_TO_ABS:   8,
525   VG_VLINE_TO_REL:   9,
526   VG_QUAD_TO:       10,
527   VG_QUAD_TO_ABS:   10,
528   VG_QUAD_TO_REL:   11,
529   VG_CUBIC_TO:      12,
530   VG_CUBIC_TO_ABS:  12,
531   VG_CUBIC_TO_REL:  13,
532   VG_SQUAD_TO:      14,
533   VG_SQUAD_TO_ABS:  14,
534   VG_SQUAD_TO_REL:  15,
535   VG_SCUBIC_TO:     16,
536   VG_SCUBIC_TO_ABS: 16,
537   VG_SCUBIC_TO_REL: 17,
538   VG_SCCWARC_TO:    20, # Note that CC and CCW commands are swapped. This is
539   VG_SCCWARC_TO_ABS:20, # needed  due to the different coordinate systems used.
540   VG_SCCWARC_TO_REL:21, # In OpenVG values along the y-axis increase from bottom
541   VG_SCWARC_TO:     18, # to top, whereas in the Canvas system it is flipped.
542   VG_SCWARC_TO_ABS: 18,
543   VG_SCWARC_TO_REL: 19,
544   VG_LCCWARC_TO:    24,
545   VG_LCCWARC_TO_ABS:24,
546   VG_LCCWARC_TO_REL:25,
547   VG_LCWARC_TO:     22,
548   VG_LCWARC_TO_ABS: 22,
549   VG_LCWARC_TO_REL: 23,
550
551   # Number of coordinates per command
552   num_coords: [
553     0, 0, # VG_CLOSE_PATH
554     2, 2, # VG_MOVE_TO
555     2, 2, # VG_LINE_TO
556     1, 1, # VG_HLINE_TO
557     1, 1, # VG_VLINE_TO
558     4, 4, # VG_QUAD_TO
559     6, 6, # VG_CUBIC_TO
560     2, 2, # VG_SQUAD_TO
561     4, 4, # VG_SCUBIC_TO
562     5, 5, # VG_SCCWARC_TO
563     5, 5, # VG_SCWARC_TO
564     5, 5, # VG_LCCWARC_TO
565     5, 5  # VG_LCWARC_TO
566   ],
567
568   #
569   new: func(parent, id)
570   {
571     var m = {
572       parents: [Path, Element.new(parent, "path", id, arg)],
573       _num_cmds: 0,
574       _num_coords: 0
575     };
576     m.color = _createColorNodes(m._node, "color");
577     m.color_fill = _createColorNodes(m._node, "color-fill");
578     return m;
579   },
580   # Remove all existing path data
581   reset: func
582   {
583     me._node.removeChildren('cmd', 0);
584     me._node.removeChildren('coord', 0);
585     me._node.removeChildren('coord-geo', 0);
586     me._num_cmds = 0;
587     me._num_coords = 0;
588     return me;
589   },
590   # Set the path data (commands and coordinates)
591   setData: func(cmds, coords)
592   {
593     me.reset();
594     me._node.setValues({cmd: cmds, coord: coords});
595     me._num_cmds = size(cmds);
596     me._num_coords = size(coords);
597     return me;
598   },
599   setDataGeo: func(cmds, coords)
600   {
601     me.reset();
602     me._node.setValues({cmd: cmds, 'coord-geo': coords});
603     me._num_cmds = size(cmds);
604     me._num_coords = size(coords);
605     return me;
606   },
607   # Add a path segment
608   addSegment: func(cmd, coords...)
609   {
610     var coords = _arg2valarray(coords);
611     var num_coords = me.num_coords[cmd];
612     if( size(coords) != num_coords )
613       debug.warn
614       (
615         "Invalid number of arguments (expected " ~ (num_coords + 1) ~ ")"
616       );
617     else
618     {
619       me.setInt("cmd[" ~ (me._num_cmds += 1) ~ "]", cmd);
620       for(var i = 0; i < num_coords; i += 1)
621         me.setDouble("coord[" ~ (me._num_coords += 1) ~ "]", coords[i]);
622     }
623     
624     return me;
625   },
626   # Move path cursor
627   moveTo: func me.addSegment(me.VG_MOVE_TO_ABS, arg),
628   move:   func me.addSegment(me.VG_MOVE_TO_REL, arg),
629   # Add a line
630   lineTo: func me.addSegment(me.VG_LINE_TO_ABS, arg),
631   line:   func me.addSegment(me.VG_LINE_TO_REL, arg),
632   # Add a horizontal line
633   horizTo: func me.addSegment(me.VG_HLINE_TO_ABS, arg),
634   horiz:   func me.addSegment(me.VG_HLINE_TO_REL, arg),
635   # Add a vertical line
636   vertTo: func me.addSegment(me.VG_VLINE_TO_ABS, arg),
637   vert:   func me.addSegment(me.VG_VLINE_TO_REL, arg),
638   # Add a quadratic Bézier curve
639   quadTo: func me.addSegment(me.VG_QUAD_TO_ABS, arg),
640   quad:   func me.addSegment(me.VG_QUAD_TO_REL, arg),
641   # Add a cubic Bézier curve
642   cubicTo: func me.addSegment(me.VG_CUBIC_TO_ABS, arg),
643   cubic:   func me.addSegment(me.VG_CUBIC_TO_REL, arg),
644   # Add a smooth quadratic Bézier curve
645   quadTo: func me.addSegment(me.VG_SQUAD_TO_ABS, arg),
646   quad:   func me.addSegment(me.VG_SQUAD_TO_REL, arg),
647   # Add a smooth cubic Bézier curve
648   cubicTo: func me.addSegment(me.VG_SCUBIC_TO_ABS, arg),
649   cubic:   func me.addSegment(me.VG_SCUBIC_TO_REL, arg),
650   # Draw an elliptical arc (shorter counter-clockwise arc)
651   arcSmallCCWTo: func me.addSegment(me.VG_SCCWARC_TO_ABS, arg),
652   arcSmallCCW:   func me.addSegment(me.VG_SCCWARC_TO_REL, arg),
653   # Draw an elliptical arc (shorter clockwise arc)
654   arcSmallCWTo: func me.addSegment(me.VG_SCWARC_TO_ABS, arg),
655   arcSmallCW:   func me.addSegment(me.VG_SCWARC_TO_REL, arg),
656   # Draw an elliptical arc (longer counter-clockwise arc)
657   arcLargeCCWTo: func me.addSegment(me.VG_LCCWARC_TO_ABS, arg),
658   arcLargeCCW:   func me.addSegment(me.VG_LCCWARC_TO_REL, arg),
659   # Draw an elliptical arc (shorter clockwise arc)
660   arcLargeCWTo: func me.addSegment(me.VG_LCWARC_TO_ABS, arg),
661   arcLargeCW:   func me.addSegment(me.VG_LCWARC_TO_REL, arg),
662   # Close the path (implicit lineTo to first point of path)
663   close: func me.addSegment(me.VG_CLOSE_PATH),
664
665   setStrokeLineWidth: func(width)
666   {
667     me.setDouble('stroke-width', width);
668   },
669   # Set stroke linecap
670   #
671   # @param linecap String, "butt", "round" or "square"
672   #
673   # See http://www.w3.org/TR/SVG/painting.html#StrokeLinecapProperty for details
674   setStrokeLineCap: func(linecap)
675   {
676     me.set('stroke-linecap', linecap);
677   },
678   # Set stroke dasharray
679   #
680   # @param pattern Vector, Vector of alternating dash and gap lengths
681   #  [on1, off1, on2, ...]
682   setStrokeDashArray: func(pattern)
683   {
684     me._node.removeChildren('stroke-dasharray');
685
686     if( typeof(pattern) == 'vector' )
687       me._node.setValues({'stroke-dasharray': pattern});
688     else
689       debug.warn("setStrokeDashArray: vector expected!");
690
691     return me;
692   },
693   # Set the fill color and enable filling this path
694   #
695   # @param color  Vector of 3 or 4 values in [0, 1]
696   setColorFill: func { _setColorNodes(me.color_fill, arg); me.setFill(1); },
697   # Enable/disable filling this path
698   setFill: func(fill)
699   {
700     me.setBool("fill", fill);
701   }
702 };
703
704 # Element factories used by #Group elements to create children
705 Group._element_factories = {
706   "group": Group.new,
707   "map": Map.new,
708   "text": Text.new,
709   "path": Path.new
710 };
711
712 # Canvas
713 # ==============================================================================
714 # Class for a canvas
715 #
716 var Canvas = {
717   # Place this canvas somewhere onto the object. Pass criterions for placement
718   # as a hash, eg:
719   #
720   #  my_canvas.addPlacement({
721   #    "texture": "EICAS.png",
722   #    "node": "PFD-Screen",
723   #    "parent": "Some parent name"
724   #  });
725   # 
726   # Note that we can choose whichever of the three filter criterions we use for
727   # matching the target object for our placement. If none of the three fields is
728   # given every texture of the model will be replaced.
729   addPlacement: func(vals)
730   {
731     var placement = _createNodeWithIndex(me.texture, "placement");
732     placement.setValues(vals);
733     return placement;
734   },
735   # Create a new group with the given name
736   #
737   # @param id Optional id/name for the group
738   createGroup: func(id = nil)
739   {
740     return Group.new(me.texture, id);
741   },
742   # Set the background color
743   #
744   # @param color  Vector of 3 or 4 values in [0, 1]
745   setColorBackground: func { _setColorNodes(me.color, arg); return me; }
746 };
747
748 # Create a new canvas. Pass parameters as hash, eg:
749 #
750 #  var my_canvas = canvas.new({
751 #    "name": "PFD-Test",
752 #    "size": [512, 512],
753 #    "view": [768, 1024],
754 #    "mipmapping": 1
755 #  });
756 var new = func(vals)
757 {
758   var m = { parents: [Canvas] };
759
760   m.texture = _createNodeWithIndex
761   (
762     props.globals.getNode("canvas", 1),
763     "texture"
764   );
765   m.color = _createColorNodes(m.texture, "color-background");
766   m.texture.setValues(vals);
767
768   return m;
769 };
770
771 # Get the first existing canvas with the given name
772 #
773 # @param name Name of the canvas
774 # @return #Canvas, if canvas with #name exists
775 #         nil, otherwise
776 var get = func(name)
777 {
778   var node_canvas = nil;
779   if( isa(name, props.Node) )
780     node_canvas = name;
781   else if( typeof(name) == 'scalar' )
782   {
783     var canvas_root = props.globals.getNode("canvas");
784     if( canvas_root == nil )
785       return nil;
786
787     foreach(var c; canvas_root.getChildren("texture"))
788     {
789       if( c.getValue("name") == name )
790         node_canvas = c;
791     }
792   }
793   
794   if( node_canvas == nil )
795   {
796     debug.warn("Canvas not found: " ~ name);
797     return nil;
798   }
799   
800   return {
801     parents: [Canvas],
802     texture: node_canvas,
803     color: _createColorNodes(node_canvas, "color-background")
804   };
805 };
806
807 # ------------------------------------------------------------------------------
808 # Show warnings if API used with too old version of FlightGear without Canvas
809 # support (Wrapped in anonymous function do not polute the canvas namespace)
810
811 (func {
812 var version_str = getprop("/sim/version/flightgear");
813 if( string.scanf(version_str, "%u.%u.%u", var fg_version = []) < 1 )
814   debug.warn("Canvas: Error parsing flightgear version (" ~ version_str ~ ")");
815 else
816 {
817   if(     fg_version[0] < 2
818       or (fg_version[0] == 2 and fg_version[1] < 8) )
819   {
820     debug.warn("Canvas: FlightGear version too old (" ~ version_str ~ ")");
821     gui.popupTip
822     (
823       "FlightGear v2.8.0 or newer needed for Canvas support!",
824       600,
825       {button: {legend: "Ok", binding: {command: "dialog-close"}}}
826     );
827   }
828 } })();