import Node , { Colors , Coordinates , ExportNodeProperties , Font , Image , NodeProperties , UserNodeProperties } from "../models/node" ; import Map from "../map" ; import * as d3 from "d3" ; import { Map as D3Map } from "d3-collection" ; import { Event } from "./events" ; import Log from "../../utils/log" ; import Utils from "../../utils/utils" ; /**
 * Manage the nodes of the map.
 */ export default class Nodes { private map : Map ; private counter : number ; private nodes : D3Map < Node > ; private selectedNode : Node ; param { Map } map constructor ( map ) { this . map = map ; this . counter = 0 ; this . nodes = d3 . map ( ) ; } param { Coordinates } coordinates public addRootNode ( coordinates ? ) { let properties = Utils . mergeObjects ( this . map . options . rootNode , { coordinates : { x : this . map . dom . container . node ( ) . clientWidth / 2 , y : this . map . dom . container . node ( ) . clientHeight / 2 } , locked : false , id : this . map . id + "_node_" + this . counter , parent : null } ) as NodeProperties ; let node = new Node ( properties ) ; if ( coordinates ) { node . coordinates . x = coordinates . x || node . coordinates . x ; node . coordinates . y = coordinates . y || node . coordinates . y ; } this . nodes . set ( properties . id , node ) ; this . counter ++ ; this . map . draw . update ( ) ; this . selectRootNode ( ) ; } param { UserNodeProperties } userProperties param { string } id public addNode = ( userProperties ? , id ? ) => { if ( id && typeof id !== "string" ) { Log . error ( "The node id must be a string" , "type" ) ; } let parentNode = id ? this . getNode ( id ) : this . selectedNode ; if ( parentNode === undefined ) { Log . error ( "There are no nodes with id \"" + id + "\"" ) ; } let properties = Utils . mergeObjects ( this . map . options . defaultNode , userProperties , true ) as NodeProperties ; properties . id = this . map . id + "_node_" + this . counter ; properties . parent = parentNode ; let node = new Node ( properties ) ; this . nodes . set ( properties . id , node ) ; this . counter ++ ; node . coordinates = this . calculateCoordinates ( node ) ; if ( userProperties && userProperties . coordinates ) { let fixedCoordinates = this . fixCoordinates ( userProperties . coordinates ) ; node . coordinates = Utils . mergeObjects ( node . coordinates , fixedCoordinates , true ) as Coordinates ; } this . map . draw . update ( ) ; this . map . history . save ( ) ; this . map . events . call ( Event . nodeCreate , node . dom , this . getNodeProperties ( node ) ) ; } ; param { string } id returns { ExportNodeProperties } public selectNode = ( id ? ) : ExportNodeProperties => { if ( id !== undefined ) { if ( typeof id !== "string" ) { Log . error ( "The node id must be a string" , "type" ) ; } if ( ! this . nodeSelectionTo ( id ) ) { if ( this . nodes . has ( id ) ) { let node = this . nodes . get ( id ) , background = node . getBackgroundDOM ( ) ; if ( ! background . style . stroke ) { if ( this . selectedNode ) { this . selectedNode . getBackgroundDOM ( ) . style . stroke = "" ; } let color = d3 . color ( background . style . fill ) . darker ( .5 ) ; background . style . stroke = color . toString ( ) ; Utils . removeAllRanges ( ) ; this . selectedNode . getNameDOM ( ) . blur ( ) ; this . selectedNode = node ; this . map . events . call ( Event . nodeSelect , node . dom , this . getNodeProperties ( node ) ) ; } } else { Log . error ( "The node id or the direction is not correct" ) ; } } } return this . getNodeProperties ( this . selectedNode ) ; } ; /**
     * Enable the node name editing of the selected node.
     */ public editNode = ( ) => { if ( this . selectedNode ) { this . map . draw . enableNodeNameEditing ( this . selectedNode ) ; } } ; /**
     * Deselect the current selected node.
     */ public deselectNode = ( ) => { if ( this . selectedNode ) { this . selectedNode . getBackgroundDOM ( ) . style . stroke = "" ; Utils . removeAllRanges ( ) ; } this . selectRootNode ( ) ; this . map . events . call ( Event . nodeDeselect ) ; } ; param { string } property param value param { string } id param { boolean } graphic public updateNode = ( property , value , graphic = false , id ? ) => { if ( id && typeof id !== "string" ) { Log . error ( "The node id must be a string" , "type" ) ; } let node = id ? this . getNode ( id ) : this . selectedNode ; if ( node === undefined ) { Log . error ( "There are no nodes with id \"" + id + "\"" ) ; } if ( typeof property !== "string" ) { Log . error ( "The property must be a string" , "type" ) ; } let updated ; switch ( property ) { case "name" : updated = this . updateNodeName ( this . selectedNode , value , graphic ) ; break ; case "locked" : updated = this . updateNodeLockedStatus ( this . selectedNode , value ) ; break ; case "coordinates" : updated = this . updateNodeCoordinates ( this . selectedNode , value ) ; break ; case "imageSrc" : updated = this . updateNodeImageSrc ( this . selectedNode , value ) ; break ; case "imageSize" : updated = this . updateNodeImageSize ( this . selectedNode , value , graphic ) ; break ; case "backgroundColor" : updated = this . updateNodeBackgroundColor ( this . selectedNode , value , graphic ) ; break ; case "branchColor" : updated = this . updateNodeBranchColor ( this . selectedNode , value , graphic ) ; break ; case "fontWeight" : updated = this . updateNodeFontWeight ( this . selectedNode , value , graphic ) ; break ; case "textDecoration" : updated = this . updateNodeTextDecoration ( this . selectedNode , value , graphic ) ; break ; case "fontStyle" : updated = this . updateNodeFontStyle ( this . selectedNode , value , graphic ) ; break ; case "fontSize" : updated = this . updateNodeFontSize ( this . selectedNode , value , graphic ) ; break ; case "nameColor" : updated = this . updateNodeNameColor ( this . selectedNode , value , graphic ) ; break ; default : Log . error ( "The property does not exist" ) ; } if ( graphic === false && updated !== false ) { this . map . history . save ( ) ; this . map . events . call ( Event . nodeUpdate , this . selectedNode . dom , this . getNodeProperties ( this . selectedNode ) ) ; } } ; param { string } id public removeNode = ( id ? ) => { if ( id && typeof id !== "string" ) { Log . error ( "The node id must be a string" , "type" ) ; } let node = id ? this . getNode ( id ) : this . selectedNode ; if ( node === undefined ) { Log . error ( "There are no nodes with id \"" + id + "\"" ) ; } if ( ! node . isRoot ( ) ) { this . nodes . remove ( node . id ) ; this . getDescendants ( node ) . forEach ( ( node ) => { this . nodes . remove ( node . id ) ; } ) ; this . map . draw . clear ( ) ; this . map . draw . update ( ) ; this . map . history . save ( ) ; this . map . events . call ( Event . nodeRemove , null , this . getNodeProperties ( node ) ) ; this . deselectNode ( ) ; } else { Log . error ( "The root node can not be deleted" ) ; } } ; param { string } id returns { ExportNodeProperties [ ] } public nodeChildren = ( id ? ) : ExportNodeProperties [ ] => { if ( id && typeof id !== "string" ) { Log . error ( "The node id must be a string" , "type" ) ; } let node = id ? this . getNode ( id ) : this . selectedNode ; if ( node === undefined ) { Log . error ( "There are no nodes with id \"" + id + "\"" ) ; } return this . nodes . values ( ) . filter ( ( n ) => { return n . parent && n . parent . id === node . id ; } ) . map ( ( n ) => { return this . getNodeProperties ( n ) ; } ) ; } ; param { Node } node param { boolean } fixedCoordinates returns { ExportNodeProperties } public getNodeProperties ( node , fixedCoordinates = true ) { return { id : node . id , parent : node . parent ? node . parent . id : "" , name : node . name , coordinates : fixedCoordinates ? this . fixCoordinates ( node . coordinates , true ) : Utils . cloneObject ( node . coordinates ) as Coordinates , image : Utils . cloneObject ( node . image ) as Image , colors : Utils . cloneObject ( node . colors ) as Colors , font : Utils . cloneObject ( node . font ) as Font , locked : node . locked , k : node . k } ; } param { Coordinates } coordinates param { boolean } reverse returns { Coordinates } public fixCoordinates ( coordinates , reverse = false ) { let zoomCoordinates = d3 . zoomTransform ( this . map . dom . svg . node ( ) ) , fixedCoordinates = { } as Coordinates ; if ( coordinates . x ) { if ( reverse === false ) { fixedCoordinates . x = ( coordinates . x - zoomCoordinates . x ) / zoomCoordinates . k ; } else { fixedCoordinates . x = coordinates . x * zoomCoordinates . k + zoomCoordinates . x ; } } if ( coordinates . y ) { if ( reverse === false ) { fixedCoordinates . y = ( coordinates . y - zoomCoordinates . y ) / zoomCoordinates . k ; } else { fixedCoordinates . y = coordinates . y * zoomCoordinates . k + zoomCoordinates . y ; } } return fixedCoordinates ; } param { string } direction returns { boolean } private nodeSelectionTo ( direction ) { switch ( direction ) { case "up" : this . moveSelectionOnLevel ( true ) ; return true ; case "down" : this . moveSelectionOnLevel ( false ) ; return true ; case "left" : this . moveSelectionOnBranch ( true ) ; return true ; case "right" : this . moveSelectionOnBranch ( false ) ; return true ; default : return false ; } } ; param { Node } node returns { Node [ ] } public getChildren ( node ) { return this . nodes . values ( ) . filter ( ( n ) => { return n . parent && n . parent . id === node . id ; } ) ; } return { boolean } public getOrientation ( node ) { if ( ! node . isRoot ( ) ) { return node . coordinates . x < this . getRoot ( ) . coordinates . x ; } } returns { Node } public getRoot ( ) { return this . nodes . get ( this . map . id + "_node_0" ) ; } returns { Node [ ] } public getDescendants ( node ) { let nodes = [ ] ; this . getChildren ( node ) . forEach ( ( node ) => { nodes . push ( node ) ; nodes = nodes . concat ( this . getDescendants ( node ) ) ; } ) ; return nodes ; } returns { Node [ ] } public getNodes ( ) { return this . nodes . values ( ) ; } param { string } id returns { Node } public getNode ( id ) { return this . nodes . get ( id ) ; } param { string } key param { Node } node public setNode ( key , node ) { this . nodes . set ( key , node ) ; } returns { number } public getCounter ( ) { return this . counter ; } param { number } number public setCounter ( number ) { this . counter = number ; } returns { Node } public getSelectedNode ( ) { return this . selectedNode ; } /**
     * Set the root node as selected node.
     */ public selectRootNode ( ) { this . selectedNode = this . nodes . get ( this . map . id + "_node_0" ) ; } /**
     * Delete all nodes.
     */ public clear ( ) { this . nodes . clear ( ) ; } param { Node } node returns { Array < Node > } private getSiblings ( node ) { if ( ! node . isRoot ( ) ) { let parentChildren = this . getChildren ( node . parent ) ; if ( parentChildren . length > 1 ) { parentChildren . splice ( parentChildren . indexOf ( node ) , 1 ) ; return parentChildren ; } else { return [ ] ; } } else { return [ ] ; } } param { Node } node returns { Coordinates } private calculateCoordinates ( node ) { let coordinates = { x : node . parent . coordinates . x , y : node . parent . coordinates . y } , siblings = this . getSiblings ( node ) ; if ( node . parent . isRoot ( ) ) { let rightNodes = [ ] , leftNodes = [ ] ; for ( let sibling of siblings ) { this . getOrientation ( sibling ) ? leftNodes . push ( sibling ) : rightNodes . push ( sibling ) ; } if ( leftNodes . length <= rightNodes . length ) { coordinates . x -= 200 ; siblings = leftNodes ; } else { coordinates . x += 200 ; siblings = rightNodes ; } } else { if ( this . getOrientation ( node . parent ) ) { coordinates . x -= 200 ; } else { coordinates . x += 200 ; } } if ( siblings . length > 0 ) { let lowerNode = this . getLowerNode ( siblings ) ; coordinates . y = lowerNode . coordinates . y + 60 ; } else { coordinates . y -= 120 ; } return coordinates ; } param { Node [ ] } nodes returns { Node } private getLowerNode ( nodes = this . nodes . values ( ) ) { if ( nodes . length > 0 ) { let tmp = nodes [ 0 ] . coordinates . y , lowerNode = nodes [ 0 ] ; for ( let node of nodes ) { if ( node . coordinates . y > tmp ) { tmp = node . coordinates . y ; lowerNode = node ; } } return lowerNode ; } } param { Node } node param { string } name param { boolean } graphic returns { boolean } private updateNodeName = ( node , name , graphic = false ) => { if ( name && typeof name !== "string" ) { Log . error ( "The name must be a string" , "type" ) ; } if ( node . name != name || graphic ) { node . getNameDOM ( ) . innerHTML = name ; this . map . draw . updateNodeShapes ( node ) ; if ( graphic === false ) { node . name = name ; } } else { return false ; } } ; param { Node } node param { Coordinates } coordinates returns { boolean } private updateNodeCoordinates = ( node , coordinates ) => { let fixedCoordinates = this . fixCoordinates ( coordinates ) ; coordinates = Utils . mergeObjects ( node . coordinates , fixedCoordinates , true ) as Coordinates ; if ( ! ( coordinates . x === node . coordinates . x && coordinates . y === node . coordinates . y ) ) { let oldOrientation = this . getOrientation ( this . selectedNode ) , dx = node . coordinates . x - coordinates . x , dy = node . coordinates . y - coordinates . y ; node . coordinates = Utils . cloneObject ( coordinates ) as Coordinates ; node . dom . setAttribute ( "transform" , "translate(" + [ coordinates . x , coordinates . y ] + ")" ) ; if ( this . selectedNode . locked ) { let root = this . selectedNode , descendants = this . getDescendants ( this . selectedNode ) , newOrientation = this . getOrientation ( this . selectedNode ) ; for ( let node of descendants ) { let x = node . coordinates . x -= dx , y = node . coordinates . y -= dy ; if ( oldOrientation !== newOrientation ) { x = node . coordinates . x += ( root . coordinates . x - node . coordinates . x ) * 2 ; } node . dom . setAttribute ( "transform" , "translate(" + [ x , y ] + ")" ) ; } } d3 . selectAll ( "." + this . map . id + "_branch" ) . attr ( "d" , ( node ) => { return < any > this . map . draw . drawBranch ( node ) ; } ) ; } else { return false ; } } ; param { Node } node param { string } color param { boolean } graphic returns { boolean } private updateNodeBackgroundColor = ( node , color , graphic = false ) => { if ( color && typeof color !== "string" ) { Log . error ( "The background color must be a string" , "type" ) ; } if ( node . colors . background !== color || graphic ) { let background = node . getBackgroundDOM ( ) ; background . style [ "fill" ] = color ; if ( background . style [ "stroke" ] !== "" ) { background . style [ "stroke" ] = d3 . color ( color ) . darker ( .5 ) . toString ( ) ; } if ( graphic === false ) { node . colors . background = color ; } } else { return false ; } } ; param { Node } node param { string } color param { boolean } graphic returns { boolean } private updateNodeNameColor = ( node , color , graphic = false ) => { if ( color && typeof color !== "string" ) { Log . error ( "The text color must be a string" , "type" ) ; } if ( node . colors . name !== color || graphic ) { node . getNameDOM ( ) . style [ "color" ] = color ; if ( graphic === false ) { node . colors . name = color ; } } else { return false ; } } ; param { Node } node param { string } color param { boolean } graphic returns { boolean } private updateNodeBranchColor = ( node , color , graphic = false ) => { if ( color && typeof color !== "string" ) { Log . error ( "The branch color must be a string" , "type" ) ; } if ( ! node . isRoot ( ) ) { if ( node . colors . name !== color || graphic ) { let branch = document . getElementById ( node . id + "_branch" ) ; branch . style [ "fill" ] = branch . style [ "stroke" ] = color ; if ( graphic === false ) { node . colors . branch = color ; } } else { return false ; } } else { Log . error ( "The root node has no branches" ) ; } } ; param { Node } node param { number } size param { boolean } graphic returns { boolean } private updateNodeFontSize = ( node , size , graphic = false ) => { if ( size && typeof size !== "number" ) { Log . error ( "The font size must be a number" , "type" ) ; } if ( node . font . size != size || graphic ) { node . getNameDOM ( ) . style [ "font-size" ] = size + "px" ; this . map . draw . updateNodeShapes ( node ) ; if ( graphic === false ) { node . font . size = size ; } } else { return false ; } } ; param { Node } node param { number } size param { boolean } graphic returns { boolean } private updateNodeImageSize = ( node , size , graphic = false ) => { if ( size && typeof size !== "number" ) { Log . error ( "The image size must be a number" , "type" ) ; } if ( node . image . src !== "" ) { if ( node . image . size !== size || graphic ) { let image = node . getImageDOM ( ) , box = ( < any > image ) . getBBox ( ) , height = size , width = box . width * height / box . height , y = - ( height + node . dimensions . height / 2 + 5 ) , x = - width / 2 ; image . setAttribute ( "height" , height . toString ( ) ) ; image . setAttribute ( "width" , width . toString ( ) ) ; image . setAttribute ( "y" , y . toString ( ) ) ; image . setAttribute ( "x" , x . toString ( ) ) ; if ( graphic === false ) { node . image . size = height ; } } else { return false ; } } else Log . error ( "The node does not have an image" ) ; } ; param { Node } node param { string } src returns { boolean } private updateNodeImageSrc = ( node , src ) => { if ( src && typeof src !== "string" ) { Log . error ( "The image path must be a string" , "type" ) ; } if ( node . image . src !== src ) { node . image . src = src ; this . map . draw . setImage ( node ) ; } else { return false ; } } ; param { Node } node param { string } style param { boolean } graphic returns { boolean } private updateNodeFontStyle = ( node , style , graphic = false ) => { if ( style && typeof style !== "string" ) { Log . error ( "The font style must be a string" , "type" ) ; } if ( node . font . style !== style ) { node . getNameDOM ( ) . style [ "font-style" ] = style ; if ( graphic === false ) { node . font . style = style ; } } else { return false ; } } ; param { Node } node param { string } weight param { boolean } graphic returns { boolean } private updateNodeFontWeight = ( node , weight , graphic = false ) => { if ( weight && typeof weight !== "string" ) { Log . error ( "The font weight must be a string" , "type" ) ; } if ( node . font . weight !== weight ) { node . getNameDOM ( ) . style [ "font-weight" ] = weight ; this . map . draw . updateNodeShapes ( node ) ; if ( graphic === false ) { node . font . weight = weight ; } } else { return false ; } } ; param { Node } node param { string } decoration param { boolean } graphic returns { boolean } private updateNodeTextDecoration = ( node , decoration , graphic = false ) => { if ( decoration && typeof decoration !== "string" ) { Log . error ( "The text decoration must be a string" , "type" ) ; } if ( node . font . decoration !== decoration ) { node . getNameDOM ( ) . style [ "text-decoration" ] = decoration ; this . map . draw . updateNodeShapes ( node ) ; if ( graphic === false ) { node . font . decoration = decoration ; } } else { return false ; } } ; param { Node } node param { boolean } flag returns { boolean } private updateNodeLockedStatus = ( node , flag ) => { if ( flag && typeof flag !== "boolean" ) { Log . error ( "The node locked status must be a boolean" , "type" ) ; } if ( ! node . isRoot ( ) ) { node . locked = flag || ! node . locked ; } else { Log . error ( "The root node can not be locked" ) ; } } ; param { boolean } direction private moveSelectionOnLevel ( direction ) { if ( ! this . selectedNode . isRoot ( ) ) { let siblings = this . getSiblings ( this . selectedNode ) . filter ( ( node ) => { return direction === node . coordinates . y < this . selectedNode . coordinates . y ; } ) ; if ( this . selectedNode . parent . isRoot ( ) ) { siblings = siblings . filter ( ( node ) => { return this . getOrientation ( node ) === this . getOrientation ( this . selectedNode ) ; } ) ; } if ( siblings . length > 0 ) { let closerNode = siblings [ 0 ] , tmp = Math . abs ( siblings [ 0 ] . coordinates . y - this . selectedNode . coordinates . y ) ; for ( let node of siblings ) { let distance = Math . abs ( node . coordinates . y - this . selectedNode . coordinates . y ) ; if ( distance < tmp ) { tmp = distance ; closerNode = node ; } } this . selectNode ( closerNode . id ) ; } } } param { boolean } direction private moveSelectionOnBranch ( direction ) { if ( ( this . getOrientation ( this . selectedNode ) === false && direction ) || ( this . getOrientation ( this . selectedNode ) === true && ! direction ) ) { this . selectNode ( this . selectedNode . parent . id ) ; } else { let children = this . getChildren ( this . selectedNode ) ; if ( this . getOrientation ( this . selectedNode ) === undefined ) { children = children . filter ( ( node ) => { return this . getOrientation ( node ) === direction ; } ) ; } let lowerNode = this . getLowerNode ( children ) ; if ( children . length > 0 ) { this . selectNode ( lowerNode . id ) ; } } } }