/* * 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 * as modelUtil from '../../util/model'; import { ComponentOption, BoxLayoutOptionMixin, Dictionary, ZRStyleProps, OptionId, CommonTooltipOption, AnimationOptionMixin, AnimationOption } from '../../util/types'; import ComponentModel from '../../model/Component'; import Element, { ElementTextConfig } from 'zrender/src/Element'; import Displayable from 'zrender/src/graphic/Displayable'; import { PathProps, PathStyleProps } from 'zrender/src/graphic/Path'; import { ImageStyleProps, ImageProps } from 'zrender/src/graphic/Image'; import { TextStyleProps, TextProps } from 'zrender/src/graphic/Text'; import GlobalModel from '../../model/Global'; import { copyLayoutParams, mergeLayoutParam } from '../../util/layout'; import { TransitionOptionMixin } from '../../animation/customGraphicTransition'; import { ElementKeyframeAnimationOption } from '../../animation/customGraphicKeyframeAnimation'; import { GroupProps } from 'zrender/src/graphic/Group'; import { TransformProp } from 'zrender/src/core/Transformable'; import { ElementEventNameWithOn } from 'zrender/src/core/types'; interface GraphicComponentBaseElementOption extends Partial>, /** * left/right/top/bottom: (like 12, '22%', 'center', default undefined) * If left/right is set, shape.x/shape.cx/position will not be used. * If top/bottom is set, shape.y/shape.cy/position will not be used. * This mechanism is useful when you want to position a group/element * against the right side or the center of this container. */ Partial> { /** * element type, mandatory. * Only can be omit if call setOption not at the first time and perform merge. */ type?: string; id?: OptionId; name?: string; // Only internal usage. Use specified value does NOT make sense. parentId?: OptionId; parentOption?: GraphicComponentElementOption; children?: GraphicComponentElementOption[]; hv?: [boolean, boolean]; /** * bounding: (enum: 'all' (default) | 'raw') * Specify how to calculate boundingRect when locating. * 'all': Get uioned and transformed boundingRect * from both itself and its descendants. * This mode simplies confining a group of elements in the bounding * of their ancester container (e.g., using 'right: 0'). * 'raw': Only use the boundingRect of itself and before transformed. * This mode is similar to css behavior, which is useful when you * want an element to be able to overflow its container. (Consider * a rotated circle needs to be located in a corner.) */ bounding?: 'raw' | 'all'; /** * info: custom info. enables user to mount some info on elements and use them * in event handlers. Update them only when user specified, otherwise, remain. */ info?: GraphicExtraElementInfo; // `false` means remove the clipPath clipPath?: Omit | false; textContent?: Omit; textConfig?: ElementTextConfig; $action?: 'merge' | 'replace' | 'remove'; tooltip?: CommonTooltipOption; enterAnimation?: AnimationOption updateAnimation?: AnimationOption leaveAnimation?: AnimationOption }; export interface GraphicComponentDisplayableOption extends GraphicComponentBaseElementOption, Partial> { style?: ZRStyleProps z2?: number } // TODO: states? // interface GraphicComponentDisplayableOptionOnState extends Partial> { // style?: ZRStyleProps; // } export interface GraphicComponentGroupOption extends GraphicComponentBaseElementOption, TransitionOptionMixin { type?: 'group'; /** * width/height: (can only be pixel value, default 0) * Is only used to specify container (group) size, if needed. And * cannot be a percentage value (like '33%'). See the reason in the * layout algorithm below. */ width?: number; height?: number; // TODO: Can only set focus, blur on the root element. // children: Omit[]; children: GraphicComponentElementOption[]; keyframeAnimation?: ElementKeyframeAnimationOption | ElementKeyframeAnimationOption[] }; export interface GraphicComponentZRPathOption extends GraphicComponentDisplayableOption, TransitionOptionMixin { shape?: PathProps['shape'] & TransitionOptionMixin; style?: PathStyleProps & TransitionOptionMixin keyframeAnimation?: ElementKeyframeAnimationOption | ElementKeyframeAnimationOption[]; } export interface GraphicComponentImageOption extends GraphicComponentDisplayableOption, TransitionOptionMixin { type?: 'image'; style?: ImageStyleProps & TransitionOptionMixin; keyframeAnimation?: ElementKeyframeAnimationOption | ElementKeyframeAnimationOption[]; } // TODO: states? // interface GraphicComponentImageOptionOnState extends GraphicComponentDisplayableOptionOnState { // style?: ImageStyleProps; // } export interface GraphicComponentTextOption extends Omit, TransitionOptionMixin { type?: 'text'; style?: TextStyleProps & TransitionOptionMixin; keyframeAnimation?: ElementKeyframeAnimationOption | ElementKeyframeAnimationOption[]; } export type GraphicComponentElementOption = GraphicComponentGroupOption | GraphicComponentZRPathOption | GraphicComponentImageOption | GraphicComponentTextOption; // type GraphicComponentElementOptionOnState = // GraphicComponentDisplayableOptionOnState // | GraphicComponentImageOptionOnState; type GraphicExtraElementInfo = Dictionary; export type ElementMap = zrUtil.HashMap; export type GraphicComponentLooseOption = (GraphicComponentOption | GraphicComponentElementOption) & { mainType?: 'graphic'; }; export interface GraphicComponentOption extends ComponentOption, AnimationOptionMixin { // Note: elements is always behind its ancestors in this elements array. elements?: GraphicComponentElementOption[]; }; export function setKeyInfoToNewElOption( resultItem: ReturnType[number], newElOption: GraphicComponentElementOption ): void { const existElOption = resultItem.existing as GraphicComponentElementOption; // Set id and type after id assigned. newElOption.id = resultItem.keyInfo.id; !newElOption.type && existElOption && (newElOption.type = existElOption.type); // Set parent id if not specified if (newElOption.parentId == null) { const newElParentOption = newElOption.parentOption; if (newElParentOption) { newElOption.parentId = newElParentOption.id; } else if (existElOption) { newElOption.parentId = existElOption.parentId; } } // Clear newElOption.parentOption = null; } function isSetLoc( obj: GraphicComponentElementOption, props: ('left' | 'right' | 'top' | 'bottom')[] ): boolean { let isSet; zrUtil.each(props, function (prop) { obj[prop] != null && obj[prop] !== 'auto' && (isSet = true); }); return isSet; } function mergeNewElOptionToExist( existList: GraphicComponentElementOption[], index: number, newElOption: GraphicComponentElementOption ): void { // Update existing options, for `getOption` feature. const newElOptCopy = zrUtil.extend({}, newElOption); const existElOption = existList[index]; const $action = newElOption.$action || 'merge'; if ($action === 'merge') { if (existElOption) { if (__DEV__) { const newType = newElOption.type; zrUtil.assert( !newType || existElOption.type === newType, 'Please set $action: "replace" to change `type`' ); } // We can ensure that newElOptCopy and existElOption are not // the same object, so `merge` will not change newElOptCopy. zrUtil.merge(existElOption, newElOptCopy, true); // Rigid body, use ignoreSize. mergeLayoutParam(existElOption, newElOptCopy, { ignoreSize: true }); // Will be used in render. copyLayoutParams(newElOption, existElOption); // Copy transition info to new option so it can be used in the transition. // DO IT AFTER merge copyTransitionInfo(newElOption, existElOption); copyTransitionInfo(newElOption, existElOption, 'shape'); copyTransitionInfo(newElOption, existElOption, 'style'); copyTransitionInfo(newElOption, existElOption, 'extra'); // Copy clipPath newElOption.clipPath = existElOption.clipPath; } else { existList[index] = newElOptCopy; } } else if ($action === 'replace') { existList[index] = newElOptCopy; } else if ($action === 'remove') { // null will be cleaned later. existElOption && (existList[index] = null); } } const TRANSITION_PROPS_TO_COPY = ['transition', 'enterFrom', 'leaveTo']; const ROOT_TRANSITION_PROPS_TO_COPY = TRANSITION_PROPS_TO_COPY.concat(['enterAnimation', 'updateAnimation', 'leaveAnimation']); function copyTransitionInfo( target: GraphicComponentElementOption, source: GraphicComponentElementOption, targetProp?: string ) { if (targetProp) { if (!(target as any)[targetProp] && (source as any)[targetProp] ) { // TODO avoid creating this empty object when there is no transition configuration. (target as any)[targetProp] = {}; } target = (target as any)[targetProp]; source = (source as any)[targetProp]; } if (!target || !source) { return; } const props = targetProp ? TRANSITION_PROPS_TO_COPY : ROOT_TRANSITION_PROPS_TO_COPY; for (let i = 0; i < props.length; i++) { const prop = props[i]; if ((target as any)[prop] == null && (source as any)[prop] != null) { (target as any)[prop] = (source as any)[prop]; } } } function setLayoutInfoToExist( existItem: GraphicComponentElementOption, newElOption: GraphicComponentElementOption ) { if (!existItem) { return; } existItem.hv = newElOption.hv = [ // Rigid body, don't care about `width`. isSetLoc(newElOption, ['left', 'right']), // Rigid body, don't care about `height`. isSetLoc(newElOption, ['top', 'bottom']) ]; // Give default group size. Otherwise layout error may occur. if (existItem.type === 'group') { const existingGroupOpt = existItem as GraphicComponentGroupOption; const newGroupOpt = newElOption as GraphicComponentGroupOption; existingGroupOpt.width == null && (existingGroupOpt.width = newGroupOpt.width = 0); existingGroupOpt.height == null && (existingGroupOpt.height = newGroupOpt.height = 0); } } export class GraphicComponentModel extends ComponentModel { static type = 'graphic'; type = GraphicComponentModel.type; preventAutoZ = true; static defaultOption: GraphicComponentOption = { elements: [] // parentId: null }; /** * Save el options for the sake of the performance (only update modified graphics). * The order is the same as those in option. (ancesters -> descendants) */ private _elOptionsToUpdate: GraphicComponentElementOption[]; mergeOption(option: GraphicComponentOption, ecModel: GlobalModel): void { // Prevent default merge to elements const elements = this.option.elements; this.option.elements = null; super.mergeOption(option, ecModel); this.option.elements = elements; } optionUpdated(newOption: GraphicComponentOption, isInit: boolean): void { const thisOption = this.option; const newList = (isInit ? thisOption : newOption).elements; const existList = thisOption.elements = isInit ? [] : thisOption.elements; const flattenedList = [] as GraphicComponentElementOption[]; this._flatten(newList, flattenedList, null); const mappingResult = modelUtil.mappingToExists(existList, flattenedList, 'normalMerge'); // Clear elOptionsToUpdate const elOptionsToUpdate = this._elOptionsToUpdate = [] as GraphicComponentElementOption[]; zrUtil.each(mappingResult, function (resultItem, index) { const newElOption = resultItem.newOption as GraphicComponentElementOption; if (__DEV__) { zrUtil.assert( zrUtil.isObject(newElOption) || resultItem.existing, 'Empty graphic option definition' ); } if (!newElOption) { return; } elOptionsToUpdate.push(newElOption); setKeyInfoToNewElOption(resultItem, newElOption); mergeNewElOptionToExist(existList, index, newElOption); setLayoutInfoToExist(existList[index], newElOption); }, this); // Clean thisOption.elements = zrUtil.filter(existList, (item) => { // $action should be volatile, otherwise option gotten from // `getOption` will contain unexpected $action. item && delete item.$action; return item != null; }); } /** * Convert * [{ * type: 'group', * id: 'xx', * children: [{type: 'circle'}, {type: 'polygon'}] * }] * to * [ * {type: 'group', id: 'xx'}, * {type: 'circle', parentId: 'xx'}, * {type: 'polygon', parentId: 'xx'} * ] */ private _flatten( optionList: GraphicComponentElementOption[], result: GraphicComponentElementOption[], parentOption: GraphicComponentElementOption ): void { zrUtil.each(optionList, function (option) { if (!option) { return; } if (parentOption) { option.parentOption = parentOption; } result.push(option); const children = option.children; // here we don't judge if option.type is `group` // when new option doesn't provide `type`, it will cause that the children can't be updated. if (children && children.length) { this._flatten(children, result, option); } // Deleting for JSON output, and for not affecting group creation. delete option.children; }, this); } // FIXME // Pass to view using payload? setOption has a payload? useElOptionsToUpdate(): GraphicComponentElementOption[] { const els = this._elOptionsToUpdate; // Clear to avoid render duplicately when zooming. this._elOptionsToUpdate = null; return els; } }