| /** | 
|  * 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; |