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