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; } this.reset(); Object.assign(this, data); this.createDefaultObjectGroupIfNeeded(); this.objectGroups.forEach((group) => this.nameToObjectGroup.set(group.name, group) ); this.nodes.forEach((node) => { this.idToNode.set(node.id, node); }); this.links.forEach((link) => { this.idToLink.set(link.id, link); }); this.connectElementsToGraph(); } protected reset() { this.nodes = []; this.links = []; this.nameToObjectGroup = new Map<string, NodeType>(); this.idToNode = new Map<number, Node>(); this.idToLink = new Map<number, Link>(); } /** * Sets the correct graph object for all the graph elements in data. */ connectElementsToGraph() { this.nodes.forEach((n) => (n.graph = this)); this.links.forEach((l) => { l.graph = this; }); this.objectGroups.forEach((t) => (t.graph = this)); } 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 { this.reset(); if (data.objectGroups === undefined) { this.createObjectGroupsFromStrings(data.nodes); } else { data.objectGroups.forEach((group) => this.createObjectGroup(group.name, group.color) ); } this.createDefaultObjectGroupIfNeeded(); data.nodes.forEach((node) => this.createNode(node)); data.links.forEach((link) => this.createLink(link.source, link.target)); this.updateNodeData(); return this; } 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++) { this.createObjectGroup( nodeTypes[i], Config.COLOR_PALETTE[i % Config.COLOR_PALETTE.length] ); } return objectGroups; } private createDefaultObjectGroupIfNeeded() { if (this.objectGroups.length == 0) { this.createObjectGroup("Default", "#000000"); } } /** * 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 addNode(node: Node) { node.id = this.nodes.length; this.nodes.push(node); this.idToNode.set(node.id, node); } public createNode(data?: NodeData): Node { const node = new Node(this); node.fromSerializedObject(data); node.type = this.nameToObjectGroup.get(data.type); node.neighbors = []; node.links = []; this.addNode(node); return node; } public createLink(source: number, target: number): Link { if (source === target) { console.warn( "Attempting to create a link where source equals target" ); return; } const sourceNode = this.idToNode.get(source); const targetNode = this.idToNode.get(target); if (sourceNode === undefined || targetNode === undefined) { console.warn("Tried to create a link between nonexisting nodes!"); return; } const link = new Link(sourceNode, targetNode, this); sourceNode.links.push(link); targetNode.links.push(link); this.addLink(link); return link; } public createObjectGroup(name?: string, color?: string): NodeType { const group = new NodeType(name, color, this); this.addObjectGroup(group); return group; } private addObjectGroup(group: NodeType) { group.id = this.objectGroups.length; this.objectGroups.push(group); this.nameToObjectGroup.set(group.name, group); // TODO: Replace with id } private addLink(link: Link) { link.id = this.links.length; this.links.push(link); this.idToLink.set(link.id, link); } public deleteLink(id: number): boolean { // Remove link from node data structures const link = this.idToLink.get(id); link.source.links.filter((l) => l.id != id); link.target.links.filter((l) => l.id != id); // Remove link from graph data structures this.links = this.links.filter((l: Link) => l.id != id); this.idToLink.delete(id); return true; } public deleteNode(id: number): boolean { 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); return true; } public deleteNodeType(id: string): boolean { if (this.objectGroups.length <= 1) { // Do not allow to delete the last node type. return false; } // 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 = this.objectGroups[0]; } } return true; } 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, }); } }