/*
 * @class Ext.draw.engine.SVG
 * @extends Ext.draw.Surface
 * Provides specific methods to draw with SVG.
 */
Ext.define('Ext.draw.engine.SVG', {

    /* Begin Definitions */

    extend: 'Ext.draw.Surface',

    requires: ['Ext.draw.Draw', 'Ext.draw.Sprite', 'Ext.draw.Matrix', 'Ext.core.Element'],

    /* End Definitions */

    engine: 'SVG',

    trimRe: /^\s+|\s+$/g,
    spacesRe: /\s+/,
    xlink: "http:/" + "/www.w3.org/1999/xlink",

    translateAttrs: {
        radius: "r",
        radiusX: "rx",
        radiusY: "ry",
        path: "d",
        lineWidth: "stroke-width",
        fillOpacity: "fill-opacity",
        strokeOpacity: "stroke-opacity",
        strokeLinejoin: "stroke-linejoin"
    },

    minDefaults: {
        circle: {
            cx: 0,
            cy: 0,
            r: 0,
            fill: "none",
            stroke: null,
            "stroke-width": null,
            opacity: null,
            "fill-opacity": null,
            "stroke-opacity": null
        },
        ellipse: {
            cx: 0,
            cy: 0,
            rx: 0,
            ry: 0,
            fill: "none",
            stroke: null,
            "stroke-width": null,
            opacity: null,
            "fill-opacity": null,
            "stroke-opacity": null
        },
        rect: {
            x: 0,
            y: 0,
            width: 0,
            height: 0,
            rx: 0,
            ry: 0,
            fill: "none",
            stroke: null,
            "stroke-width": null,
            opacity: null,
            "fill-opacity": null,
            "stroke-opacity": null
        },
        text: {
            x: 0,
            y: 0,
            "text-anchor": "start",
            "font-family": null,
            "font-size": null,
            "font-weight": null,
            "font-style": null,
            fill: "#000",
            stroke: null,
            "stroke-width": null,
            opacity: null,
            "fill-opacity": null,
            "stroke-opacity": null
        },
        path: {
            d: "M0,0",
            fill: "none",
            stroke: null,
            "stroke-width": null,
            opacity: null,
            "fill-opacity": null,
            "stroke-opacity": null
        },
        image: {
            x: 0,
            y: 0,
            width: 0,
            height: 0,
            preserveAspectRatio: "none",
            opacity: null
        }
    },

    createSVGElement: function(type, attrs) {
        var el = this.domRef.createElementNS("http:/" + "/www.w3.org/2000/svg", type),
            key;
        if (attrs) {
            for (key in attrs) {
                el.setAttribute(key, String(attrs[key]));
            }
        }
        return el;
    },

    createSprite: function(sprite) {
        // Create svg element and append to the DOM.
        var el = this.createSVGElement(sprite.type);
        el.id = sprite.id;
        el.style.webkitTapHighlightColor = "rgba(0,0,0,0)";
        sprite.el = Ext.get(el);
        this.applyZIndex(sprite); //performs the insertion
        sprite.matrix = new Ext.draw.Matrix;
        sprite.bbox = {
            plain: 0,
            transform: 0
        };
        sprite.fireEvent("render", sprite);
        return el;
    },

    getBBox: function (sprite, isWithoutTransform) {
        var realPath = this["getPath" + sprite.type](sprite);
        if (isWithoutTransform) {
            sprite.bbox.plain = sprite.bbox.plain || Ext.draw.Draw.pathDimensions(realPath);
            return sprite.bbox.plain;
        }
        sprite.bbox.transform = sprite.bbox.transform || Ext.draw.Draw.pathDimensions(Ext.draw.Draw.mapPath(realPath, sprite.matrix));
        return sprite.bbox.transform;
    },
    
    getBBoxText: function (sprite) {
        var bbox = {},
            bb, height, width, i, ln, el;
        if (sprite && sprite.el) {
            el = sprite.el.dom;
            try {
                bbox = el.getBBox();
                return bbox;
            } catch(e) {
                // Firefox 3.0.x plays badly here
            }
            bbox = {x: bbox.x, y: Infinity, width: 0, height: 0};
            ln = el.getNumberOfChars();
            for (i = 0; i < ln; i++) {
                bb = el.getExtentOfChar(i);
                bbox.y = Math.min(bb.y, bbox.y);
                height = bb.y + bb.height - bbox.y;
                bbox.height = Math.max(bbox.height, height);
                width = bb.x + bb.width - bbox.x;
                bbox.width = Math.max(bbox.width, width);
            }
            return bbox;
        }
    },

    hide: function() {
        Ext.get(this.el).hide();
    },

    show: function() {
        Ext.get(this.el).show();
    },

    // Currently we'll only use the class attribute for visibility.  If we need more functionality we'll need to port
    // over addCls/removeCls from Element.
    hidePrim: function(sprite) {
        this.addCls(sprite, Ext.baseCSSPrefix + 'hide-visibility');
    },

    showPrim: function(sprite) {
        this.removeCls(sprite, Ext.baseCSSPrefix + 'hide-visibility');
    },

    getDefs: function() {
        return this._defs || (this._defs = this.createSVGElement("defs"));
    },

    transform: function(sprite) {
        var me = this,
            matrix = new Ext.draw.Matrix,
            transforms = sprite.transformations,
            transformsLength = transforms.length,
            i = 0,
            transform, type;

        for (; i < transformsLength; i++) {
            transform = transforms[i];
            type = transform.type;
            if (type == "translate") {
                matrix.translate(transform.x, transform.y);
            }
            else if (type == "rotate") {
                matrix.rotate(transform.degrees, transform.x, transform.y);
            }
            else if (type == "scale") {
                matrix.scale(transform.x, transform.y, transform.centerX, transform.centerY);
            }
        }
        sprite.matrix = matrix;
        sprite.el.set({transform: matrix.toSVG()});
    },

    setSize: function(w, h) {
        w = +w || this.width;
        h = +h || this.height;
        this.width = w;
        this.height = h;

        var el = this.el;
        el.setSize(w, h);
        el.set({
            width: w,
            height: h
        });
        Ext.draw.engine.SVG.superclass.setSize.call(this, w, h);
    },

    
/** * Get the region for the surface's canvas area * @returns {Ext.util.Region} */ getRegion: function() { // Mozilla requires using the background rect because the svg element returns an // incorrect region. Webkit gives no region for the rect and must use the svg element. var svgXY = this.el.getXY(), rectXY = this.bgRect.getXY(), max = Math.max, x = max(svgXY[0], rectXY[0]), y = max(svgXY[1], rectXY[1]); return { left: x, top: y, right: x + this.width, bottom: y + this.height }; }, onRemove: function(item) { if (item.el) { Ext.removeNode(item.el); delete item.el; } Ext.draw.engine.SVG.superclass.onRemove.call(this); }, setViewBox: function(x, y, width, height) { Ext.draw.engine.SVG.superclass.setViewBox.call(this); this.el.dom.setAttribute("viewBox", [x, y, width, height].join(" ")); }, render: function (container) { if (!this.el) { var width = this.width || 10, height = this.height || 10, el = this.createSVGElement('svg', { xmlns: "http:/" + "/www.w3.org/2000/svg", version: 1.1, width: width, height: height }), defs = this.getDefs(), // Create a rect that is always the same size as the svg root; this serves 2 purposes: // (1) It allows mouse events to be fired over empty areas in Webkit, and (2) we can // use it rather than the svg element for retrieving the correct client rect of the // surface in Mozilla (see https://bugzilla.mozilla.org/show_bug.cgi?id=530985) bgRect = this.createSVGElement("rect", { width: "100%", height: "100%", fill: "#000", stroke: "none", opacity: 0 }); el.appendChild(defs); el.appendChild(bgRect); container.appendChild(el); this.el = Ext.get(el); this.bgRect = Ext.get(bgRect); this.el.on({ scope: this, mouseup: this.onMouseUp, mousedown: this.onMouseDown, mouseover: this.onMouseOver, mouseout: this.onMouseOut, mousemove: this.onMouseMove, mouseenter: this.onMouseEnter, mouseleave: this.onMouseLeave, click: this.onClick }); } this.renderAll(); }, // private onMouseEnter: function(e) { if (this.bgRect.getRegion().contains(e.getPoint())) { this.fireEvent('mouseenter', e); } }, // private onMouseLeave: function(e) { if (this.bgRect.getRegion().contains(e.getPoint())) { this.fireEvent('mouseleave', e); } }, // @private - Normalize a delegated single event from the main container to each sprite and sprite group processEvent: function(name, e) { var target = e.getTarget(), surface = this.surface, sprite; this.fireEvent(name, e); // We wrap text types in a tspan, sprite is the parent. if (target.nodeName == "tspan") { target = target.parentNode; } sprite = this.items.get(target.id); if (sprite) { sprite.fireEvent(name, sprite, e); } }, /* @private - Wrap SVG text inside a tspan to allow for line wrapping. In addition this normallizes * the baseline for text the vertical middle of the text to be the same as VML. */ tuneText: function (sprite, attrs) { var el = sprite.el.dom, height, tspan, tspans = [], text, i, ln, texts, x = el.getAttribute("x"); if (attrs.hasOwnProperty("text")) { while (el.firstChild) { el.removeChild(el.firstChild); } // Wrap each row into tspan to emulate rows texts = String(attrs.text).split("\n"); for (i = 0, ln = texts.length; i < ln; i++) { text = texts[i]; if (text) { tspan = this.createSVGElement("tspan"); tspan.appendChild(Ext.getDoc().dom.createTextNode(text)); tspan.setAttribute("x", x); el.appendChild(tspan); tspans[i] = tspan; } } } // Normalize baseline via a DY shift of first tspan. Shift other rows by height * line height (1.2) if (tspans.length) { height = this.getBBoxText(sprite).height; for (i = 0, ln = tspans.length; i < ln; i++) { tspans[i].setAttribute("dy", i ? height * 1.2 : height / 4); } sprite.dirty = true; } }, renderAll: function() { this.items.each(this.renderItem, this); }, renderItem: function (sprite) { if (!this.el) { return; } if (!sprite.el) { this.createSprite(sprite); } if (sprite.zIndexDirty) { this.applyZIndex(sprite); } if (sprite.dirty) { this.applyAttrs(sprite); this.applyTransformations(sprite); } }, redraw: function(sprite) { sprite.dirty = sprite.zIndexDirty = true; this.renderItem(sprite); }, applyAttrs: function (sprite) { var me = this, el = sprite.el, group = sprite.group, sattr = sprite.attr, groups, i, ln, attrs, font, key, style, name; if (group) { groups = [].concat(group); ln = groups.length; for (i = 0; i < ln; i++) { group = groups[i]; me.getGroup(group).add(sprite); } delete sprite.group; } attrs = me.scrubAttrs(sprite) || {}; // if (sprite.dirtyPath) { sprite.bbox.plain = 0; sprite.bbox.transform = 0; if (sprite.type == "circle" || sprite.type == "ellipse") { attrs.cx = attrs.cx || attrs.x; attrs.cy = attrs.cy || attrs.y; } else if (sprite.type == "rect") { attrs.rx = attrs.ry = attrs.r; } else if (sprite.type == "path" && attrs.d) { attrs.d = Ext.draw.Draw.pathToAbsolute(attrs.d); } sprite.dirtyPath = false; // } if (sprite.type == 'text' && attrs.font && sprite.dirtyFont) { el.set({ style: "font: " + attrs.font}); sprite.dirtyFont = false; } if (sprite.type == "image") { el.dom.setAttributeNS(me.xlink, "href", attrs.src); } Ext.applyIf(attrs, me.minDefaults[sprite.type]); if (sprite.dirtyHidden) { (sattr.hidden) ? me.hidePrim(sprite) : me.showPrim(sprite); sprite.dirtyHidden = false; } for (key in attrs) { if (attrs.hasOwnProperty(key) && attrs[key] != null) { el.dom.setAttribute(key, String(attrs[key])); } } if (sprite.type == 'text') { me.tuneText(sprite, attrs); } //set styles style = sattr.style; if (style) { el.setStyle(style); } sprite.dirty = false; },
/** * Insert or move a given sprite's element to the correct place in the DOM list for its zIndex * @param {Ext.draw.Sprite} sprite */ applyZIndex: function(sprite) { var idx = this.positionSpriteInList(sprite), el = sprite.el, prevEl; if (this.el.dom.childNodes[idx + 2] !== el.dom) { //shift by 2 to account for defs and bg rect if (idx > 0) { // Find the first previous sprite which has its DOM element created already do { prevEl = this.items.getAt(--idx).el; } while (!prevEl && idx > 0); } el.insertAfter(prevEl || this.bgRect); } sprite.zIndexDirty = false; }, createItem: function (config) { var sprite = new Ext.draw.Sprite(config); sprite.surface = this; return sprite; }, addGradient: function(gradient) { gradient = Ext.draw.Draw.parseGradient(gradient); var ln = gradient.stops.length, vector = gradient.vector, gradientEl, stop, stopEl, i; if (gradient.type == "linear") { gradientEl = this.createSVGElement("linearGradient"); gradientEl.setAttribute("x1", vector[0]); gradientEl.setAttribute("y1", vector[1]); gradientEl.setAttribute("x2", vector[2]); gradientEl.setAttribute("y2", vector[3]); } else { gradientEl = this.createSVGElement("radialGradient"); gradientEl.setAttribute("cx", gradient.centerX); gradientEl.setAttribute("cy", gradient.centerY); gradientEl.setAttribute("r", gradient.radius); if (Ext.isNumber(gradient.focalX) && Ext.isNumber(gradient.focalY)) { gradientEl.setAttribute("fx", gradient.focalX); gradientEl.setAttribute("fy", gradient.focalY); } } gradientEl.id = gradient.id; this.getDefs().appendChild(gradientEl); for (i = 0; i < ln; i++) { stop = gradient.stops[i]; stopEl = this.createSVGElement("stop"); stopEl.setAttribute("offset", stop.offset + "%"); stopEl.setAttribute("stop-color", stop.color); stopEl.setAttribute("stop-opacity",stop.opacity); gradientEl.appendChild(stopEl); } },
/** * Checks if the specified CSS class exists on this element's DOM node. * @param {String} className The CSS class to check for * @return {Boolean} True if the class exists, else false */ hasCls: function(sprite, className) { return className && (' ' + (sprite.el.dom.getAttribute('class') || '') + ' ').indexOf(' ' + className + ' ') != -1; }, addCls: function(sprite, className) { var el = sprite.el, i, len, v, cls = [], curCls = el.getAttribute('class') || ''; // Separate case is for speed if (!Ext.isArray(className)) { if (typeof className == 'string' && !this.hasCls(sprite, className)) { el.set({ 'class': curCls + ' ' + className }); } } else { for (i = 0, len = className.length; i < len; i++) { v = className[i]; if (typeof v == 'string' && (' ' + curCls + ' ').indexOf(' ' + v + ' ') == -1) { cls.push(v); } } if (cls.length) { el.set({ 'class': ' ' + cls.join(' ') }); } } }, removeCls: function(sprite, className) { var me = this, el = sprite.el, curCls = el.getAttribute('class') || '', i, idx, len, cls, elClasses; if (!Ext.isArray(className)){ className = [className]; } if (curCls) { elClasses = curCls.replace(me.trimRe, ' ').split(me.spacesRe); for (i = 0, len = className.length; i < len; i++) { cls = className[i]; if (typeof cls == 'string') { cls = cls.replace(me.trimRe, ''); idx = Ext.Array.indexOf(elClasses, cls); if (idx != -1) { elClasses.splice(idx, 1); } } } el.set({ 'class': elClasses.join(' ') }); } }, destroy: function() { Ext.draw.engine.SVG.superclass.destroy.call(this); Ext.removeNode(this.el); /* this.un({ scope: this, mousedown: this.onMouseDown, click: this.onClick, dblclick: this.onDblClick, contextmenu: this.onContextMenu }); */ delete this.el; } });