/** 
 | 
 * SVG Painter 
 | 
 */ 
 | 
  
 | 
import {createElement, SVGNS, XLINKNS, XMLNS} from '../svg/core'; 
 | 
import { normalizeColor } from '../svg/helper'; 
 | 
import * as util from '../core/util'; 
 | 
import Path from '../graphic/Path'; 
 | 
import ZRImage from '../graphic/Image'; 
 | 
import TSpan from '../graphic/TSpan'; 
 | 
import arrayDiff from '../core/arrayDiff'; 
 | 
import GradientManager from './helper/GradientManager'; 
 | 
import PatternManager from './helper/PatternManager'; 
 | 
import ClippathManager, {hasClipPath} from './helper/ClippathManager'; 
 | 
import ShadowManager from './helper/ShadowManager'; 
 | 
import { 
 | 
    path as svgPath, 
 | 
    image as svgImage, 
 | 
    text as svgText, 
 | 
    SVGProxy 
 | 
} from './graphic'; 
 | 
import Displayable from '../graphic/Displayable'; 
 | 
import Storage from '../Storage'; 
 | 
import { PainterBase } from '../PainterBase'; 
 | 
import { getSize } from '../canvas/helper'; 
 | 
  
 | 
function getSvgProxy(el: Displayable) { 
 | 
    if (el instanceof Path) { 
 | 
        return svgPath; 
 | 
    } 
 | 
    else if (el instanceof ZRImage) { 
 | 
        return svgImage; 
 | 
    } 
 | 
    else if (el instanceof TSpan) { 
 | 
        return svgText; 
 | 
    } 
 | 
    else { 
 | 
        return svgPath; 
 | 
    } 
 | 
} 
 | 
  
 | 
function checkParentAvailable(parent: SVGElement, child: SVGElement) { 
 | 
    return child && parent && child.parentNode !== parent; 
 | 
} 
 | 
  
 | 
function insertAfter(parent: SVGElement, child: SVGElement, prevSibling: SVGElement) { 
 | 
    if (checkParentAvailable(parent, child) && prevSibling) { 
 | 
        const nextSibling = prevSibling.nextSibling; 
 | 
        nextSibling ? parent.insertBefore(child, nextSibling) 
 | 
            : parent.appendChild(child); 
 | 
    } 
 | 
} 
 | 
  
 | 
function prepend(parent: SVGElement, child: SVGElement) { 
 | 
    if (checkParentAvailable(parent, child)) { 
 | 
        const firstChild = parent.firstChild; 
 | 
        firstChild ? parent.insertBefore(child, firstChild) 
 | 
            : parent.appendChild(child); 
 | 
    } 
 | 
} 
 | 
  
 | 
function remove(parent: SVGElement, child: SVGElement) { 
 | 
    if (child && parent && child.parentNode === parent) { 
 | 
        parent.removeChild(child); 
 | 
    } 
 | 
} 
 | 
function removeFromMyParent(child: SVGElement) { 
 | 
    if (child && child.parentNode) { 
 | 
        child.parentNode.removeChild(child); 
 | 
    } 
 | 
} 
 | 
  
 | 
function getSvgElement(displayable: Displayable) { 
 | 
    return displayable.__svgEl; 
 | 
} 
 | 
  
 | 
interface SVGPainterOption { 
 | 
    width?: number | string 
 | 
    height?: number | string 
 | 
} 
 | 
  
 | 
class SVGPainter implements PainterBase { 
 | 
  
 | 
    type = 'svg' 
 | 
  
 | 
    root: HTMLElement 
 | 
  
 | 
    storage: Storage 
 | 
  
 | 
    private _opts: SVGPainterOption 
 | 
  
 | 
    private _svgDom: SVGElement 
 | 
    private _svgRoot: SVGGElement 
 | 
    private _backgroundRoot: SVGGElement 
 | 
    private _backgroundNode: SVGRectElement 
 | 
  
 | 
    private _gradientManager: GradientManager 
 | 
    private _patternManager: PatternManager 
 | 
    private _clipPathManager: ClippathManager 
 | 
    private _shadowManager: ShadowManager 
 | 
  
 | 
    private _viewport: HTMLDivElement 
 | 
    private _visibleList: Displayable[] 
 | 
  
 | 
    private _width: number 
 | 
    private _height: number 
 | 
  
