Canvas: remove API for FlightGear 2.8
[fg:toms-fgdata.git] / Nasal / canvas / api.nas
1 # Internal helper
2 var _getColor = func(color)
3 {
4   if( size(color) == 1 )
5     var color = color[0];
6
7   if( typeof(color) == 'scalar' )
8     return color;
9   if( typeof(color) != "vector" )
10     return debug.warn("Wrong type for color");
11
12   if( size(color) < 3 or size(color) > 4 )
13     return debug.warn("Color needs 3 or 4 values (RGB or RGBA)");
14
15   var str = 'rgb';
16   if( size(color) == 4 )
17     str ~= 'a';
18   str ~= '(';
19
20   # rgb = [0,255], a = [0,1]
21   for(var i = 0; i < size(color); i += 1)
22     str ~= (i > 0 ? ',' : '') ~ (i < 3 ? int(color[i] * 255) : color[i]);
23
24   return str ~ ')';
25 };
26
27 var _arg2valarray = func
28 {
29   var ret = arg;
30   while (    typeof(ret) == "vector"
31             and size(ret) == 1 and typeof(ret[0]) == "vector" )
32       ret = ret[0];
33   return ret;
34 }
35
36 # Transform
37 # ==============================================================================
38 # A transformation matrix which is used to transform an #Element on the canvas.
39 # The dimensions of the matrix are 3x3 where the last row is always 0 0 1:
40 #
41 #  a c e
42 #  b d f
43 #  0 0 1
44 #
45 # See http://www.w3.org/TR/SVG/coords.html#TransformMatrixDefined for details.
46 #
47 var Transform = {
48   new: func(node, vals = nil)
49   {
50     var m = {
51       parents: [Transform],
52       _node: node,
53       a: node.getNode("m[0]", 1),
54       b: node.getNode("m[1]", 1),
55       c: node.getNode("m[2]", 1),
56       d: node.getNode("m[3]", 1),
57       e: node.getNode("m[4]", 1),
58       f: node.getNode("m[5]", 1)
59     };
60
61     var use_vals = typeof(vals) == 'vector' and size(vals) == 6;
62
63     # initialize to identity matrix
64     m.a.setDoubleValue(use_vals ? vals[0] : 1);
65     m.b.setDoubleValue(use_vals ? vals[1] : 0);
66     m.c.setDoubleValue(use_vals ? vals[2] : 0);
67     m.d.setDoubleValue(use_vals ? vals[3] : 1);
68     m.e.setDoubleValue(use_vals ? vals[4] : 0);
69     m.f.setDoubleValue(use_vals ? vals[5] : 0);
70
71     return m;
72   },
73   setTranslation: func
74   {
75     var trans = _arg2valarray(arg);
76
77     me.e.setDoubleValue(trans[0]);
78     me.f.setDoubleValue(trans[1]);
79
80     return me;
81   },
82   # Set rotation (Optionally around a specified point instead of (0,0))
83   #
84   #  setRotation(rot)
85   #  setRotation(rot, cx, cy)
86   #
87   # @note If using with rotation center different to (0,0) don't use
88   #       #setTranslation as it would interfere with the rotation.
89   setRotation: func(angle)
90   {
91     var center = _arg2valarray(arg);
92
93     var s = math.sin(angle);
94     var c = math.cos(angle);
95
96     me.a.setDoubleValue(c);
97     me.b.setDoubleValue(s);
98     me.c.setDoubleValue(-s);
99     me.d.setDoubleValue(c);
100
101     if( size(center) == 2 )
102     {
103       me.e.setDoubleValue( (-center[0] * c) + (center[1] * s) + center[0] );
104       me.f.setDoubleValue( (-center[0] * s) - (center[1] * c) + center[1] );
105     }
106
107     return me;
108   },
109   # Set scale (either as parameters or array)
110   #
111   # If only one parameter is given its value is used for both x and y
112   #  setScale(x, y)
113   #  setScale([x, y])
114   setScale: func
115   {
116     var scale = _arg2valarray(arg);
117
118     me.a.setDoubleValue(scale[0]);
119     me.d.setDoubleValue(size(scale) >= 2 ? scale[1] : scale[0]);
120
121     return me;
122   },
123   getScale: func()
124   {
125     # TODO handle rotation
126     return [me.a.getValue(), me.d.getValue()];
127   }
128 };
129
130 # Element
131 # ==============================================================================
132 # Baseclass for all elements on a canvas
133 #
134 var Element = {
135   # Reference frames (for "clip" coordinates)
136   GLOBAL: 0,
137   PARENT: 1,
138   LOCAL:  2,
139
140   # Constructor
141   #
142   # @param ghost  Element ghost as retrieved from core methods
143   new: func(ghost)
144   {
145     return {
146       parents: [PropertyElement, Element, ghost],
147       _node: props.wrapNode(ghost._node_ghost)
148     };
149   },
150   # Get parent group/element
151   getParent: func()
152   {
153     var parent_ghost = me._getParent();
154     if( parent_ghost == nil )
155       return nil;
156
157     var type = props.wrapNode(parent_ghost._node_ghost).getName();
158     var factory = me._getFactory(type);
159     if( factory == nil )
160       return parent_ghost;
161
162     return factory(parent_ghost);
163   },
164   # Check if elements represent same instance
165   #
166   # @param el Other Element or element ghost
167   equals: func(el)
168   {
169     return me._node.equals(el._node_ghost);
170   },
171   # Trigger an update of the element
172   #
173   # Elements are automatically updated once a frame, with a delay of one frame.
174   # If you wan't to get an element updated in the current frame you have to use
175   # this method.
176   update: func()
177   {
178     me.setInt("update", 1);
179   },
180   # Hide/Show element
181   #
182   # @param visible  Whether the element should be visible
183   setVisible: func(visible = 1)
184   {
185     me.setBool("visible", visible);
186   },
187   getVisible: func me.getBool("visible"),
188   # Hide element (Shortcut for setVisible(0))
189   hide: func me.setVisible(0),
190   # Show element (Shortcut for setVisible(1))
191   show: func me.setVisible(1),
192   # Toggle element visibility
193   toggleVisibility: func me.setVisible( !me.getVisible() ),
194   #
195   setGeoPosition: func(lat, lon)
196   {
197     me._getTf()._node.getNode("m-geo[4]", 1).setValue("N" ~ lat);
198     me._getTf()._node.getNode("m-geo[5]", 1).setValue("E" ~ lon);
199     return me;
200   },
201   # Create a new transformation matrix
202   #
203   # @param vals Default values (Vector of 6 elements)
204   createTransform: func(vals = nil)
205   {
206     var node = me._node.addChild("tf", 1); # tf[0] is reserved for
207                                            # setRotation
208     return Transform.new(node, vals);
209   },
210   # Shortcut for setting translation
211   setTranslation: func { me._getTf().setTranslation(arg); return me; },
212   # Set rotation around transformation center (see #setCenter).
213   #
214   # @note This replaces the the existing transformation. For additional scale or
215   #       translation use additional transforms (see #createTransform).
216   setRotation: func(rot)
217   {
218     if( me['_tf_rot'] == nil )
219       # always use the first matrix slot to ensure correct rotation
220       # around transformation center.
221       me['_tf_rot'] = Transform.new(me._node.getNode("tf[0]", 1));
222
223     me._tf_rot.setRotation(rot, me.getCenter());
224     return me;
225   },
226   # Shortcut for setting scale
227   setScale: func { me._getTf().setScale(arg); return me; },
228   # Shortcut for getting scale
229   getScale: func me._getTf().getScale(),
230   # Set the fill/background/boundingbox color
231   #
232   # @param color  Vector of 3 or 4 values in [0, 1]
233   setColorFill: func me.set('fill', _getColor(arg)),
234   #
235   getBoundingBox: func()
236   {
237     var bb = me._node.getNode("bounding-box");
238     if( bb != nil )
239     {
240       var min_x = bb.getNode("min-x").getValue();
241
242       if( min_x != nil )
243         return [ min_x,
244                   bb.getNode("min-y").getValue(),
245                   bb.getNode("max-x").getValue(),
246                   bb.getNode("max-y").getValue() ];
247     }
248
249     return [0, 0, 0, 0];
250   },
251   # Calculate the transformation center based on bounding box and center-offset
252   updateCenter: func
253   {
254     me.update();
255     var bb = me.getTransformedBounds();
256
257     if( bb[0] > bb[2] or bb[1] > bb[3] )
258       return;
259
260     me._setupCenterNodes
261     (
262       (bb[0] + bb[2]) / 2 + (me.get("center-offset-x") or 0),
263       (bb[1] + bb[3]) / 2 + (me.get("center-offset-y") or 0)
264     );
265     return me;
266   },
267   # Set transformation center (currently only used for rotation)
268   setCenter: func()
269   {
270     var center = _arg2valarray(arg);
271     if( size(center) != 2 )
272       return debug.warn("invalid arg");
273
274     me._setupCenterNodes(center[0], center[1]);
275     return me;
276   },
277   # Get transformation center
278   getCenter: func()
279   {
280     var center = [0, 0];
281     me._setupCenterNodes();
282
283     if( me._center[0] != nil )
284       center[0] = me._center[0].getValue() or 0;
285     if( me._center[1] != nil )
286       center[1] = me._center[1].getValue() or 0;
287
288     return center;
289   },
290   # Internal Transform for convenience transform functions
291   _getTf: func
292   {
293     if( me['_tf'] == nil )
294       me['_tf'] = me.createTransform();
295     return me._tf;
296   },
297   _setupCenterNodes: func(cx = nil, cy = nil)
298   {
299     if( me["_center"] == nil )
300       me["_center"] = [
301         me._node.getNode("center[0]", cx != nil),
302         me._node.getNode("center[1]", cy != nil)
303       ];
304
305     if( cx != nil )
306       me._center[0].setDoubleValue(cx);
307     if( cy != nil )
308       me._center[1].setDoubleValue(cy);
309   }
310 };
311
312 # Group
313 # ==============================================================================
314 # Class for a group element on a canvas
315 #
316 var Group = {
317 # public:
318   new: func(ghost)
319   {
320     return { parents: [Group, Element.new(ghost)] };
321   },
322   # Create a child of given type with specified id.
323   # type can be group, text
324   createChild: func(type, id = nil)
325   {
326     var ghost = me._createChild(type, id);
327     var factory = me._getFactory(type);
328     if( factory == nil )
329       return ghost;
330
331     return factory(ghost);
332   },
333   # Create multiple children of given type
334   createChildren: func(type, count)
335   {
336     var factory = me._getFactory(type);
337     if( factory == nil )
338       return [];
339
340     var nodes = props._addChildren(me._node._g, [type, count, 0, 0]);
341     for(var i = 0; i < count; i += 1)
342       nodes[i] = factory( me._getChild(nodes[i]) );
343
344     return nodes;
345   },
346   # Create a path child drawing a (rounded) rectangle
347   #
348   # @param x    Position of left border
349   # @param y    Position of top border
350   # @param w    Width
351   # @param h    Height
352   # @param cfg  Optional settings (eg. {"border-top-radius": 5})
353   rect: func(x, y, w, h, cfg = nil)
354   {
355     return me.createChild("path").rect(x, y, w, h, cfg);
356   },
357   # Get a vector of all child elements
358   getChildren: func()
359   {
360     var children = [];
361
362     foreach(var c; me._node.getChildren())
363       if( me._isElementNode(c) )
364         append(children, me._wrapElement(c));
365
366     return children;
367   },
368   # Get first child with given id (breadth-first search)
369   #
370   # @note Use with care as it can take several miliseconds (for me eg. ~2ms).
371   #       TODO check with new C++ implementation
372   getElementById: func(id)
373   {
374     var ghost = me._getElementById(id);
375     if( ghost == nil )
376       return nil;
377
378     var node = props.wrapNode(ghost._node_ghost);
379     var factory = me._getFactory( node.getName() );
380     if( factory == nil )
381       return ghost;
382
383     return factory(ghost);
384   },
385   # Remove all children
386   removeAllChildren: func()
387   {
388     foreach(var type; keys(me._element_factories))
389       me._node.removeChildren(type, 0);
390     return me;
391   },
392 # private:
393   _isElementNode: func(el)
394   {
395     # element nodes have type NONE and valid element names (those in the factory
396     # list)
397     return el.getType() == "NONE"
398         and me._element_factories[ el.getName() ] != nil;
399   },
400   _wrapElement: func(node)
401   {
402     # Create element from existing node
403     return me._element_factories[ node.getName() ]( me._getChild(node._g) );
404   },
405   _getFactory: func(type)
406   {
407     var factory = me._element_factories[type];
408
409     if( factory == nil )
410       debug.dump("canvas.Group.createChild(): unknown type (" ~ type ~ ")");
411
412     return factory;
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   df_controller: nil,
423   new: func(ghost)
424   {
425     return { parents: [Map, Group.new(ghost)] };
426   },
427   del: func()
428   {
429     #print("canvas.Map.del()");
430     call(func {
431       me.controller.del(me);
432     }, var err=[]);
433     if (size(err)) {
434       debug.printerror(err);
435       setsize(err, 0);
436     }
437     call(func {
438       foreach (var l; me.layers)
439         call(l[0].del, nil, l[0]);
440       setsize(me.layers, 0);
441     }, err);
442     if (size(err)) {
443       debug.printerror(err);
444       setsize(err, 0);
445     }
446     # call inherited 'del'
447     me.parents = subvec(me.parents,1);
448     me.del();
449   },
450   setController: func(controller=nil)
451   {
452     if (controller == nil)
453       controller = Map.df_controller;
454     elsif (typeof(controller) != 'hash')
455       controller = Map.Controller.get(controller);
456     if (controller.parents[0] != Map.Controller)
457       die("OOP error");
458     me.controller = controller.new(me);
459
460     return me;
461   },
462   addLayer: func(factory, type_arg=nil, priority=nil)
463   {
464     if (!contains(me, "layers"))
465       me.layers = {};
466
467     if(contains(me.layers, type_arg))
468       print("addLayer() warning: overwriting existing layer:", type_arg);
469
470     # print("addLayer():", type_arg);
471
472     # Argument handling
473     if (type_arg != nil)
474       var type = factory.get(type_arg);
475     else var type = factory;
476
477     me.layers[type_arg]= type.new(me);
478     if (priority == nil)
479       priority = type.df_priority;
480     if (priority != nil)
481       me.layers[type_arg].setInt("z-index", priority);
482     return me;
483   },
484   getLayer: func(type_arg) me.layers[type_arg],
485   setPos: func(lat, lon, hdg=nil, range=nil)
486   {
487     me.set("ref-lat", lat);
488     me.set("ref-lon", lon);
489     if (hdg != nil)
490       me.set("hdg", hdg);
491     if (range != nil)
492       me.set("range", range);
493   },
494   # Update each layer on this Map. Called by
495   # me.controller.
496   update: func
497   {
498     foreach (var l; keys(me.layers)) {
499       var layer = me.layers[l];
500       call(layer.update, arg, layer);
501     }
502     return me;
503   },
504 };
505
506 # Text
507 # ==============================================================================
508 # Class for a text element on a canvas
509 #
510 var Text = {
511   new: func(ghost)
512   {
513     return { parents: [Text, Element.new(ghost)] };
514   },
515   # Set the text
516   setText: func(text)
517   {
518     me.set("text", typeof(text) == 'scalar' ? text : "");
519   },
520   # Set alignment
521   #
522   #  @param align String, one of:
523   #   left-top
524   #   left-center
525   #   left-bottom
526   #   center-top
527   #   center-center
528   #   center-bottom
529   #   right-top
530   #   right-center
531   #   right-bottom
532   #   left-baseline
533   #   center-baseline
534   #   right-baseline
535   #   left-bottom-baseline
536   #   center-bottom-baseline
537   #   right-bottom-baseline
538   #
539   setAlignment: func(align)
540   {
541     me.set("alignment", align);
542   },
543   # Set the font size
544   setFontSize: func(size, aspect = 1)
545   {
546     me.setDouble("character-size", size);
547     me.setDouble("character-aspect-ratio", aspect);
548   },
549   # Set font (by name of font file)
550   setFont: func(name)
551   {
552     me.set("font", name);
553   },
554   # Enumeration of values for drawing mode:
555   TEXT:               1, # The text itself
556   BOUNDINGBOX:        2, # A bounding box (only lines)
557   FILLEDBOUNDINGBOX:  4, # A filled bounding box
558   ALIGNMENT:          8, # Draw a marker (cross) at the position of the text
559   # Set draw mode. Binary combination of the values above. Since I haven't found
560   # a bitwise or we have to use a + instead.
561   #
562   #  eg. my_text.setDrawMode(Text.TEXT + Text.BOUNDINGBOX);
563   setDrawMode: func(mode)
564   {
565     me.setInt("draw-mode", mode);
566   },
567   # Set bounding box padding
568   setPadding: func(pad)
569   {
570     me.setDouble("padding", pad);
571   },
572   setMaxWidth: func(w)
573   {
574     me.setDouble("max-width", w);
575   },
576   setColor: func me.set('fill', _getColor(arg)),
577   setColorFill: func me.set('background', _getColor(arg))
578 };
579
580 # Path
581 # ==============================================================================
582 # Class for an (OpenVG) path element on a canvas
583 #
584 var Path = {
585   # Path segment commands (VGPathCommand)
586   VG_CLOSE_PATH:     0,
587   VG_MOVE_TO:        2,
588   VG_MOVE_TO_ABS:    2,
589   VG_MOVE_TO_REL:    3,
590   VG_LINE_TO:        4,
591   VG_LINE_TO_ABS:    4,
592   VG_LINE_TO_REL:    5,
593   VG_HLINE_TO:       6,
594   VG_HLINE_TO_ABS:   6,
595   VG_HLINE_TO_REL:   7,
596   VG_VLINE_TO:       8,
597   VG_VLINE_TO_ABS:   8,
598   VG_VLINE_TO_REL:   9,
599   VG_QUAD_TO:       10,
600   VG_QUAD_TO_ABS:   10,
601   VG_QUAD_TO_REL:   11,
602   VG_CUBIC_TO:      12,
603   VG_CUBIC_TO_ABS:  12,
604   VG_CUBIC_TO_REL:  13,
605   VG_SQUAD_TO:      14,
606   VG_SQUAD_TO_ABS:  14,
607   VG_SQUAD_TO_REL:  15,
608   VG_SCUBIC_TO:     16,
609   VG_SCUBIC_TO_ABS: 16,
610   VG_SCUBIC_TO_REL: 17,
611   VG_SCCWARC_TO:    20, # Note that CC and CCW commands are swapped. This is
612   VG_SCCWARC_TO_ABS:20, # needed  due to the different coordinate systems used.
613   VG_SCCWARC_TO_REL:21, # In OpenVG values along the y-axis increase from bottom
614   VG_SCWARC_TO:     18, # to top, whereas in the Canvas system it is flipped.
615   VG_SCWARC_TO_ABS: 18,
616   VG_SCWARC_TO_REL: 19,
617   VG_LCCWARC_TO:    24,
618   VG_LCCWARC_TO_ABS:24,
619   VG_LCCWARC_TO_REL:25,
620   VG_LCWARC_TO:     22,
621   VG_LCWARC_TO_ABS: 22,
622   VG_LCWARC_TO_REL: 23,
623
624   # Number of coordinates per command
625   num_coords: [
626     0, 0, # VG_CLOSE_PATH
627     2, 2, # VG_MOVE_TO
628     2, 2, # VG_LINE_TO
629     1, 1, # VG_HLINE_TO
630     1, 1, # VG_VLINE_TO
631     4, 4, # VG_QUAD_TO
632     6, 6, # VG_CUBIC_TO
633     2, 2, # VG_SQUAD_TO
634     4, 4, # VG_SCUBIC_TO
635     5, 5, # VG_SCCWARC_TO
636     5, 5, # VG_SCWARC_TO
637     5, 5, # VG_LCCWARC_TO
638     5, 5  # VG_LCWARC_TO
639   ],
640
641   #
642   new: func(ghost)
643   {
644     return {
645       parents: [Path, Element.new(ghost)],
646       _first_cmd: 0,
647       _first_coord: 0,
648       _last_cmd: -1,
649       _last_coord: -1
650     };
651   },
652   # Remove all existing path data
653   reset: func
654   {
655     me._node.removeChildren('cmd', 0);
656     me._node.removeChildren('coord', 0);
657     me._node.removeChildren('coord-geo', 0);
658     me._first_cmd = 0;
659     me._first_coord = 0;
660     me._last_cmd = -1;
661     me._last_coord = -1;
662     return me;
663   },
664   # Set the path data (commands and coordinates)
665   setData: func(cmds, coords)
666   {
667     me.reset();
668     me._node.setValues({cmd: cmds, coord: coords});
669     me._last_cmd = size(cmds) - 1;
670     me._last_coord = size(coords) - 1;
671     return me;
672   },
673   setDataGeo: func(cmds, coords)
674   {
675     me.reset();
676     me._node.setValues({cmd: cmds, 'coord-geo': coords});
677     me._last_cmd = size(cmds) - 1;
678     me._last_coord = size(coords) - 1;
679     return me;
680   },
681   # Add a path segment
682   addSegment: func(cmd, coords...)
683   {
684     var coords = _arg2valarray(coords);
685     var num_coords = me.num_coords[cmd];
686     if( size(coords) != num_coords )
687       debug.warn
688       (
689         "Invalid number of arguments (expected " ~ num_coords ~ ")"
690       );
691     else
692     {
693       me.setInt("cmd[" ~ (me._last_cmd += 1) ~ "]", cmd);
694       for(var i = 0; i < num_coords; i += 1)
695         me.setDouble("coord[" ~ (me._last_coord += 1) ~ "]", coords[i]);
696     }
697
698     return me;
699   },
700   # Remove first segment
701   pop_front: func me._removeSegment(1),
702   # Remove last segment
703   pop_back: func me._removeSegment(0),
704   # Get the number of segments
705   getNumSegments: func()
706   {
707     return me._last_cmd - me._first_cmd + 1;
708   },
709   # Get the number of coordinates (each command has 0..n coords)
710   getNumCoords: func()
711   {
712     return me._last_coord - me._first_coord + 1;
713   },
714   # Move path cursor
715   moveTo: func me.addSegment(me.VG_MOVE_TO_ABS, arg),
716   move:   func me.addSegment(me.VG_MOVE_TO_REL, arg),
717   # Add a line
718   lineTo: func me.addSegment(me.VG_LINE_TO_ABS, arg),
719   line:   func me.addSegment(me.VG_LINE_TO_REL, arg),
720   # Add a horizontal line
721   horizTo: func me.addSegment(me.VG_HLINE_TO_ABS, arg),
722   horiz:   func me.addSegment(me.VG_HLINE_TO_REL, arg),
723   # Add a vertical line
724   vertTo: func me.addSegment(me.VG_VLINE_TO_ABS, arg),
725   vert:   func me.addSegment(me.VG_VLINE_TO_REL, arg),
726   # Add a quadratic Bézier curve
727   quadTo: func me.addSegment(me.VG_QUAD_TO_ABS, arg),
728   quad:   func me.addSegment(me.VG_QUAD_TO_REL, arg),
729   # Add a cubic Bézier curve
730   cubicTo: func me.addSegment(me.VG_CUBIC_TO_ABS, arg),
731   cubic:   func me.addSegment(me.VG_CUBIC_TO_REL, arg),
732   # Add a smooth quadratic Bézier curve
733   squadTo: func me.addSegment(me.VG_SQUAD_TO_ABS, arg),
734   squad:   func me.addSegment(me.VG_SQUAD_TO_REL, arg),
735   # Add a smooth cubic Bézier curve
736   scubicTo: func me.addSegment(me.VG_SCUBIC_TO_ABS, arg),
737   scubic:   func me.addSegment(me.VG_SCUBIC_TO_REL, arg),
738   # Draw an elliptical arc (shorter counter-clockwise arc)
739   arcSmallCCWTo: func me.addSegment(me.VG_SCCWARC_TO_ABS, arg),
740   arcSmallCCW:   func me.addSegment(me.VG_SCCWARC_TO_REL, arg),
741   # Draw an elliptical arc (shorter clockwise arc)
742   arcSmallCWTo: func me.addSegment(me.VG_SCWARC_TO_ABS, arg),
743   arcSmallCW:   func me.addSegment(me.VG_SCWARC_TO_REL, arg),
744   # Draw an elliptical arc (longer counter-clockwise arc)
745   arcLargeCCWTo: func me.addSegment(me.VG_LCCWARC_TO_ABS, arg),
746   arcLargeCCW:   func me.addSegment(me.VG_LCCWARC_TO_REL, arg),
747   # Draw an elliptical arc (shorter clockwise arc)
748   arcLargeCWTo: func me.addSegment(me.VG_LCWARC_TO_ABS, arg),
749   arcLargeCW:   func me.addSegment(me.VG_LCWARC_TO_REL, arg),
750   # Close the path (implicit lineTo to first point of path)
751   close: func me.addSegment(me.VG_CLOSE_PATH),
752
753   # Add a (rounded) rectangle to the path
754   #
755   # @param x    Position of left border
756   # @param y    Position of top border
757   # @param w    Width
758   # @param h    Height
759   # @param cfg  Optional settings (eg. {"border-top-radius": 5})
760   rect: func(x, y, w, h, cfg = nil)
761   {
762     var opts = (cfg != nil) ? cfg : {};
763
764     # resolve border-[top-,bottom-][left-,right-]radius
765     var br = opts["border-radius"];
766     if( typeof(br) == 'scalar' )
767       br = [br, br];
768
769     var _parseRadius = func(id)
770     {
771       if( (var r = opts["border-" ~ id ~ "-radius"]) == nil )
772       {
773         # parse top, bottom, left, right separate if no value specified for
774         # single corner
775         foreach(var s; ["top", "bottom", "left", "right"])
776         {
777           if( id.starts_with(s ~ "-") )
778           {
779             r = opts["border-" ~ s ~ "-radius"];
780             break;
781           }
782         }
783       }
784
785       if( r == nil )
786         return br;
787       else if( typeof(r) == 'scalar' )
788         return [r, r];
789       else
790         return r;
791     };
792
793     # top-left
794     if( (var r = _parseRadius("top-left")) != nil )
795     {
796       me.moveTo(x, y + r[1])
797         .arcSmallCWTo(r[0], r[1], 0, x + r[0], y);
798     }
799     else
800       me.moveTo(x, y);
801
802     # top-right
803     if( (r = _parseRadius("top-right")) != nil )
804     {
805       me.horizTo(x + w - r[0])
806         .arcSmallCWTo(r[0], r[1], 0, x + w, y + r[1]);
807     }
808     else
809       me.horizTo(x + w);
810
811     # bottom-right
812     if( (r = _parseRadius("bottom-right")) != nil )
813     {
814       me.vertTo(y + h - r[1])
815         .arcSmallCWTo(r[0], r[1], 0, x + w - r[0], y + h);
816     }
817     else
818       me.vertTo(y + h);
819
820     # bottom-left
821     if( (r = _parseRadius("bottom-left")) != nil )
822     {
823       me.horizTo(x + r[0])
824         .arcSmallCWTo(r[0], r[1], 0, x, y + h - r[1]);
825     }
826     else
827       me.horizTo(x);
828
829     return me.close();
830   },
831
832   setColor: func me.setStroke(_getColor(arg)),
833   setColorFill: func me.setFill(_getColor(arg)),
834
835   setFill: func(fill)
836   {
837     me.set('fill', fill);
838   },
839   setStroke: func(stroke)
840   {
841     me.set('stroke', stroke);
842   },
843   setStrokeLineWidth: func(width)
844   {
845     me.setDouble('stroke-width', width);
846   },
847   # Set stroke linecap
848   #
849   # @param linecap String, "butt", "round" or "square"
850   #
851   # See http://www.w3.org/TR/SVG/painting.html#StrokeLinecapProperty for details
852   setStrokeLineCap: func(linecap)
853   {
854     me.set('stroke-linecap', linecap);
855   },
856   # Set stroke dasharray
857   #
858   # @param pattern Vector, Vector of alternating dash and gap lengths
859   #  [on1, off1, on2, ...]
860   setStrokeDashArray: func(pattern)
861   {
862     if( typeof(pattern) == 'vector' )
863       me.set('stroke-dasharray', string.join(',', pattern));
864     else
865       debug.warn("setStrokeDashArray: vector expected!");
866
867     return me;
868   },
869
870 # private:
871   _removeSegment: func(front)
872   {
873     if( me.getNumSegments() < 1 )
874     {
875       debug.warn("No segment available");
876       return me;
877     }
878
879     var cmd = front ? me._first_cmd : me._last_cmd;
880     var num_coords = me.num_coords[ me.get("cmd[" ~ cmd ~ "]") ];
881     if( me.getNumCoords() < num_coords )
882     {
883       debug.warn("To few coords available");
884     }
885
886     me._node.removeChild("cmd", cmd);
887
888     var first_coord = front ? me._first_coord : me._last_coord - num_coords + 1;
889     for(var i = 0; i < num_coords; i += 1)
890       me._node.removeChild("coord", first_coord + i);
891
892     if( front )
893     {
894       me._first_cmd += 1;
895       me._first_coord += num_coords;
896     }
897     else
898     {
899       me._last_cmd -= 1;
900       me._last_coord -= num_coords;
901     }
902
903     return me;
904   },
905 };
906
907 # Image
908 # ==============================================================================
909 # Class for an image element on a canvas
910 #
911 var Image = {
912   new: func(ghost)
913   {
914     return {parents: [Image, Element.new(ghost)]};
915   },
916   # Set image file to be used
917   #
918   # @param file Path to file or canvas (Use canvas://... for canvas, eg.
919   #             canvas://by-index/texture[0])
920   setFile: func(file)
921   {
922     me.set("file", file);
923   },
924   # Set rectangular region of source image to be used
925   #
926   # @param left   Rectangle minimum x coordinate
927   # @param top    Rectangle minimum y coordinate
928   # @param right  Rectangle maximum x coordinate
929   # @param bottom Rectangle maximum y coordinate
930   # @param normalized Whether to use normalized ([0,1]) or image
931   #                   ([0, image_width]/[0, image_height]) coordinates
932   setSourceRect: func(left, top, right, bottom, normalized = 1)
933   {
934     me._node.getNode("source", 1).setValues({
935       left: left,
936       top: top,
937       right: right,
938       bottom: bottom,
939       normalized: normalized
940     });
941     return me;
942   },
943   # Set size of image element
944   #
945   # @param width
946   # @param height
947   # - or -
948   # @param size ([width, height])
949   setSize: func
950   {
951     me._node.setValues({size: _arg2valarray(arg)});
952     return me;
953   }
954 };
955
956 # Element factories used by #Group elements to create children
957 Group._element_factories = {
958   "group": Group.new,
959   "map": Map.new,
960   "text": Text.new,
961   "path": Path.new,
962   "image": Image.new
963 };
964
965 # Canvas
966 # ==============================================================================
967 # Class for a canvas
968 #
969 var Canvas = {
970   # Place this canvas somewhere onto the object. Pass criterions for placement
971   # as a hash, eg:
972   #
973   #  my_canvas.addPlacement({
974   #    "texture": "EICAS.png",
975   #    "node": "PFD-Screen",
976   #    "parent": "Some parent name"
977   #  });
978   #
979   # Note that we can choose whichever of the three filter criterions we use for
980   # matching the target object for our placement. If none of the three fields is
981   # given every texture of the model will be replaced.
982   addPlacement: func(vals)
983   {
984     var placement = me.texture.addChild("placement", 0, 0);
985     placement.setValues(vals);
986     return placement;
987   },
988   # Create a new group with the given name
989   #
990   # @param id Optional id/name for the group
991   createGroup: func(id = nil)
992   {
993     return Group.new(me._createGroup(id));
994   },
995   # Get the group with the given name
996   getGroup: func(id)
997   {
998     return Group.new(me._getGroup(id));
999   },
1000   # Set the background color
1001   #
1002   # @param color  Vector of 3 or 4 values in [0, 1]
1003   setColorBackground: func () { me.texture.getNode('background', 1).setValue(_getColor(arg)); me; },
1004   # Get path of canvas to be used eg. in Image::setFile
1005   getPath: func()
1006   {
1007     return "canvas://by-index/texture[" ~ me.texture.getIndex() ~ "]";
1008   },
1009   # Destructor
1010   #
1011   # releases associated canvas and makes this object unusable
1012   del: func
1013   {
1014     me.texture.remove();
1015     me.parents = nil; # ensure all ghosts get destroyed
1016   }
1017 };
1018
1019 var wrapCanvas = func(canvas_ghost)
1020 {
1021   var m = {
1022     parents: [PropertyElement, Canvas, canvas_ghost],
1023     texture: props.wrapNode(canvas_ghost._node_ghost)
1024   };
1025   m._node = m.texture;
1026   return m;
1027 }
1028
1029 # Create a new canvas. Pass parameters as hash, eg:
1030 #
1031 #  var my_canvas = canvas.new({
1032 #    "name": "PFD-Test",
1033 #    "size": [512, 512],
1034 #    "view": [768, 1024],
1035 #    "mipmapping": 1
1036 #  });
1037 var new = func(vals)
1038 {
1039   var m = wrapCanvas(_newCanvasGhost());
1040   m.texture.setValues(vals);
1041   return m;
1042 };
1043
1044 # Get the first existing canvas with the given name
1045 #
1046 # @param name Name of the canvas
1047 # @return #Canvas, if canvas with #name exists
1048 #         nil, otherwise
1049 var get = func(arg)
1050 {
1051   if( isa(arg, props.Node) )
1052     var node = arg;
1053   else if( typeof(arg) == "hash" )
1054     var node = props.Node.new(arg);
1055   else
1056     die("canvas.new: Invalid argument.");
1057
1058   var canvas_ghost = _getCanvasGhost(node._g);
1059   if( canvas_ghost == nil )
1060     return nil;
1061
1062   return wrapCanvas(canvas_ghost);
1063 };
1064
1065 var getDesktop = func()
1066 {
1067   return Group.new(_getDesktopGhost());
1068 };