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