 | 
    constructor(root: HTMLElement, storage: Storage, opts: SVGPainterOption, zrId: number) { 
 | 
        this.root = root; 
 | 
        this.storage = storage; 
 | 
        this._opts = opts = util.extend({}, opts || {}); 
 | 
  
 | 
        const svgDom = createElement('svg'); 
 | 
        svgDom.setAttributeNS(XMLNS, 'xmlns', SVGNS); 
 | 
        svgDom.setAttributeNS(XMLNS, 'xmlns:xlink', XLINKNS); 
 | 
  
 | 
        svgDom.setAttribute('version', '1.1'); 
 | 
        svgDom.setAttribute('baseProfile', 'full'); 
 | 
        svgDom.style.cssText = 'user-select:none;position:absolute;left:0;top:0;'; 
 | 
  
 | 
        const bgRoot = createElement('g') as SVGGElement; 
 | 
        svgDom.appendChild(bgRoot); 
 | 
        const svgRoot = createElement('g') as SVGGElement; 
 | 
        svgDom.appendChild(svgRoot); 
 | 
  
 | 
        this._gradientManager = new GradientManager(zrId, svgRoot); 
 | 
        this._patternManager = new PatternManager(zrId, svgRoot); 
 | 
        this._clipPathManager = new ClippathManager(zrId, svgRoot); 
 | 
        this._shadowManager = new ShadowManager(zrId, svgRoot); 
 | 
  
 | 
        const viewport = document.createElement('div'); 
 | 
        viewport.style.cssText = 'overflow:hidden;position:relative'; 
 | 
  
 | 
        this._svgDom = svgDom; 
 | 
        this._svgRoot = svgRoot; 
 | 
        this._backgroundRoot = bgRoot; 
 | 
        this._viewport = viewport; 
 | 
  
 | 
        root.appendChild(viewport); 
 | 
        viewport.appendChild(svgDom); 
 | 
  
 | 
        this.resize(opts.width, opts.height); 
 | 
  
 | 
        this._visibleList = []; 
 | 
    } 
 | 
  
 | 
    getType() { 
 | 
        return 'svg'; 
 | 
    } 
 | 
  
 | 
    getViewportRoot() { 
 | 
        return this._viewport; 
 | 
    } 
 | 
  
 | 
    getSvgDom() { 
 | 
        return this._svgDom; 
 | 
    } 
 | 
  
 | 
    getSvgRoot() { 
 | 
        return this._svgRoot; 
 | 
    } 
 | 
  
 | 
    getViewportRootOffset() { 
 | 
        const viewportRoot = this.getViewportRoot(); 
 | 
        if (viewportRoot) { 
 | 
            return { 
 | 
                offsetLeft: viewportRoot.offsetLeft || 0, 
 | 
                offsetTop: viewportRoot.offsetTop || 0 
 | 
            }; 
 | 
        } 
 | 
    } 
 | 
  
 | 
    refresh() { 
 | 
        const list = this.storage.getDisplayList(true); 
 | 
        this._paintList(list); 
 | 
    } 
 | 
  
 | 
    setBackgroundColor(backgroundColor: string) { 
 | 
        // TODO gradient 
 | 
        // Insert a bg rect instead of setting background to viewport. 
 | 
        // Otherwise, the exported SVG don't have background. 
 | 
        if (this._backgroundRoot && this._backgroundNode) { 
 | 
            this._backgroundRoot.removeChild(this._backgroundNode); 
 | 
        } 
 | 
  
 | 
        const bgNode = createElement('rect') as SVGRectElement; 
 | 
        bgNode.setAttribute('width', this.getWidth() as any); 
 | 
        bgNode.setAttribute('height', this.getHeight() as any); 
 | 
        bgNode.setAttribute('x', 0 as any); 
 | 
        bgNode.setAttribute('y', 0 as any); 
 | 
        bgNode.setAttribute('id', 0 as any); 
 | 
        const { color, opacity } = normalizeColor(backgroundColor); 
 | 
        bgNode.setAttribute('fill', color); 
 | 
        bgNode.setAttribute('fill-opacity', opacity as any); 
 | 
  
 | 
        this._backgroundRoot.appendChild(bgNode); 
 | 
        this._backgroundNode = bgNode; 
 | 
    } 
 | 
  
 | 
    createSVGElement(tag: string): SVGElement { 
 | 
        return createElement(tag); 
 | 
    } 
 | 
  
 | 
    paintOne(el: Displayable): SVGElement { 
 | 
        const svgProxy = getSvgProxy(el); 
 | 
        svgProxy && (svgProxy as SVGProxy<Displayable>).brush(el); 
 | 
        return getSvgElement(el); 
 | 
    } 
 | 
  
