Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / draw / Surface.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * A Surface is an interface to render methods inside a draw {@link Ext.draw.Component}.
17  * A Surface contains methods to render sprites, get bounding boxes of sprites, add
18  * sprites to the canvas, initialize other graphic components, etc. One of the most used
19  * methods for this class is the `add` method, to add Sprites to the surface.
20  *
21  * Most of the Surface methods are abstract and they have a concrete implementation
22  * in VML or SVG engines.
23  *
24  * A Surface instance can be accessed as a property of a draw component. For example:
25  *
26  *     drawComponent.surface.add({
27  *         type: 'circle',
28  *         fill: '#ffc',
29  *         radius: 100,
30  *         x: 100,
31  *         y: 100
32  *     });
33  *
34  * The configuration object passed in the `add` method is the same as described in the {@link Ext.draw.Sprite}
35  * class documentation.
36  *
37  * # Listeners
38  *
39  * You can also add event listeners to the surface using the `Observable` listener syntax. Supported events are:
40  *
41  * - mousedown
42  * - mouseup
43  * - mouseover
44  * - mouseout
45  * - mousemove
46  * - mouseenter
47  * - mouseleave
48  * - click
49  *
50  * For example:
51  *
52  *     drawComponent.surface.on({
53  *        'mousemove': function() {
54  *             console.log('moving the mouse over the surface');
55  *         }
56  *     });
57  *
58  * # Example
59  *
60  *     var drawComponent = Ext.create('Ext.draw.Component', {
61  *         width: 800,
62  *         height: 600,
63  *         renderTo: document.body
64  *     }), surface = drawComponent.surface;
65  *
66  *     surface.add([{
67  *         type: 'circle',
68  *         radius: 10,
69  *         fill: '#f00',
70  *         x: 10,
71  *         y: 10,
72  *         group: 'circles'
73  *     }, {
74  *         type: 'circle',
75  *         radius: 10,
76  *         fill: '#0f0',
77  *         x: 50,
78  *         y: 50,
79  *         group: 'circles'
80  *     }, {
81  *         type: 'circle',
82  *         radius: 10,
83  *         fill: '#00f',
84  *         x: 100,
85  *         y: 100,
86  *         group: 'circles'
87  *     }, {
88  *         type: 'rect',
89  *         width: 20,
90  *         height: 20,
91  *         fill: '#f00',
92  *         x: 10,
93  *         y: 10,
94  *         group: 'rectangles'
95  *     }, {
96  *         type: 'rect',
97  *         width: 20,
98  *         height: 20,
99  *         fill: '#0f0',
100  *         x: 50,
101  *         y: 50,
102  *         group: 'rectangles'
103  *     }, {
104  *         type: 'rect',
105  *         width: 20,
106  *         height: 20,
107  *         fill: '#00f',
108  *         x: 100,
109  *         y: 100,
110  *         group: 'rectangles'
111  *     }]);
112  *
113  *     // Get references to my groups
114  *     circles = surface.getGroup('circles');
115  *     rectangles = surface.getGroup('rectangles');
116  *
117  *     // Animate the circles down
118  *     circles.animate({
119  *         duration: 1000,
120  *         to: {
121  *             translate: {
122  *                 y: 200
123  *             }
124  *         }
125  *     });
126  *
127  *     // Animate the rectangles across
128  *     rectangles.animate({
129  *         duration: 1000,
130  *         to: {
131  *             translate: {
132  *                 x: 200
133  *             }
134  *         }
135  *     });
136  */
137 Ext.define('Ext.draw.Surface', {
138
139     /* Begin Definitions */
140
141     mixins: {
142         observable: 'Ext.util.Observable'
143     },
144
145     requires: ['Ext.draw.CompositeSprite'],
146     uses: ['Ext.draw.engine.Svg', 'Ext.draw.engine.Vml'],
147
148     separatorRe: /[, ]+/,
149
150     statics: {
151         /**
152          * Creates and returns a new concrete Surface instance appropriate for the current environment.
153          * @param {Object} config Initial configuration for the Surface instance
154          * @param {String[]} enginePriority (Optional) order of implementations to use; the first one that is
155          * available in the current environment will be used. Defaults to `['Svg', 'Vml']`.
156          * @return {Object} The created Surface or false.
157          * @static
158          */
159         create: function(config, enginePriority) {
160             enginePriority = enginePriority || ['Svg', 'Vml'];
161
162             var i = 0,
163                 len = enginePriority.length,
164                 surfaceClass;
165
166             for (; i < len; i++) {
167                 if (Ext.supports[enginePriority[i]]) {
168                     return Ext.create('Ext.draw.engine.' + enginePriority[i], config);
169                 }
170             }
171             return false;
172         }
173     },
174
175     /* End Definitions */
176
177     // @private
178     availableAttrs: {
179         blur: 0,
180         "clip-rect": "0 0 1e9 1e9",
181         cursor: "default",
182         cx: 0,
183         cy: 0,
184         'dominant-baseline': 'auto',
185         fill: "none",
186         "fill-opacity": 1,
187         font: '10px "Arial"',
188         "font-family": '"Arial"',
189         "font-size": "10",
190         "font-style": "normal",
191         "font-weight": 400,
192         gradient: "",
193         height: 0,
194         hidden: false,
195         href: "http://sencha.com/",
196         opacity: 1,
197         path: "M0,0",
198         radius: 0,
199         rx: 0,
200         ry: 0,
201         scale: "1 1",
202         src: "",
203         stroke: "#000",
204         "stroke-dasharray": "",
205         "stroke-linecap": "butt",
206         "stroke-linejoin": "butt",
207         "stroke-miterlimit": 0,
208         "stroke-opacity": 1,
209         "stroke-width": 1,
210         target: "_blank",
211         text: "",
212         "text-anchor": "middle",
213         title: "Ext Draw",
214         width: 0,
215         x: 0,
216         y: 0,
217         zIndex: 0
218     },
219
220     /**
221      * @cfg {Number} height
222      * The height of this component in pixels (defaults to auto).
223      */
224     /**
225      * @cfg {Number} width
226      * The width of this component in pixels (defaults to auto).
227      */
228
229     container: undefined,
230     height: 352,
231     width: 512,
232     x: 0,
233     y: 0,
234
235     /**
236      * @private Flag indicating that the surface implementation requires sprites to be maintained
237      * in order of their zIndex. Impls that don't require this can set it to false.
238      */
239     orderSpritesByZIndex: true,
240
241
242     /**
243      * Creates new Surface.
244      * @param {Object} config (optional) Config object.
245      */
246     constructor: function(config) {
247         var me = this;
248         config = config || {};
249         Ext.apply(me, config);
250
251         me.domRef = Ext.getDoc().dom;
252
253         me.customAttributes = {};
254
255         me.addEvents(
256             'mousedown',
257             'mouseup',
258             'mouseover',
259             'mouseout',
260             'mousemove',
261             'mouseenter',
262             'mouseleave',
263             'click'
264         );
265
266         me.mixins.observable.constructor.call(me);
267
268         me.getId();
269         me.initGradients();
270         me.initItems();
271         if (me.renderTo) {
272             me.render(me.renderTo);
273             delete me.renderTo;
274         }
275         me.initBackground(config.background);
276     },
277
278     // @private called to initialize components in the surface
279     // this is dependent on the underlying implementation.
280     initSurface: Ext.emptyFn,
281
282     // @private called to setup the surface to render an item
283     //this is dependent on the underlying implementation.
284     renderItem: Ext.emptyFn,
285
286     // @private
287     renderItems: Ext.emptyFn,
288
289     // @private
290     setViewBox: function (x, y, width, height) {
291         if (isFinite(x) && isFinite(y) && isFinite(width) && isFinite(height)) {
292             this.viewBox = {x: x, y: y, width: width, height: height};
293             this.applyViewBox();
294         }
295     },
296
297     /**
298      * Adds one or more CSS classes to the element. Duplicate classes are automatically filtered out.
299      *
300      * For example:
301      *
302      *     drawComponent.surface.addCls(sprite, 'x-visible');
303      *
304      * @param {Object} sprite The sprite to add the class to.
305      * @param {String/String[]} className The CSS class to add, or an array of classes
306      * @method
307      */
308     addCls: Ext.emptyFn,
309
310     /**
311      * Removes one or more CSS classes from the element.
312      *
313      * For example:
314      *
315      *     drawComponent.surface.removeCls(sprite, 'x-visible');
316      *
317      * @param {Object} sprite The sprite to remove the class from.
318      * @param {String/String[]} className The CSS class to remove, or an array of classes
319      * @method
320      */
321     removeCls: Ext.emptyFn,
322
323     /**
324      * Sets CSS style attributes to an element.
325      *
326      * For example:
327      *
328      *     drawComponent.surface.setStyle(sprite, {
329      *         'cursor': 'pointer'
330      *     });
331      *
332      * @param {Object} sprite The sprite to add, or an array of classes to
333      * @param {Object} styles An Object with CSS styles.
334      * @method
335      */
336     setStyle: Ext.emptyFn,
337
338     // @private
339     initGradients: function() {
340         var gradients = this.gradients;
341         if (gradients) {
342             Ext.each(gradients, this.addGradient, this);
343         }
344     },
345
346     // @private
347     initItems: function() {
348         var items = this.items;
349         this.items = Ext.create('Ext.draw.CompositeSprite');
350         this.groups = Ext.create('Ext.draw.CompositeSprite');
351         if (items) {
352             this.add(items);
353         }
354     },
355
356     // @private
357     initBackground: function(config) {
358         var me = this,
359             width = me.width,
360             height = me.height,
361             gradientId, gradient, backgroundSprite;
362         if (config) {
363             if (config.gradient) {
364                 gradient = config.gradient;
365                 gradientId = gradient.id;
366                 me.addGradient(gradient);
367                 me.background = me.add({
368                     type: 'rect',
369                     x: 0,
370                     y: 0,
371                     width: width,
372                     height: height,
373                     fill: 'url(#' + gradientId + ')'
374                 });
375             } else if (config.fill) {
376                 me.background = me.add({
377                     type: 'rect',
378                     x: 0,
379                     y: 0,
380                     width: width,
381                     height: height,
382                     fill: config.fill
383                 });
384             } else if (config.image) {
385                 me.background = me.add({
386                     type: 'image',
387                     x: 0,
388                     y: 0,
389                     width: width,
390                     height: height,
391                     src: config.image
392                 });
393             }
394         }
395     },
396
397     /**
398      * Sets the size of the surface. Accomodates the background (if any) to fit the new size too.
399      *
400      * For example:
401      *
402      *     drawComponent.surface.setSize(500, 500);
403      *
404      * This method is generally called when also setting the size of the draw Component.
405      *
406      * @param {Number} w The new width of the canvas.
407      * @param {Number} h The new height of the canvas.
408      */
409     setSize: function(w, h) {
410         if (this.background) {
411             this.background.setAttributes({
412                 width: w,
413                 height: h,
414                 hidden: false
415             }, true);
416         }
417         this.applyViewBox();
418     },
419
420     // @private
421     scrubAttrs: function(sprite) {
422         var i,
423             attrs = {},
424             exclude = {},
425             sattr = sprite.attr;
426         for (i in sattr) {
427             // Narrow down attributes to the main set
428             if (this.translateAttrs.hasOwnProperty(i)) {
429                 // Translated attr
430                 attrs[this.translateAttrs[i]] = sattr[i];
431                 exclude[this.translateAttrs[i]] = true;
432             }
433             else if (this.availableAttrs.hasOwnProperty(i) && !exclude[i]) {
434                 // Passtrhough attr
435                 attrs[i] = sattr[i];
436             }
437         }
438         return attrs;
439     },
440
441     // @private
442     onClick: function(e) {
443         this.processEvent('click', e);
444     },
445
446     // @private
447     onMouseUp: function(e) {
448         this.processEvent('mouseup', e);
449     },
450
451     // @private
452     onMouseDown: function(e) {
453         this.processEvent('mousedown', e);
454     },
455
456     // @private
457     onMouseOver: function(e) {
458         this.processEvent('mouseover', e);
459     },
460
461     // @private
462     onMouseOut: function(e) {
463         this.processEvent('mouseout', e);
464     },
465
466     // @private
467     onMouseMove: function(e) {
468         this.fireEvent('mousemove', e);
469     },
470
471     // @private
472     onMouseEnter: Ext.emptyFn,
473
474     // @private
475     onMouseLeave: Ext.emptyFn,
476
477     /**
478      * Adds a gradient definition to the Surface. Note that in some surface engines, adding
479      * a gradient via this method will not take effect if the surface has already been rendered.
480      * Therefore, it is preferred to pass the gradients as an item to the surface config, rather
481      * than calling this method, especially if the surface is rendered immediately (e.g. due to
482      * 'renderTo' in its config). For more information on how to create gradients in the Chart
483      * configuration object please refer to {@link Ext.chart.Chart}.
484      *
485      * The gradient object to be passed into this method is composed by:
486      *
487      * - **id** - string - The unique name of the gradient.
488      * - **angle** - number, optional - The angle of the gradient in degrees.
489      * - **stops** - object - An object with numbers as keys (from 0 to 100) and style objects as values.
490      *
491      * For example:
492      *
493      *    drawComponent.surface.addGradient({
494      *        id: 'gradientId',
495      *        angle: 45,
496      *        stops: {
497      *            0: {
498      *                color: '#555'
499      *            },
500      *            100: {
501      *                color: '#ddd'
502      *            }
503      *        }
504      *    });
505      *
506      * @method
507      */
508     addGradient: Ext.emptyFn,
509
510     /**
511      * Adds a Sprite to the surface. See {@link Ext.draw.Sprite} for the configuration object to be
512      * passed into this method.
513      *
514      * For example:
515      *
516      *     drawComponent.surface.add({
517      *         type: 'circle',
518      *         fill: '#ffc',
519      *         radius: 100,
520      *         x: 100,
521      *         y: 100
522      *     });
523      *
524      */
525     add: function() {
526         var args = Array.prototype.slice.call(arguments),
527             sprite,
528             index;
529
530         var hasMultipleArgs = args.length > 1;
531         if (hasMultipleArgs || Ext.isArray(args[0])) {
532             var items = hasMultipleArgs ? args : args[0],
533                 results = [],
534                 i, ln, item;
535
536             for (i = 0, ln = items.length; i < ln; i++) {
537                 item = items[i];
538                 item = this.add(item);
539                 results.push(item);
540             }
541
542             return results;
543         }
544         sprite = this.prepareItems(args[0], true)[0];
545         this.insertByZIndex(sprite);
546         this.onAdd(sprite);
547         return sprite;
548     },
549
550     /**
551      * @private
552      * Inserts a given sprite into the correct position in the items collection, according to
553      * its zIndex. It will be inserted at the end of an existing series of sprites with the same or
554      * lower zIndex. By ensuring sprites are always ordered, this allows surface subclasses to render
555      * the sprites in the correct order for proper z-index stacking.
556      * @param {Ext.draw.Sprite} sprite
557      * @return {Number} the sprite's new index in the list
558      */
559     insertByZIndex: function(sprite) {
560         var me = this,
561             sprites = me.items.items,
562             len = sprites.length,
563             ceil = Math.ceil,
564             zIndex = sprite.attr.zIndex,
565             idx = len,
566             high = idx - 1,
567             low = 0,
568             otherZIndex;
569
570         if (me.orderSpritesByZIndex && len && zIndex < sprites[high].attr.zIndex) {
571             // Find the target index via a binary search for speed
572             while (low <= high) {
573                 idx = ceil((low + high) / 2);
574                 otherZIndex = sprites[idx].attr.zIndex;
575                 if (otherZIndex > zIndex) {
576                     high = idx - 1;
577                 }
578                 else if (otherZIndex < zIndex) {
579                     low = idx + 1;
580                 }
581                 else {
582                     break;
583                 }
584             }
585             // Step forward to the end of a sequence of the same or lower z-index
586             while (idx < len && sprites[idx].attr.zIndex <= zIndex) {
587                 idx++;
588             }
589         }
590
591         me.items.insert(idx, sprite);
592         return idx;
593     },
594
595     onAdd: function(sprite) {
596         var group = sprite.group,
597             draggable = sprite.draggable,
598             groups, ln, i;
599         if (group) {
600             groups = [].concat(group);
601             ln = groups.length;
602             for (i = 0; i < ln; i++) {
603                 group = groups[i];
604                 this.getGroup(group).add(sprite);
605             }
606             delete sprite.group;
607         }
608         if (draggable) {
609             sprite.initDraggable();
610         }
611     },
612
613     /**
614      * Removes a given sprite from the surface, optionally destroying the sprite in the process.
615      * You can also call the sprite own `remove` method.
616      *
617      * For example:
618      *
619      *     drawComponent.surface.remove(sprite);
620      *     //or...
621      *     sprite.remove();
622      *
623      * @param {Ext.draw.Sprite} sprite
624      * @param {Boolean} destroySprite
625      * @return {Number} the sprite's new index in the list
626      */
627     remove: function(sprite, destroySprite) {
628         if (sprite) {
629             this.items.remove(sprite);
630             this.groups.each(function(item) {
631                 item.remove(sprite);
632             });
633             sprite.onRemove();
634             if (destroySprite === true) {
635                 sprite.destroy();
636             }
637         }
638     },
639
640     /**
641      * Removes all sprites from the surface, optionally destroying the sprites in the process.
642      *
643      * For example:
644      *
645      *     drawComponent.surface.removeAll();
646      *
647      * @param {Boolean} destroySprites Whether to destroy all sprites when removing them.
648      * @return {Number} The sprite's new index in the list.
649      */
650     removeAll: function(destroySprites) {
651         var items = this.items.items,
652             ln = items.length,
653             i;
654         for (i = ln - 1; i > -1; i--) {
655             this.remove(items[i], destroySprites);
656         }
657     },
658
659     onRemove: Ext.emptyFn,
660
661     onDestroy: Ext.emptyFn,
662
663     /**
664      * @private Using the current viewBox property and the surface's width and height, calculate the
665      * appropriate viewBoxShift that will be applied as a persistent transform to all sprites.
666      */
667     applyViewBox: function() {
668         var me = this,
669             viewBox = me.viewBox,
670             width = me.width,
671             height = me.height,
672             viewBoxX, viewBoxY, viewBoxWidth, viewBoxHeight,
673             relativeHeight, relativeWidth, size;
674
675         if (viewBox && (width || height)) {
676             viewBoxX = viewBox.x;
677             viewBoxY = viewBox.y;
678             viewBoxWidth = viewBox.width;
679             viewBoxHeight = viewBox.height;
680             relativeHeight = height / viewBoxHeight;
681             relativeWidth = width / viewBoxWidth;
682
683             if (viewBoxWidth * relativeHeight < width) {
684                 viewBoxX -= (width - viewBoxWidth * relativeHeight) / 2 / relativeHeight;
685             }
686             if (viewBoxHeight * relativeWidth < height) {
687                 viewBoxY -= (height - viewBoxHeight * relativeWidth) / 2 / relativeWidth;
688             }
689
690             size = 1 / Math.min(viewBoxWidth, relativeHeight);
691
692             me.viewBoxShift = {
693                 dx: -viewBoxX,
694                 dy: -viewBoxY,
695                 scale: size
696             };
697         }
698     },
699
700     transformToViewBox: function (x, y) {
701         if (this.viewBoxShift) {
702             var me = this, shift = me.viewBoxShift;
703             return [x * shift.scale - shift.dx, y * shift.scale - shift.dy];
704         } else {
705             return [x, y];
706         }
707     },
708
709     // @private
710     applyTransformations: function(sprite) {
711             sprite.bbox.transform = 0;
712             this.transform(sprite);
713
714         var me = this,
715             dirty = false,
716             attr = sprite.attr;
717
718         if (attr.translation.x != null || attr.translation.y != null) {
719             me.translate(sprite);
720             dirty = true;
721         }
722         if (attr.scaling.x != null || attr.scaling.y != null) {
723             me.scale(sprite);
724             dirty = true;
725         }
726         if (attr.rotation.degrees != null) {
727             me.rotate(sprite);
728             dirty = true;
729         }
730         if (dirty) {
731             sprite.bbox.transform = 0;
732             this.transform(sprite);
733             sprite.transformations = [];
734         }
735     },
736
737     // @private
738     rotate: function (sprite) {
739         var bbox,
740             deg = sprite.attr.rotation.degrees,
741             centerX = sprite.attr.rotation.x,
742             centerY = sprite.attr.rotation.y;
743         if (!Ext.isNumber(centerX) || !Ext.isNumber(centerY)) {
744             bbox = this.getBBox(sprite);
745             centerX = !Ext.isNumber(centerX) ? bbox.x + bbox.width / 2 : centerX;
746             centerY = !Ext.isNumber(centerY) ? bbox.y + bbox.height / 2 : centerY;
747         }
748         sprite.transformations.push({
749             type: "rotate",
750             degrees: deg,
751             x: centerX,
752             y: centerY
753         });
754     },
755
756     // @private
757     translate: function(sprite) {
758         var x = sprite.attr.translation.x || 0,
759             y = sprite.attr.translation.y || 0;
760         sprite.transformations.push({
761             type: "translate",
762             x: x,
763             y: y
764         });
765     },
766
767     // @private
768     scale: function(sprite) {
769         var bbox,
770             x = sprite.attr.scaling.x || 1,
771             y = sprite.attr.scaling.y || 1,
772             centerX = sprite.attr.scaling.centerX,
773             centerY = sprite.attr.scaling.centerY;
774
775         if (!Ext.isNumber(centerX) || !Ext.isNumber(centerY)) {
776             bbox = this.getBBox(sprite);
777             centerX = !Ext.isNumber(centerX) ? bbox.x + bbox.width / 2 : centerX;
778             centerY = !Ext.isNumber(centerY) ? bbox.y + bbox.height / 2 : centerY;
779         }
780         sprite.transformations.push({
781             type: "scale",
782             x: x,
783             y: y,
784             centerX: centerX,
785             centerY: centerY
786         });
787     },
788
789     // @private
790     rectPath: function (x, y, w, h, r) {
791         if (r) {
792             return [["M", x + r, y], ["l", w - r * 2, 0], ["a", r, r, 0, 0, 1, r, r], ["l", 0, h - r * 2], ["a", r, r, 0, 0, 1, -r, r], ["l", r * 2 - w, 0], ["a", r, r, 0, 0, 1, -r, -r], ["l", 0, r * 2 - h], ["a", r, r, 0, 0, 1, r, -r], ["z"]];
793         }
794         return [["M", x, y], ["l", w, 0], ["l", 0, h], ["l", -w, 0], ["z"]];
795     },
796
797     // @private
798     ellipsePath: function (x, y, rx, ry) {
799         if (ry == null) {
800             ry = rx;
801         }
802         return [["M", x, y], ["m", 0, -ry], ["a", rx, ry, 0, 1, 1, 0, 2 * ry], ["a", rx, ry, 0, 1, 1, 0, -2 * ry], ["z"]];
803     },
804
805     // @private
806     getPathpath: function (el) {
807         return el.attr.path;
808     },
809
810     // @private
811     getPathcircle: function (el) {
812         var a = el.attr;
813         return this.ellipsePath(a.x, a.y, a.radius, a.radius);
814     },
815
816     // @private
817     getPathellipse: function (el) {
818         var a = el.attr;
819         return this.ellipsePath(a.x, a.y,
820                                 a.radiusX || (a.width / 2) || 0,
821                                 a.radiusY || (a.height / 2) || 0);
822     },
823
824     // @private
825     getPathrect: function (el) {
826         var a = el.attr;
827         return this.rectPath(a.x, a.y, a.width, a.height, a.r);
828     },
829
830     // @private
831     getPathimage: function (el) {
832         var a = el.attr;
833         return this.rectPath(a.x || 0, a.y || 0, a.width, a.height);
834     },
835
836     // @private
837     getPathtext: function (el) {
838         var bbox = this.getBBoxText(el);
839         return this.rectPath(bbox.x, bbox.y, bbox.width, bbox.height);
840     },
841
842     createGroup: function(id) {
843         var group = this.groups.get(id);
844         if (!group) {
845             group = Ext.create('Ext.draw.CompositeSprite', {
846                 surface: this
847             });
848             group.id = id || Ext.id(null, 'ext-surface-group-');
849             this.groups.add(group);
850         }
851         return group;
852     },
853
854     /**
855      * Returns a new group or an existent group associated with the current surface.
856      * The group returned is a {@link Ext.draw.CompositeSprite} group.
857      *
858      * For example:
859      *
860      *     var spriteGroup = drawComponent.surface.getGroup('someGroupId');
861      *
862      * @param {String} id The unique identifier of the group.
863      * @return {Object} The {@link Ext.draw.CompositeSprite}.
864      */
865     getGroup: function(id) {
866         if (typeof id == "string") {
867             var group = this.groups.get(id);
868             if (!group) {
869                 group = this.createGroup(id);
870             }
871         } else {
872             group = id;
873         }
874         return group;
875     },
876
877     // @private
878     prepareItems: function(items, applyDefaults) {
879         items = [].concat(items);
880         // Make sure defaults are applied and item is initialized
881         var item, i, ln;
882         for (i = 0, ln = items.length; i < ln; i++) {
883             item = items[i];
884             if (!(item instanceof Ext.draw.Sprite)) {
885                 // Temporary, just take in configs...
886                 item.surface = this;
887                 items[i] = this.createItem(item);
888             } else {
889                 item.surface = this;
890             }
891         }
892         return items;
893     },
894
895     /**
896      * Changes the text in the sprite element. The sprite must be a `text` sprite.
897      * This method can also be called from {@link Ext.draw.Sprite}.
898      *
899      * For example:
900      *
901      *     var spriteGroup = drawComponent.surface.setText(sprite, 'my new text');
902      *
903      * @param {Object} sprite The Sprite to change the text.
904      * @param {String} text The new text to be set.
905      * @method
906      */
907     setText: Ext.emptyFn,
908
909     //@private Creates an item and appends it to the surface. Called
910     //as an internal method when calling `add`.
911     createItem: Ext.emptyFn,
912
913     /**
914      * Retrieves the id of this component.
915      * Will autogenerate an id if one has not already been set.
916      */
917     getId: function() {
918         return this.id || (this.id = Ext.id(null, 'ext-surface-'));
919     },
920
921     /**
922      * Destroys the surface. This is done by removing all components from it and
923      * also removing its reference to a DOM element.
924      *
925      * For example:
926      *
927      *      drawComponent.surface.destroy();
928      */
929     destroy: function() {
930         delete this.domRef;
931         this.removeAll();
932     }
933 });