import { ElementTypeIri , LinkTypeIri , PropertyTypeIri } from '../data/model' ; import { OrderedMap } from '../viewUtils/collections' ; import { EventSource , Events , AnyEvent , AnyListener , } from '../viewUtils/events' ; import { Element as DiagramElement , ElementEvents , Link as DiagramLink , LinkEvents , FatLinkType , FatLinkTypeEvents , FatClassModel , FatClassModelEvents , RichProperty , } from './elements' ; export interface GraphEvents { changeCells : CellsChangedEvent ; elementEvent : AnyEvent < ElementEvents > ; linkEvent : AnyEvent < LinkEvents > ; linkTypeEvent : AnyEvent < FatLinkTypeEvents > ; classEvent : AnyEvent < FatClassModelEvents > ; } export interface CellsChangedEvent { readonly updateAll : boolean ; readonly changedElement ? : DiagramElement ; readonly changedLinks ? : ReadonlyArray < DiagramLink > ; } export class Graph { private readonly source = new EventSource < GraphEvents > ( ) ; readonly events : Events < GraphEvents > = this . source ; private elements = new OrderedMap < DiagramElement > ( ) ; private links = new OrderedMap < DiagramLink > ( ) ; private classesById = new Map < ElementTypeIri , FatClassModel > ( ) ; private propertiesById = new Map < PropertyTypeIri , RichProperty > ( ) ; private linkTypes = new Map < LinkTypeIri , FatLinkType > ( ) ; private static nextLinkTypeIndex = 0 ; getElements ( ) { return this . elements . items ; } getLinks ( ) { return this . links . items ; } getLink ( linkId ) { return this . links . get ( linkId ) ; } findLink ( linkTypeId , sourceId , targetId ) { const source = this . getElement ( sourceId ) ; if ( ! source ) { return undefined ; } const index = findLinkIndex ( source . links , linkTypeId , sourceId , targetId ) ; return index >= 0 ? source . links [ index ] : undefined ; } sourceOf ( link ) { return this . getElement ( link . sourceId ) ; } targetOf ( link ) { return this . getElement ( link . targetId ) ; } reorderElements ( compare ) { this . elements . reorder ( compare ) ; } getElement ( elementId ) { return this . elements . get ( elementId ) ; } addElement ( element ) { if ( this . getElement ( element . id ) ) { throw new Error ( `Element '${ element . id }' already exists.` ) ; } element . events . onAny ( this . onElementEvent ) ; this . elements . push ( element . id , element ) ; this . source . trigger ( 'changeCells' , { updateAll : false , changedElement : element , } ) ; } private onElementEvent : AnyListener < ElementEvents > = ( data , key ) => { this . source . trigger ( 'elementEvent' , { key , data } ) ; } ; removeElement ( elementId ) { const element = this . elements . get ( elementId ) ; if ( element ) { const options = { silent : true } ; const changedLinks = [ ... element . links ] ; for ( const link of changedLinks ) { this . removeLink ( link . id , options ) ; } this . elements . delete ( elementId ) ; element . events . offAny ( this . onElementEvent ) ; this . source . trigger ( 'changeCells' , { updateAll : false , changedElement : element , changedLinks , } ) ; } } addLink ( link ) { if ( this . getLink ( link . id ) ) { throw new Error ( `Link '${ link . id }' already exists.` ) ; } const linkType = this . getLinkType ( link . typeId ) ; if ( ! linkType ) { throw new Error ( `Link type '${ link . typeId }' not found.` ) ; } this . registerLink ( link ) ; } private registerLink ( link ) { this . sourceOf ( link ) . links . push ( link ) ; if ( link . sourceId !== link . targetId ) { this . targetOf ( link ) . links . push ( link ) ; } link . events . onAny ( this . onLinkEvent ) ; this . links . push ( link . id , link ) ; this . source . trigger ( 'changeCells' , { updateAll : false , changedLinks : [ link ] , } ) ; } private onLinkEvent : AnyListener < LinkEvents > = ( data , key ) => { this . source . trigger ( 'linkEvent' , { key , data } ) ; } ; removeLink ( linkId , options ? ) { const link = this . links . delete ( linkId ) ; if ( link ) { const { typeId , sourceId , targetId } = link ; link . events . offAny ( this . onLinkEvent ) ; this . removeLinkReferences ( typeId , sourceId , targetId ) ; if ( ! ( options && options . silent ) ) { this . source . trigger ( 'changeCells' , { updateAll : false , changedLinks : [ link ] , } ) ; } } } private removeLinkReferences ( linkTypeId , sourceId , targetId ) { const source = this . getElement ( sourceId ) ; if ( source ) { removeLinkFrom ( source . links , linkTypeId , sourceId , targetId ) ; } const target = this . getElement ( targetId ) ; if ( target ) { removeLinkFrom ( target . links , linkTypeId , sourceId , targetId ) ; } } getLinkTypes ( ) { const result = [ ] ; this . linkTypes . forEach ( ( type ) => result . push ( type ) ) ; return result ; } getLinkType ( linkTypeId ) { return this . linkTypes . get ( linkTypeId ) ; } addLinkType ( linkType ) { if ( this . getLinkType ( linkType . id ) ) { throw new Error ( `Link type '${ linkType . id }' already exists.` ) ; } linkType . setIndex ( Graph . nextLinkTypeIndex ++ ) ; linkType . events . onAny ( this . onLinkTypeEvent ) ; this . linkTypes . set ( linkType . id , linkType ) ; } private onLinkTypeEvent : AnyListener < FatLinkTypeEvents > = ( data , key ) => { this . source . trigger ( 'linkTypeEvent' , { key , data } ) ; } ; getProperty ( propertyId ) { return this . propertiesById . get ( propertyId ) ; } addProperty ( property ) { if ( this . getProperty ( property . id ) ) { throw new Error ( `Property '${ property . id }' already exists.` ) ; } this . propertiesById . set ( property . id , property ) ; } getClass ( classId ) { return this . classesById . get ( classId ) ; } getClasses ( ) { const classes = [ ] ; this . classesById . forEach ( ( richClass ) => classes . push ( richClass ) ) ; return classes ; } addClass ( classModel ) { if ( this . getClass ( classModel . id ) ) { throw new Error ( `Class '${ classModel . id }' already exists.` ) ; } classModel . events . onAny ( this . onClassEvent ) ; this . classesById . set ( classModel . id , classModel ) ; } private onClassEvent : AnyListener < FatClassModelEvents > = ( data , key ) => { this . source . trigger ( 'classEvent' , { key , data } ) ; } ; } function removeLinkFrom ( links , linkTypeId , sourceId , targetId ) { if ( ! links ) { return ; } while ( true ) { const index = findLinkIndex ( links , linkTypeId , sourceId , targetId ) ; if ( index < 0 ) { break ; } links . splice ( index , 1 ) ; } } function findLinkIndex ( haystack , linkTypeId , sourceId , targetId ) { for ( let i = 0 ; i < haystack . length ; i ++ ) { const link = haystack [ i ] ; if ( link . sourceId === sourceId && link . targetId === targetId && link . typeId === linkTypeId ) { return i ; } } return - 1 ; }