|  | /* | 
|  | * Licensed to the Apache Software Foundation (ASF) under one | 
|  | * or more contributor license agreements.  See the NOTICE file | 
|  | * distributed with this work for additional information | 
|  | * regarding copyright ownership.  The ASF licenses this file | 
|  | * to you under the Apache License, Version 2.0 (the | 
|  | * "License"); you may not use this file except in compliance | 
|  | * with the License.  You may obtain a copy of the License at | 
|  | * | 
|  | *   http://www.apache.org/licenses/LICENSE-2.0 | 
|  | * | 
|  | * Unless required by applicable law or agreed to in writing, | 
|  | * software distributed under the License is distributed on an | 
|  | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | 
|  | * KIND, either express or implied.  See the License for the | 
|  | * specific language governing permissions and limitations | 
|  | * under the License. | 
|  | */ | 
|  |  | 
|  | import * as zrUtil from 'zrender/src/core/util'; | 
|  | import { TextStyleProps } from 'zrender/src/graphic/Text'; | 
|  | import Displayable from 'zrender/src/graphic/Displayable'; | 
|  | import Element from 'zrender/src/Element'; | 
|  | import * as modelUtil from '../../util/model'; | 
|  | import * as graphicUtil from '../../util/graphic'; | 
|  | import * as layoutUtil from '../../util/layout'; | 
|  | import { parsePercent } from '../../util/number'; | 
|  | import GlobalModel from '../../model/Global'; | 
|  | import ComponentView from '../../view/Component'; | 
|  | import ExtensionAPI from '../../core/ExtensionAPI'; | 
|  | import { getECData } from '../../util/innerStore'; | 
|  | import { isEC4CompatibleStyle, convertFromEC4CompatibleStyle } from '../../util/styleCompat'; | 
|  | import { | 
|  | ElementMap, | 
|  | GraphicComponentModel, | 
|  | GraphicComponentDisplayableOption, | 
|  | GraphicComponentZRPathOption, | 
|  | GraphicComponentGroupOption, | 
|  | GraphicComponentElementOption | 
|  | } from './GraphicModel'; | 
|  | import { | 
|  | applyLeaveTransition, | 
|  | applyUpdateTransition, | 
|  | isTransitionAll, | 
|  | updateLeaveTo | 
|  | } from '../../animation/customGraphicTransition'; | 
|  | import { updateProps } from '../../animation/basicTransition'; | 
|  | import { | 
|  | applyKeyframeAnimation, | 
|  | stopPreviousKeyframeAnimationAndRestore | 
|  | } from '../../animation/customGraphicKeyframeAnimation'; | 
|  |  | 
|  | const nonShapeGraphicElements = { | 
|  | // Reserved but not supported in graphic component. | 
|  | path: null as unknown, | 
|  | compoundPath: null as unknown, | 
|  |  | 
|  | // Supported in graphic component. | 
|  | group: graphicUtil.Group, | 
|  | image: graphicUtil.Image, | 
|  | text: graphicUtil.Text | 
|  | } as const; | 
|  | type NonShapeGraphicElementType = keyof typeof nonShapeGraphicElements; | 
|  |  | 
|  | export const inner = modelUtil.makeInner<{ | 
|  | width: number; | 
|  | height: number; | 
|  | isNew: boolean; | 
|  | id: string; | 
|  | type: string; | 
|  | option: GraphicComponentElementOption | 
|  | }, Element>(); | 
|  | // ------------------------ | 
|  | // View | 
|  | // ------------------------ | 
|  | export class GraphicComponentView extends ComponentView { | 
|  |  | 
|  | static type = 'graphic'; | 
|  | type = GraphicComponentView.type; | 
|  |  | 
|  | private _elMap: ElementMap; | 
|  | private _lastGraphicModel: GraphicComponentModel; | 
|  |  | 
|  | init() { | 
|  | this._elMap = zrUtil.createHashMap(); | 
|  | } | 
|  |  | 
|  | render(graphicModel: GraphicComponentModel, ecModel: GlobalModel, api: ExtensionAPI): void { | 
|  | // Having leveraged between use cases and algorithm complexity, a very | 
|  | // simple layout mechanism is used: | 
|  | // The size(width/height) can be determined by itself or its parent (not | 
|  | // implemented yet), but can not by its children. (Top-down travel) | 
|  | // The location(x/y) can be determined by the bounding rect of itself | 
|  | // (can including its descendants or not) and the size of its parent. | 
|  | // (Bottom-up travel) | 
|  |  | 
|  | // When `chart.clear()` or `chart.setOption({...}, true)` with the same id, | 
|  | // view will be reused. | 
|  | if (graphicModel !== this._lastGraphicModel) { | 
|  | this._clear(); | 
|  | } | 
|  | this._lastGraphicModel = graphicModel; | 
|  |  | 
|  | this._updateElements(graphicModel); | 
|  | this._relocate(graphicModel, api); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Update graphic elements. | 
|  | */ | 
|  | private _updateElements(graphicModel: GraphicComponentModel): void { | 
|  | const elOptionsToUpdate = graphicModel.useElOptionsToUpdate(); | 
|  |  | 
|  | if (!elOptionsToUpdate) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | const elMap = this._elMap; | 
|  | const rootGroup = this.group; | 
|  |  | 
|  | const globalZ = graphicModel.get('z'); | 
|  | const globalZLevel = graphicModel.get('zlevel'); | 
|  |  | 
|  | // Top-down tranverse to assign graphic settings to each elements. | 
|  | zrUtil.each(elOptionsToUpdate, function (elOption) { | 
|  | const id = modelUtil.convertOptionIdName(elOption.id, null); | 
|  | const elExisting = id != null ? elMap.get(id) : null; | 
|  | const parentId = modelUtil.convertOptionIdName(elOption.parentId, null); | 
|  | const targetElParent = (parentId != null ? elMap.get(parentId) : rootGroup) as graphicUtil.Group; | 
|  |  | 
|  | const elType = elOption.type; | 
|  | const elOptionStyle = (elOption as GraphicComponentDisplayableOption).style; | 
|  | if (elType === 'text' && elOptionStyle) { | 
|  | // In top/bottom mode, textVerticalAlign should not be used, which cause | 
|  | // inaccurately locating. | 
|  | if (elOption.hv && elOption.hv[1]) { | 
|  | (elOptionStyle as any).textVerticalAlign = | 
|  | (elOptionStyle as any).textBaseline = | 
|  | (elOptionStyle as TextStyleProps).verticalAlign = | 
|  | (elOptionStyle as TextStyleProps).align = null; | 
|  | } | 
|  | } | 
|  |  | 
|  | let textContentOption = (elOption as GraphicComponentZRPathOption).textContent; | 
|  | let textConfig = (elOption as GraphicComponentZRPathOption).textConfig; | 
|  | if (elOptionStyle | 
|  | && isEC4CompatibleStyle(elOptionStyle, elType, !!textConfig, !!textContentOption)) { | 
|  | const convertResult = | 
|  | convertFromEC4CompatibleStyle(elOptionStyle, elType, true) as GraphicComponentZRPathOption; | 
|  | if (!textConfig && convertResult.textConfig) { | 
|  | textConfig = (elOption as GraphicComponentZRPathOption).textConfig = convertResult.textConfig; | 
|  | } | 
|  | if (!textContentOption && convertResult.textContent) { | 
|  | textContentOption = convertResult.textContent; | 
|  | } | 
|  | } | 
|  |  | 
|  | // Remove unnecessary props to avoid potential problems. | 
|  | const elOptionCleaned = getCleanedElOption(elOption); | 
|  |  | 
|  |  | 
|  | // For simple, do not support parent change, otherwise reorder is needed. | 
|  | if (__DEV__) { | 
|  | elExisting && zrUtil.assert( | 
|  | targetElParent === elExisting.parent, | 
|  | 'Changing parent is not supported.' | 
|  | ); | 
|  | } | 
|  |  | 
|  | const $action = elOption.$action || 'merge'; | 
|  | const isMerge = $action === 'merge'; | 
|  | const isReplace = $action === 'replace'; | 
|  | if (isMerge) { | 
|  | const isInit = !elExisting; | 
|  | let el = elExisting; | 
|  | if (isInit) { | 
|  | el = createEl(id, targetElParent, elOption.type, elMap); | 
|  | } | 
|  | else { | 
|  | el && (inner(el).isNew = false); | 
|  | // Stop and restore before update any other attributes. | 
|  | stopPreviousKeyframeAnimationAndRestore(el); | 
|  | } | 
|  | if (el) { | 
|  | applyUpdateTransition( | 
|  | el, | 
|  | elOptionCleaned, | 
|  | graphicModel, | 
|  | { isInit } | 
|  | ); | 
|  | updateCommonAttrs(el, elOption, globalZ, globalZLevel); | 
|  | } | 
|  | } | 
|  | else if (isReplace) { | 
|  | removeEl(elExisting, elOption, elMap, graphicModel); | 
|  | const el = createEl(id, targetElParent, elOption.type, elMap); | 
|  | if (el) { | 
|  | applyUpdateTransition( | 
|  | el, | 
|  | elOptionCleaned, | 
|  | graphicModel, | 
|  | { isInit: true} | 
|  | ); | 
|  | updateCommonAttrs(el, elOption, globalZ, globalZLevel); | 
|  | } | 
|  | } | 
|  | else if ($action === 'remove') { | 
|  | updateLeaveTo(elExisting, elOption); | 
|  | removeEl(elExisting, elOption, elMap, graphicModel); | 
|  | } | 
|  |  | 
|  | const el = elMap.get(id); | 
|  |  | 
|  | if (el && textContentOption) { | 
|  | if (isMerge) { | 
|  | const textContentExisting = el.getTextContent(); | 
|  | textContentExisting | 
|  | ? textContentExisting.attr(textContentOption) | 
|  | : el.setTextContent(new graphicUtil.Text(textContentOption)); | 
|  | } | 
|  | else if (isReplace) { | 
|  | el.setTextContent(new graphicUtil.Text(textContentOption)); | 
|  | } | 
|  | } | 
|  |  | 
|  | if (el) { | 
|  | const clipPathOption = elOption.clipPath; | 
|  | if (clipPathOption) { | 
|  | const clipPathType = clipPathOption.type; | 
|  | let clipPath: graphicUtil.Path; | 
|  | let isInit = false; | 
|  | if (isMerge) { | 
|  | const oldClipPath = el.getClipPath(); | 
|  | isInit = !oldClipPath | 
|  | || inner(oldClipPath).type !== clipPathType; | 
|  | clipPath = isInit ? newEl(clipPathType) as graphicUtil.Path : oldClipPath; | 
|  | } | 
|  | else if (isReplace) { | 
|  | isInit = true; | 
|  | clipPath = newEl(clipPathType) as graphicUtil.Path; | 
|  | } | 
|  |  | 
|  | el.setClipPath(clipPath); | 
|  |  | 
|  | applyUpdateTransition( | 
|  | clipPath, | 
|  | clipPathOption, | 
|  | graphicModel, | 
|  | { isInit} | 
|  | ); | 
|  | applyKeyframeAnimation( | 
|  | clipPath, | 
|  | clipPathOption.keyframeAnimation, | 
|  | graphicModel | 
|  | ); | 
|  | } | 
|  |  | 
|  | const elInner = inner(el); | 
|  |  | 
|  | el.setTextConfig(textConfig); | 
|  |  | 
|  | elInner.option = elOption; | 
|  | setEventData(el, graphicModel, elOption); | 
|  |  | 
|  | graphicUtil.setTooltipConfig({ | 
|  | el: el, | 
|  | componentModel: graphicModel, | 
|  | itemName: el.name, | 
|  | itemTooltipOption: elOption.tooltip | 
|  | }); | 
|  |  | 
|  | applyKeyframeAnimation(el, elOption.keyframeAnimation, graphicModel); | 
|  | } | 
|  | }); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Locate graphic elements. | 
|  | */ | 
|  | private _relocate(graphicModel: GraphicComponentModel, api: ExtensionAPI): void { | 
|  | const elOptions = graphicModel.option.elements; | 
|  | const rootGroup = this.group; | 
|  | const elMap = this._elMap; | 
|  | const apiWidth = api.getWidth(); | 
|  | const apiHeight = api.getHeight(); | 
|  |  | 
|  | const xy = ['x', 'y'] as const; | 
|  |  | 
|  | // Top-down to calculate percentage width/height of group | 
|  | for (let i = 0; i < elOptions.length; i++) { | 
|  | const elOption = elOptions[i]; | 
|  | const id = modelUtil.convertOptionIdName(elOption.id, null); | 
|  | const el = id != null ? elMap.get(id) : null; | 
|  |  | 
|  | if (!el || !el.isGroup) { | 
|  | continue; | 
|  | } | 
|  | const parentEl = el.parent; | 
|  | const isParentRoot = parentEl === rootGroup; | 
|  | // Like 'position:absolut' in css, default 0. | 
|  | const elInner = inner(el); | 
|  | const parentElInner = inner(parentEl); | 
|  | elInner.width = parsePercent( | 
|  | (elInner.option as GraphicComponentGroupOption).width, | 
|  | isParentRoot ? apiWidth : parentElInner.width | 
|  | ) || 0; | 
|  | elInner.height = parsePercent( | 
|  | (elInner.option as GraphicComponentGroupOption).height, | 
|  | isParentRoot ? apiHeight : parentElInner.height | 
|  | ) || 0; | 
|  | } | 
|  |  | 
|  | // Bottom-up tranvese all elements (consider ec resize) to locate elements. | 
|  | for (let i = elOptions.length - 1; i >= 0; i--) { | 
|  | const elOption = elOptions[i]; | 
|  | const id = modelUtil.convertOptionIdName(elOption.id, null); | 
|  | const el = id != null ? elMap.get(id) : null; | 
|  |  | 
|  | if (!el) { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | const parentEl = el.parent; | 
|  | const parentElInner = inner(parentEl); | 
|  | const containerInfo = parentEl === rootGroup | 
|  | ? { | 
|  | width: apiWidth, | 
|  | height: apiHeight | 
|  | } | 
|  | : { | 
|  | width: parentElInner.width, | 
|  | height: parentElInner.height | 
|  | }; | 
|  |  | 
|  | // PENDING | 
|  | // Currently, when `bounding: 'all'`, the union bounding rect of the group | 
|  | // does not include the rect of [0, 0, group.width, group.height], which | 
|  | // is probably weird for users. Should we make a break change for it? | 
|  | const layoutPos = {} as Record<'x' | 'y', number>; | 
|  | const layouted = layoutUtil.positionElement( | 
|  | el, elOption, containerInfo, null, | 
|  | { hv: elOption.hv, boundingMode: elOption.bounding }, | 
|  | layoutPos | 
|  | ); | 
|  |  | 
|  | if (!inner(el).isNew && layouted) { | 
|  | const transition = elOption.transition; | 
|  | const animatePos = {} as Record<'x' | 'y', number>; | 
|  | for (let k = 0; k < xy.length; k++) { | 
|  | const key = xy[k]; | 
|  | const val = layoutPos[key]; | 
|  | if (transition && (isTransitionAll(transition) || zrUtil.indexOf(transition, key) >= 0)) { | 
|  | animatePos[key] = val; | 
|  | } | 
|  | else { | 
|  | el[key] = val; | 
|  | } | 
|  | } | 
|  | updateProps(el, animatePos, graphicModel, 0); | 
|  | } | 
|  | else { | 
|  | el.attr(layoutPos); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Clear all elements. | 
|  | */ | 
|  | private _clear(): void { | 
|  | const elMap = this._elMap; | 
|  | elMap.each((el) => { | 
|  | removeEl(el, inner(el).option, elMap, this._lastGraphicModel); | 
|  | }); | 
|  | this._elMap = zrUtil.createHashMap(); | 
|  | } | 
|  |  | 
|  | dispose(): void { | 
|  | this._clear(); | 
|  | } | 
|  | } | 
|  |  | 
|  | function newEl(graphicType: string) { | 
|  | if (__DEV__) { | 
|  | zrUtil.assert(graphicType, 'graphic type MUST be set'); | 
|  | } | 
|  |  | 
|  | const Clz = ( | 
|  | zrUtil.hasOwn(nonShapeGraphicElements, graphicType) | 
|  | // Those graphic elements are not shapes. They should not be | 
|  | // overwritten by users, so do them first. | 
|  | ? nonShapeGraphicElements[graphicType as NonShapeGraphicElementType] | 
|  | : graphicUtil.getShapeClass(graphicType) | 
|  | ) as { new(opt: GraphicComponentElementOption): Element; }; | 
|  |  | 
|  | if (__DEV__) { | 
|  | zrUtil.assert(Clz, `graphic type ${graphicType} can not be found`); | 
|  | } | 
|  |  | 
|  | const el = new Clz({}); | 
|  | inner(el).type = graphicType; | 
|  | return el; | 
|  | } | 
|  | function createEl( | 
|  | id: string, | 
|  | targetElParent: graphicUtil.Group, | 
|  | graphicType: string, | 
|  | elMap: ElementMap | 
|  | ): Element { | 
|  |  | 
|  | const el = newEl(graphicType); | 
|  |  | 
|  | targetElParent.add(el); | 
|  | elMap.set(id, el); | 
|  | inner(el).id = id; | 
|  | inner(el).isNew = true; | 
|  |  | 
|  | return el; | 
|  | } | 
|  | function removeEl( | 
|  | elExisting: Element, | 
|  | elOption: GraphicComponentElementOption, | 
|  | elMap: ElementMap, | 
|  | graphicModel: GraphicComponentModel | 
|  | ): void { | 
|  | const existElParent = elExisting && elExisting.parent; | 
|  | if (existElParent) { | 
|  | elExisting.type === 'group' && elExisting.traverse(function (el) { | 
|  | removeEl(el, elOption, elMap, graphicModel); | 
|  | }); | 
|  | applyLeaveTransition(elExisting, elOption, graphicModel); | 
|  | elMap.removeKey(inner(elExisting).id); | 
|  | } | 
|  | } | 
|  |  | 
|  | function updateCommonAttrs( | 
|  | el: Element, | 
|  | elOption: GraphicComponentElementOption, | 
|  | defaultZ: number, | 
|  | defaultZlevel: number | 
|  | ) { | 
|  | if (!el.isGroup) { | 
|  | zrUtil.each([ | 
|  | ['cursor', Displayable.prototype.cursor], | 
|  | // We should not support configure z and zlevel in the element level. | 
|  | // But seems we didn't limit it previously. So here still use it to avoid breaking. | 
|  | ['zlevel', defaultZlevel || 0], | 
|  | ['z', defaultZ || 0], | 
|  | // z2 must not be null/undefined, otherwise sort error may occur. | 
|  | ['z2', 0] | 
|  | ], item => { | 
|  | const prop = item[0] as any; | 
|  | if (zrUtil.hasOwn(elOption, prop)) { | 
|  | (el as any)[prop] = zrUtil.retrieve2( | 
|  | (elOption as any)[prop], | 
|  | item[1] | 
|  | ); | 
|  | } | 
|  | else if ((el as any)[prop] == null) { | 
|  | (el as any)[prop] = item[1]; | 
|  | } | 
|  | }); | 
|  | } | 
|  |  | 
|  | zrUtil.each(zrUtil.keys(elOption), key => { | 
|  | // Assign event handlers. | 
|  | // PENDING: should enumerate all event names or use pattern matching? | 
|  | if (key.indexOf('on') === 0) { | 
|  | const val = (elOption as any)[key]; | 
|  | (el as any)[key] = zrUtil.isFunction(val) ? val : null; | 
|  | } | 
|  | }); | 
|  | if (zrUtil.hasOwn(elOption, 'draggable')) { | 
|  | el.draggable = elOption.draggable; | 
|  | } | 
|  |  | 
|  | // Other attributes | 
|  | elOption.name != null && (el.name = elOption.name); | 
|  | elOption.id != null && ((el as any).id = elOption.id); | 
|  | } | 
|  | // Remove unnecessary props to avoid potential problems. | 
|  | function getCleanedElOption( | 
|  | elOption: GraphicComponentElementOption | 
|  | ): Omit<GraphicComponentElementOption, 'textContent'> { | 
|  | elOption = zrUtil.extend({}, elOption); | 
|  | zrUtil.each( | 
|  | ['id', 'parentId', '$action', 'hv', 'bounding', 'textContent', 'clipPath'].concat(layoutUtil.LOCATION_PARAMS), | 
|  | function (name) { | 
|  | delete (elOption as any)[name]; | 
|  | } | 
|  | ); | 
|  | return elOption; | 
|  | } | 
|  |  | 
|  | function setEventData( | 
|  | el: Element, | 
|  | graphicModel: GraphicComponentModel, | 
|  | elOption: GraphicComponentElementOption | 
|  | ): void { | 
|  | let eventData = getECData(el).eventData; | 
|  | // Simple optimize for large amount of elements that no need event. | 
|  | if (!el.silent && !el.ignore && !eventData) { | 
|  | eventData = getECData(el).eventData = { | 
|  | componentType: 'graphic', | 
|  | componentIndex: graphicModel.componentIndex, | 
|  | name: el.name | 
|  | }; | 
|  | } | 
|  |  | 
|  | // `elOption.info` enables user to mount some info on | 
|  | // elements and use them in event handlers. | 
|  | if (eventData) { | 
|  | eventData.info = elOption.info; | 
|  | } | 
|  | } |