 | 
    _paintList(list: Displayable[]) { 
 | 
        const gradientManager = this._gradientManager; 
 | 
        const patternManager = this._patternManager; 
 | 
        const clipPathManager = this._clipPathManager; 
 | 
        const shadowManager = this._shadowManager; 
 | 
  
 | 
        gradientManager.markAllUnused(); 
 | 
        patternManager.markAllUnused(); 
 | 
        clipPathManager.markAllUnused(); 
 | 
        shadowManager.markAllUnused(); 
 | 
  
 | 
        const svgRoot = this._svgRoot; 
 | 
        const visibleList = this._visibleList; 
 | 
        const listLen = list.length; 
 | 
  
 | 
        const newVisibleList = []; 
 | 
  
 | 
        for (let i = 0; i < listLen; i++) { 
 | 
            const displayable = list[i]; 
 | 
            const svgProxy = getSvgProxy(displayable); 
 | 
            let svgElement = getSvgElement(displayable); 
 | 
            if (!displayable.invisible) { 
 | 
                if (displayable.__dirty || !svgElement) { 
 | 
                    svgProxy && (svgProxy as SVGProxy<Displayable>).brush(displayable); 
 | 
                    svgElement = getSvgElement(displayable); 
 | 
                    // Update gradient and shadow 
 | 
                    if (svgElement && displayable.style) { 
 | 
                        gradientManager.update(displayable.style.fill); 
 | 
                        gradientManager.update(displayable.style.stroke); 
 | 
                        patternManager.update(displayable.style.fill); 
 | 
                        patternManager.update(displayable.style.stroke); 
 | 
                        shadowManager.update(svgElement, displayable); 
 | 
                    } 
 | 
  
 | 
                    displayable.__dirty = 0; 
 | 
                } 
 | 
  
 | 
                // May have optimizations and ignore brush(like empty string in TSpan) 
 | 
                if (svgElement) { 
 | 
                    newVisibleList.push(displayable); 
 | 
                } 
 | 
            } 
 | 
        } 
 | 
  
 | 
        const diff = arrayDiff(visibleList, newVisibleList); 
 | 
        let prevSvgElement; 
 | 
        let topPrevSvgElement; 
 | 
  
 | 
        // NOTE: First do remove, in case element moved to the head and do remove 
 | 
        // after add 
 | 
        for (let i = 0; i < diff.length; i++) { 
 | 
            const item = diff[i]; 
 | 
            if (item.removed) { 
 | 
                for (let k = 0; k < item.count; k++) { 
 | 
                    const displayable = visibleList[item.indices[k]]; 
 | 
                    const svgElement = getSvgElement(displayable); 
 | 
                    hasClipPath(displayable) ? removeFromMyParent(svgElement) 
 | 
                        : remove(svgRoot, svgElement); 
 | 
                } 
 | 
            } 
 | 
        } 
 | 
  
 | 
        let prevDisplayable; 
 | 
        let currentClipGroup; 
 | 
        for (let i = 0; i < diff.length; i++) { 
 | 
            const item = diff[i]; 
 | 
            // const isAdd = item.added; 
 | 
            if (item.removed) { 
 | 
                continue; 
 | 
            } 
 | 
            for (let k = 0; k < item.count; k++) { 
 | 
                const displayable = newVisibleList[item.indices[k]]; 
 | 
                // Update clipPath 
 | 
                const clipGroup = clipPathManager.update(displayable, prevDisplayable); 
 | 
                if (clipGroup !== currentClipGroup) { 
 | 
                    // First pop to top level. 
 | 
                    prevSvgElement = topPrevSvgElement; 
 | 
                    if (clipGroup) { 
 | 
                        // Enter second level of clipping group. 
 | 
                        prevSvgElement ? insertAfter(svgRoot, clipGroup, prevSvgElement) 
 | 
                            : prepend(svgRoot, clipGroup); 
 | 
                        topPrevSvgElement = clipGroup; 
 | 
                        // Reset prevSvgElement in second level. 
 | 
                        prevSvgElement = null; 
 | 
                    } 
 | 
                    currentClipGroup = clipGroup; 
 | 
                } 
 | 
  
 | 
                const svgElement = getSvgElement(displayable); 
 | 
                // if (isAdd) { 
 | 
                prevSvgElement 
 | 
                    ? insertAfter(currentClipGroup || svgRoot, svgElement, prevSvgElement) 
 | 
                    : prepend(currentClipGroup || svgRoot, svgElement); 
 | 
                // } 
 | 
  
 | 
                prevSvgElement = svgElement || prevSvgElement; 
 | 
                if (!currentClipGroup) { 
 | 
                    topPrevSvgElement = prevSvgElement; 
 | 
                } 
 | 
  
 | 
                gradientManager.markUsed(displayable); 
 | 
                gradientManager.addWithoutUpdate(svgElement, displayable); 
 | 
  
 | 
                patternManager.markUsed(displayable); 
 | 
                patternManager.addWithoutUpdate(svgElement, displayable); 
 | 
  
 | 
                clipPathManager.markUsed(displayable); 
 | 
  
 | 
                prevDisplayable = displayable; 
 | 
            } 
 | 
        } 
 | 
  
 | 
        gradientManager.removeUnused(); 
 | 
        patternManager.removeUnused(); 
 | 
        clipPathManager.removeUnused(); 
 | 
        shadowManager.removeUnused(); 
 | 
  
 | 
        this._visibleList = newVisibleList; 
 | 
    } 
 | 
  
