import { IGroup } from '@antv/g-base' ; import { clone , deepMix , each , isArray , isObject , isString , upperFirst , throttle } from '@antv/util' ; import Edge from '../../item/edge' ; import Node from '../../item/node' ; import Combo from '../../item/combo' ; import { EdgeConfig , Item , ITEM_TYPE , ModelConfig , NodeConfig , NodeMap , ComboTree , ComboConfig , UpdateType , } from '../../types' ; import { IAbstractGraph } from '../../interface/graph' ; import { IEdge , INode , ICombo } from '../../interface/item' ; import { traverseTreeUp , traverseTree , getComboBBox , shouldRefreshEdge } from '../../util/graphic' ; const NODE = 'node' ; const EDGE = 'edge' ; const VEDGE = 'vedge' ; const COMBO = 'combo' ; const CFG_PREFIX = 'default' ; const MAPPER_SUFFIX = 'Mapper' ; const STATE_SUFFIX = 'stateStyles' ; type Id = string | Item | undefined ; export default class ItemController { private graph : IAbstractGraph ; public destroyed : boolean ; private edgeToBeUpdateMap : { [ key ] : { edge : IEdge ; updateType : UpdateType ; } ; } = { } ; constructor ( graph ) { this . graph = graph ; this . destroyed = false ; } param { ITEM_TYPE } type param { ( NodeConfig & EdgeConfig ) } model returns { ( Item ) } memberof public addItem < T extends Item > ( type , model ) { const { graph } = this ; const vType = type === VEDGE ? EDGE : type ; const parent = graph . get ( `${ vType }Group` ) || graph . get ( 'group' ) ; const upperType = upperFirst ( vType ) ; let item = null ; let styles = graph . get ( vType + upperFirst ( STATE_SUFFIX ) ) || { } ; const defaultModel = graph . get ( CFG_PREFIX + upperType ) ; if ( model [ STATE_SUFFIX ] ) { styles = model [ STATE_SUFFIX ] ; } if ( defaultModel ) { each ( defaultModel , ( val , cfg ) => { if ( isObject ( val ) && ! isArray ( val ) ) { model [ cfg ] = deepMix ( { } , val , model [ cfg ] ) ; } else if ( isArray ( val ) ) { model [ cfg ] = model [ cfg ] || clone ( defaultModel [ cfg ] ) ; } else { model [ cfg ] = model [ cfg ] || defaultModel [ cfg ] ; } } ) ; } const mapper = graph . get ( vType + MAPPER_SUFFIX ) ; if ( mapper ) { const mappedModel = mapper ( model ) ; if ( mappedModel [ STATE_SUFFIX ] ) { styles = mappedModel [ STATE_SUFFIX ] ; delete mappedModel [ STATE_SUFFIX ] ; } each ( mappedModel , ( val , cfg ) => { if ( isObject ( val ) && ! isArray ( val ) ) { model [ cfg ] = deepMix ( { } , model [ cfg ] , val ) ; } else { model [ cfg ] = mappedModel [ cfg ] || model [ cfg ] ; } } ) ; } graph . emit ( 'beforeadditem' , { type , model } ) ; if ( type === EDGE || type === VEDGE ) { let source ; let target ; source = ( model as EdgeConfig ) . source ; target = ( model as EdgeConfig ) . target ; if ( source && isString ( source ) ) { source = graph . findById ( source ) ; } if ( target && isString ( target ) ) { target = graph . findById ( target ) ; } if ( ! source || ! target ) { console . warn ( `The source or target node of edge ${ model . id } does not exist!` ) ; return ; } if ( ( source as Item ) . getType && ( source as Item ) . getType ( ) === 'combo' ) { model . isComboEdge = true ; } if ( ( target as Item ) . getType && ( target as Item ) . getType ( ) === 'combo' ) { model . isComboEdge = true ; } item = new Edge ( { model , source , target , styles , linkCenter : graph . get ( 'linkCenter' ) , group : parent . addGroup ( ) , } ) ; } else if ( type === NODE ) { item = new Node ( { model , styles , group : parent . addGroup ( ) , } ) ; } else if ( type === COMBO ) { const children = ( model as ComboConfig ) . children ; const comboBBox = getComboBBox ( children , graph ) ; if ( ! isNaN ( comboBBox . x ) ) model . x = comboBBox . x ; else if ( isNaN ( model . x ) ) model . x = Math . random ( ) * 100 ; if ( ! isNaN ( comboBBox . y ) ) model . y = comboBBox . y ; else if ( isNaN ( model . y ) ) model . y = Math . random ( ) * 100 ; const comboGroup = parent . addGroup ( ) ; comboGroup . setZIndex ( ( model as ComboConfig ) . depth as number ) ; item = new Combo ( { model , styles , bbox : model . collapsed ? getComboBBox ( [ ] , graph ) : comboBBox , group : comboGroup , } ) ; const comboModel = item . getModel ( ) ; ( children || [ ] ) . forEach ( ( child ) => { const childItem = graph . findById ( child . id ) as ICombo | INode ; ( item as ICombo ) . addChild ( childItem ) ; child . depth = ( comboModel . depth as number ) + 2 ; } ) ; if ( model . collapsed ) { setTimeout ( ( ) => { if ( ! item . destroyed ) { graph . collapseCombo ( item as ICombo ) ; } } , 0 ) ; } } if ( item ) { graph . get ( `${ type }s` ) . push ( item ) ; graph . get ( 'itemMap' ) [ item . get ( 'id' ) ] = item ; graph . emit ( 'afteradditem' , { item , model } ) ; return item as T ; } } param { Item } item param { ( EdgeConfig | Partial < NodeConfig > ) } cfg returns memberof public updateItem ( item , cfg ) { const { graph } = this ; if ( isString ( item ) ) { item = graph . findById ( item ) as Item ; } if ( ! item || item . destroyed ) { return ; } let type = '' ; if ( item . getType ) type = item . getType ( ) ; const mapper = graph . get ( type + MAPPER_SUFFIX ) ; const model = item . getModel ( ) ; const updateType = item . getUpdateType ( cfg ) ; if ( mapper ) { const result = deepMix ( { } , model , cfg ) ; const mappedModel = mapper ( result ) ; const newModel = deepMix ( { } , model , mappedModel , cfg ) ; if ( mappedModel [ STATE_SUFFIX ] ) { item . set ( 'styles' , newModel [ STATE_SUFFIX ] ) ; delete newModel [ STATE_SUFFIX ] ; } each ( newModel , ( val , key ) => { cfg [ key ] = val ; } ) ; } else { each ( cfg , ( val , key ) => { if ( model [ key ] ) { if ( isObject ( val ) && ! isArray ( val ) ) { cfg [ key ] = { ... ( model [ key ] as Object ) , ... ( cfg [ key ] as Object ) } ; } } } ) ; } graph . emit ( 'beforeupdateitem' , { item , cfg } ) ; if ( type === EDGE ) { if ( cfg . source ) { let source = cfg . source as INode ; if ( isString ( source ) ) { source = graph . findById ( source ) as INode ; } ( item as IEdge ) . setSource ( source ) ; } if ( cfg . target ) { let target = cfg . target as INode ; if ( isString ( target ) ) { target = graph . findById ( target ) as INode ; } ( item as IEdge ) . setTarget ( target ) ; } item . update ( cfg ) ; } if ( type === NODE || type === COMBO ) { item . update ( cfg , updateType ) ; const edges = ( item as INode ) . getEdges ( ) ; const refreshEdge = updateType ?. includes ( 'bbox' ) || updateType === 'move' ; if ( type === NODE ) { if ( updateType === 'move' ) { each ( edges , ( edge ) => { this . edgeToBeUpdateMap [ edge . getID ( ) ] = { edge : edge , updateType } ; this . throttleRefresh ( ) ; } ) ; } else if ( refreshEdge ) { each ( edges , ( edge ) => { edge . refresh ( updateType ) ; } ) ; } } else if ( refreshEdge && type === COMBO ) { const shapeFactory = item . get ( 'shapeFactory' ) ; const shapeType = ( model . type as string ) || 'circle' ; const comboAnimate = model . animate === undefined || cfg . animate === undefined ? shapeFactory [ shapeType ] ?. options ?. animate : model . animate || cfg . animate ; if ( comboAnimate ) { setTimeout ( ( ) => { if ( ! item || ( item as ICombo ) . destroyed ) return ; const keyShape = ( item as ICombo ) . getKeyShape ( ) ; if ( ! keyShape || keyShape . destroyed ) return ; each ( edges , ( edge ) => { if ( edge && ! edge . destroyed ) edge . refresh ( ) ; } ) ; } , 201 ) ; } else { each ( edges , ( edge ) => { edge . refresh ( ) ; } ) ; } } } graph . emit ( 'afterupdateitem' , { item , cfg } ) ; } /**
     * 更新边限流，同时可以防止相同的边频繁重复更新
     * */ private throttleRefresh = throttle ( _ => { const { graph } = this ; if ( ! graph || graph . get ( 'destroyed' ) ) return ; const edgeToBeUpdateMap = this . edgeToBeUpdateMap ; if ( ! edgeToBeUpdateMap || ! Object . keys ( edgeToBeUpdateMap ) ?. length ) return ; Object . keys ( edgeToBeUpdateMap ) . forEach ( eid => { const edge = edgeToBeUpdateMap [ eid ] . edge ; if ( ! edge || edge . destroyed ) return ; edge . refresh ( edgeToBeUpdateMap [ eid ] . updateType ) ; } ) ; this . edgeToBeUpdateMap = { } ; } , 16 , { trailing : true , leading : true , } ) ; param { ICombo } combo returns memberof public updateCombo ( combo , children ) { const { graph } = this ; if ( isString ( combo ) ) { combo = graph . findById ( combo ) as ICombo ; } if ( ! combo || combo . destroyed ) { return ; } const model = combo . getModel ( ) ; const comboBBox = getComboBBox ( children , graph , combo ) ; const { x : comboX , y : comboY } = comboBBox ; combo . set ( 'bbox' , comboBBox ) ; combo . update ( { x : comboX || model . x , y : comboY || model . y , } ) ; const shapeFactory = combo . get ( 'shapeFactory' ) ; const shapeType = ( model . type as string ) || 'circle' ; const comboAnimate = model . animate === undefined ? shapeFactory [ shapeType ] ?. options ?. animate : model . animate ; if ( comboAnimate ) { setTimeout ( ( ) => { if ( ! combo || ( combo as ICombo ) . destroyed ) return ; const keyShape = ( combo as ICombo ) . getKeyShape ( ) ; if ( ! keyShape || keyShape . destroyed ) return ; ( combo as ICombo ) . getShapeCfg ( model ) ; this . updateComboEdges ( combo as ICombo ) ; } , 201 ) ; } else { this . updateComboEdges ( combo as ICombo ) ; } } private updateComboEdges ( combo ) { const combEdges = combo . getEdges ( ) || [ ] ; for ( let i = 0 ; i < combEdges . length ; i ++ ) { const edge = combEdges [ i ] ; if ( edge && ! edge . destroyed ) { const edgeSF = edge . get ( 'shapeFactory' ) ; const edgeCfg = edge . getShapeCfg ( edge . getModel ( ) ) ; const edgeGroup = edge . getContainer ( ) ; edgeGroup . clear ( ) ; const keyShape = edgeSF . draw ( edgeCfg . type , edgeCfg , edgeGroup ) ; edge . set ( 'keyShape' , keyShape ) ; keyShape . set ( 'isKeyShape' , true ) ; keyShape . set ( 'draggable' , true ) ; edge . setOriginStyle ( ) ; } } } /**
     * 收起 combo，隐藏相关元素
     */ public collapseCombo ( combo ) { const graph = this . graph ; if ( isString ( combo ) ) { combo = graph . findById ( combo ) as ICombo ; } const children = ( combo as ICombo ) . getChildren ( ) ; children . nodes . forEach ( ( node ) => { graph . hideItem ( node ) ; } ) ; children . combos . forEach ( ( c ) => { graph . hideItem ( c ) ; } ) ; } /**
     * 展开 combo，相关元素出现
     * 若子 combo 原先是收起状态，则保持它的收起状态
     */ public expandCombo ( combo ) { const graph = this . graph ; if ( isString ( combo ) ) { combo = graph . findById ( combo ) as ICombo ; } const children = ( combo as ICombo ) . getChildren ( ) ; children . nodes . forEach ( ( node ) => { graph . showItem ( node ) ; } ) ; children . combos . forEach ( ( c ) => { if ( c . getModel ( ) . collapsed ) { c . show ( ) ; } else { graph . showItem ( c ) ; } } ) ; } param { Item } item returns { void } memberof public removeItem ( item ) { const { graph } = this ; if ( isString ( item ) ) { item = graph . findById ( item ) ; } if ( ! item || item . destroyed ) { return ; } const itemModel = clone ( item . getModel ( ) ) ; graph . emit ( 'beforeremoveitem' , { item : itemModel } ) ; let type = '' ; if ( item . getType ) type = item . getType ( ) ; const items = graph . get ( `${ type }s` ) ; const index = items . indexOf ( item ) ; if ( index > - 1 ) items . splice ( index , 1 ) ; if ( type === EDGE ) { const vitems = graph . get ( `v${ type }s` ) ; const vindex = vitems . indexOf ( item ) ; if ( vindex > - 1 ) vitems . splice ( vindex , 1 ) ; } const itemId = item . get ( 'id' ) ; const itemMap = graph . get ( 'itemMap' ) ; delete itemMap [ itemId ] ; const comboTrees = graph . get ( 'comboTrees' ) ; const id = item . get ( 'id' ) ; if ( type === NODE ) { const comboId = item . getModel ( ) . comboId as string ; if ( comboTrees && comboId ) { let brothers = comboTrees ; let found = false ; comboTrees . forEach ( ( ctree ) => { if ( found ) return ; traverseTree < ComboTree > ( ctree , ( combo ) => { if ( combo . id === id && brothers ) { const bidx = brothers . indexOf ( combo ) ; brothers . splice ( bidx , 1 ) ; found = true ; return false ; } brothers = combo . children ; return true ; } ) ; } ) ; } const edges = ( item as INode ) . getEdges ( ) ; for ( let i = edges . length - 1 ; i >= 0 ; i -- ) { graph . removeItem ( edges [ i ] , false ) ; } if ( comboId ) graph . updateCombo ( comboId ) ; } else if ( type === COMBO ) { const parentId = item . getModel ( ) . parentId as string ; let comboInTree ; let found = false ; ( comboTrees || [ ] ) . forEach ( ( ctree ) => { if ( found ) return ; traverseTree < ComboTree > ( ctree , ( combo ) => { if ( combo . id === id ) { comboInTree = combo ; found = true ; return false ; } return true ; } ) ; } ) ; comboInTree . removed = true ; if ( comboInTree && comboInTree . children ) { comboInTree . children . forEach ( ( child ) => { this . removeItem ( child . id ) ; } ) ; } const edges = ( item as ICombo ) . getEdges ( ) ; for ( let i = edges . length ; i >= 0 ; i -- ) { graph . removeItem ( edges [ i ] , false ) ; } if ( parentId ) graph . updateCombo ( parentId ) ; } item . destroy ( ) ; graph . emit ( 'afterremoveitem' , { item : itemModel , type } ) ; } param { Item } item param { string } state param { boolean } value returns { void } memberof public setItemState ( item , state , value ) { const { graph } = this ; let stateName = state ; if ( isString ( value ) ) { stateName = `${ state }:${ value }` ; } if ( ( item . hasState ( stateName ) === value && value ) || ( isString ( value ) && item . hasState ( stateName ) ) ) { return ; } graph . emit ( 'beforeitemstatechange' , { item , state : stateName , enabled : value } ) ; item . setState ( state , value ) ; graph . autoPaint ( ) ; graph . emit ( 'afteritemstatechange' , { item , state : stateName , enabled : value } ) ; } param { Item } item param state public priorityState ( item , state ) { const { graph } = this ; let currentItem = item ; if ( isString ( item ) ) { currentItem = graph . findById ( item ) ; } this . setItemState ( currentItem as Item , state , false ) ; this . setItemState ( currentItem as Item , state , true ) ; } param { Item } item param { string [ ] } states memberof public clearItemStates ( item , states ? ) { const { graph } = this ; if ( isString ( item ) ) { item = graph . findById ( item ) ; } graph . emit ( 'beforeitemstatesclear' , { item , states } ) ; item . clearStates ( states ) ; graph . emit ( 'afteritemstatesclear' , { item , states } ) ; } param { Item } item memberof public refreshItem ( item ) { const { graph } = this ; if ( isString ( item ) ) { item = graph . findById ( item ) ; } graph . emit ( 'beforeitemrefresh' , { item } ) ; item . refresh ( ) ; graph . emit ( 'afteritemrefresh' , { item } ) ; } param { ComboTree [ ] } comboTrees param { ComboConfig [ ] } comboModels memberof public addCombos ( comboTrees , comboModels ) { const { graph } = this ; ( comboTrees || [ ] ) . forEach ( ( ctree ) => { traverseTreeUp < ComboTree > ( ctree , ( child ) => { let comboModel ; comboModels . forEach ( ( model ) => { if ( model . id === child . id ) { model . children = child . children ; model . depth = child . depth ; comboModel = model ; } } ) ; if ( comboModel ) { this . addItem ( 'combo' , comboModel ) ; } return true ; } ) ; } ) ; const comboGroup = graph . get ( 'comboGroup' ) ; if ( comboGroup ) comboGroup . sort ( ) ; } param { Item } item param { boolean } visible memberof public changeItemVisibility ( item , visible ) { const { graph } = this ; if ( isString ( item ) ) { item = graph . findById ( item ) ; } if ( ! item ) { console . warn ( 'The item to be shown or hidden does not exist!' ) ; return ; } graph . emit ( 'beforeitemvisibilitychange' , { item , visible } ) ; item . changeVisibility ( visible ) ; if ( item . getType && item . getType ( ) === NODE ) { const edges = ( item as INode ) . getEdges ( ) ; each ( edges , ( edge ) => { if ( visible && ! ( edge . get ( 'source' ) . isVisible ( ) && edge . get ( 'target' ) . isVisible ( ) ) ) { return ; } this . changeItemVisibility ( edge , visible ) ; } ) ; } else if ( item . getType && item . getType ( ) === COMBO ) { const comboTrees = graph . get ( 'comboTrees' ) ; const id = item . get ( 'id' ) ; let children = [ ] ; let found = false ; ( comboTrees || [ ] ) . forEach ( ( ctree ) => { if ( found ) return ; if ( ! ctree . children || ctree . children . length === 0 ) return ; traverseTree < ComboTree > ( ctree , ( combo ) => { if ( combo . id === id ) { children = combo . children ; found = true ; return false ; } return true ; } ) ; } ) ; if ( children && ( ! visible || ( visible && ! item . getModel ( ) . collapsed ) ) ) { children . forEach ( ( child ) => { const childItem = graph . findById ( child . id ) ; this . changeItemVisibility ( childItem , visible ) ; } ) ; } const edges = ( item as INode ) . getEdges ( ) ; each ( edges , ( edge ) => { if ( visible && ! ( edge . get ( 'source' ) . isVisible ( ) && edge . get ( 'target' ) . isVisible ( ) ) ) { return ; } this . changeItemVisibility ( edge , visible ) ; } ) ; } graph . emit ( 'afteritemvisibilitychange' , { item , visible } ) ; return item ; } public destroy ( ) { ( this . graph as IAbstractGraph | null ) = null ; this . destroyed = true ; } }