import * as Config from "../config"; import { Node, NodeData, SimNodeData } from "./node"; import { Link, LinkData, SimLinkData } from "./link"; import { NodeType, NodeTypeData } from "./nodetype"; import { SerializableItem } from "./serializableitem"; export interface Coordinate { x: number; y: number; z: number; } export interface GraphData { nodes: NodeData[]; links: LinkData[]; objectGroups?: NodeTypeData[]; } export interface SimGraphData { nodes: SimNodeData[]; links: SimLinkData[]; objectGroups: NodeTypeData[]; } export interface GraphContent { nodes: Node[]; links: Link[]; objectGroups?: NodeType[]; } /** * Basic graph data structure. */ export class Graph extends SerializableItem<GraphData, SimGraphData> implements GraphContent { public nodes: Node[]; public links: Link[]; public objectGroups: NodeType[]; public nameToObjectGroup: Map<string, NodeType>; public initialized: boolean; private idToNode: Map<number, Node>; private idToLink: Map<number, Link>; /** * Creates a new Graph object. * Make sure the nodes and links are connected to the correct objects before calling this method! */ constructor(data?: GraphContent) { super(0); if (data === undefined) { this.initialized = false; return; } Object.assign(this, data); this.nameToObjectGroup = new Map<string, NodeType>(); this.objectGroups.forEach((group) => this.nameToObjectGroup.set(group.name, group) ); this.idToNode = new Map<number, Node>(); this.nodes.forEach((node) => { this.idToNode.set(node.id, node); }); this.idToLink = new Map<number, Link>(); this.links.forEach((link) => { this.idToLink.set(link.id, link); }); } public toJSONSerializableObject(): GraphData { return { nodes: this.nodes.map((node) => node.toJSONSerializableObject()), links: this.links.map((link) => link.toJSONSerializableObject()), objectGroups: this.objectGroups.map((group) => group.toJSONSerializableObject() ), }; } public toHistorySerializableObject(): SimGraphData { return { nodes: this.nodes.map((node) => node.toHistorySerializableObject()), links: this.links.map((link) => link.toHistorySerializableObject()), objectGroups: this.objectGroups.map((group) => group.toHistorySerializableObject() ), }; } public fromSerializedObject(data: GraphData | SimGraphData): Graph { let objectGroups: Array<NodeType>; if (data.objectGroups === undefined) { objectGroups = this.createObjectGroupsFromStrings(data.nodes); } else { objectGroups = this.createObjectGroupsFromObjects( data.objectGroups ); } this.nameToObjectGroup = new Map<string, NodeType>(); objectGroups.forEach((group) => this.nameToObjectGroup.set(group.name, group) ); this.nodes = this.createNodes(data.nodes); this.idToNode = new Map<number, Node>(); this.nodes.forEach((node) => { this.idToNode.set(node.id, node); }); this.links = data.links.map((link, i) => { const l = new Link(); l.id = i; l.source = this.idToNode.get(link.source); l.target = this.idToNode.get(link.target); return l; }); this.idToLink = new Map<number, Link>(); this.links.forEach((link) => { this.idToLink.set(link.id, link); }); this.updateNodeData(); return this; } private createNodes(nodeJSONData: NodeData[]): Array<Node> { const nodes: Array<Node> = []; for (const nodeData of nodeJSONData) { const node = new Node(); node.fromSerializedObject(nodeData); node.type = this.nameToObjectGroup.get(nodeData.type); node.neighbors = []; node.links = []; nodes.push(node); } return nodes; } private createObjectGroupsFromStrings(nodes: NodeData[]): Array<NodeType> { const objectGroups: NodeType[] = []; const nodeClasses: string[] = []; nodes.forEach((node) => nodeClasses.push(node.type)); const nodeTypes = [...new Set(nodeClasses)].map((c) => String(c)); for (let i = 0; i < nodeTypes.length; i++) { const nodeType = new NodeType(); nodeType.fromSerializedObject({ id: i, name: nodeTypes[i], color: Config.COLOR_PALETTE[i % Config.COLOR_PALETTE.length], }); } return objectGroups; } private createObjectGroupsFromObjects( groups: NodeTypeData[] ): Array<NodeType> { return groups.map((group) => { const t = new NodeType(); t.fromSerializedObject(group); return t; }); } /** * Updates the graph data structure to contain additional values. * Creates a 'neighbors' and 'links' array for each node object. */ private updateNodeData(): Link[] { this.links.forEach((link) => { const a = link.source; const b = link.target; a.neighbors.push(b); b.neighbors.push(a); a.links.push(link); b.links.push(link); }); return this.links; } public removeFloatingNodes() { this.nodes = this.nodes.filter((node) => node.neighbors.length > 0); } public node(id: number): Node { return this.idToNode.get(id); } private checkNode(node: Node) { for (const neighbor of node.neighbors) { if (this.idToNode.get(neighbor.id) === undefined) { return false; } } for (const link of node.links) { if (this.idToLink.get(link.id) === undefined) { return false; } } return node.isInitialized(); } private checkLink(link: Link) { return ( link.isInitialized() && !( this.idToNode.get(link.source.id) === undefined || this.idToNode.get(link.target.id) === undefined ) ); } public addNode(node: Node) { node.id = this.nodes.length; if (!this.checkNode(node)) { return false; } this.nodes.push(node); this.idToNode.set(node.id, node); return true; } public addLink(link: Link) { link.id = this.links.length; if (!this.checkLink(link)) { return false; } this.links.push(link); this.idToLink.set(link.id, link); return true; } public deleteLink(id: number) { this.links = this.links.filter((l: Link) => l.id !== id); this.idToLink.delete(id); } public deleteNode(id: number) { const node = this.idToNode.get(id); this.idToNode.delete(id); for (const link of node.links) { this.deleteLink(link.id); } this.nodes = this.nodes.filter((n: Node) => n.id !== id); } public deleteNodeType(id: string) { // TODO: Change to id/number const nodeType = this.nameToObjectGroup.get(id); for (const node of this.nodes) { if (node.type.id === nodeType.id) { node.type = undefined; } } } public view( nodeTypes: Map<string, boolean>, linkTypes?: Map<string, boolean> ): Graph { // Filter nodes depending on type const nodes = this.nodes.filter((l) => nodeTypes.get(l.type.name)); // Filter links depending on type let links; if (linkTypes === undefined) { links = this.links; } else { links = this.links.filter((l) => linkTypes.get(l.type.name)); } // Filter links which are connected to an invisible node links = links.filter( (l) => nodeTypes.get(l.source.type.name) && nodeTypes.get(l.target.type.name) ); return new Graph({ nodes: nodes, links: links, objectGroups: this.objectGroups, }); } }