 | 
    resize(width: number | string, height: number | string) { 
 | 
        const viewport = this._viewport; 
 | 
        // FIXME Why ? 
 | 
        viewport.style.display = 'none'; 
 | 
  
 | 
        // Save input w/h 
 | 
        const opts = this._opts; 
 | 
        width != null && (opts.width = width); 
 | 
        height != null && (opts.height = height); 
 | 
  
 | 
        width = getSize(this.root, 0, opts); 
 | 
        height = getSize(this.root, 1, opts); 
 | 
  
 | 
        viewport.style.display = ''; 
 | 
  
 | 
        if (this._width !== width || this._height !== height) { 
 | 
            this._width = width; 
 | 
            this._height = height; 
 | 
  
 | 
            const viewportStyle = viewport.style; 
 | 
            viewportStyle.width = width + 'px'; 
 | 
            viewportStyle.height = height + 'px'; 
 | 
  
 | 
            const svgRoot = this._svgDom; 
 | 
            // Set width by 'svgRoot.width = width' is invalid 
 | 
            svgRoot.setAttribute('width', width + ''); 
 | 
            svgRoot.setAttribute('height', height + ''); 
 | 
        } 
 | 
  
 | 
        if (this._backgroundNode) { 
 | 
            this._backgroundNode.setAttribute('width', width as any); 
 | 
            this._backgroundNode.setAttribute('height', height as any); 
 | 
        } 
 | 
    } 
 | 
  
 | 
    /** 
 | 
     * 获取绘图区域宽度 
 | 
     */ 
 | 
    getWidth() { 
 | 
        return this._width; 
 | 
    } 
 | 
  
 | 
    /** 
 | 
     * 获取绘图区域高度 
 | 
     */ 
 | 
    getHeight() { 
 | 
        return this._height; 
 | 
    } 
 | 
  
 | 
    dispose() { 
 | 
        this.root.innerHTML = ''; 
 | 
  
 | 
        this._svgRoot = 
 | 
            this._backgroundRoot = 
 | 
            this._svgDom = 
 | 
            this._backgroundNode = 
 | 
            this._viewport = this.storage = null; 
 | 
    } 
 | 
  
 | 
    clear() { 
 | 
        const viewportNode = this._viewport; 
 | 
        if (viewportNode && viewportNode.parentNode) { 
 | 
            viewportNode.parentNode.removeChild(viewportNode); 
 | 
        } 
 | 
    } 
 | 
  
 | 
    toDataURL() { 
 | 
        this.refresh(); 
 | 
        const svgDom = this._svgDom; 
 | 
        const outerHTML = svgDom.outerHTML 
 | 
            // outerHTML of `svg` tag is not supported in IE, use `parentNode.innerHTML` instead 
 | 
            // PENDING: Or use `new XMLSerializer().serializeToString(svg)`? 
 | 
            || (svgDom.parentNode && (svgDom.parentNode as HTMLElement).innerHTML); 
 | 
        const html = encodeURIComponent(outerHTML.replace(/></g, '>\n\r<')); 
 | 
        return 'data:image/svg+xml;charset=UTF-8,' + html; 
 | 
    } 
 | 
    refreshHover = createMethodNotSupport('refreshHover') as PainterBase['refreshHover']; 
 | 
    configLayer = createMethodNotSupport('configLayer') as PainterBase['configLayer']; 
 | 
} 
 | 
  
 | 
  
 | 
// Not supported methods 
 | 
function createMethodNotSupport(method: string): any { 
 | 
    return function () { 
 | 
        if (process.env.NODE_ENV !== 'production') { 
 | 
            util.logError('In SVG mode painter not support method "' + method + '"'); 
 | 
        } 
 | 
    }; 
 | 
} 
 | 
  
 | 
  
 | 
export default SVGPainter; 
 |