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.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); }); } private reset() { this.nodes = []; this.links = []; this.nameToObjectGroup = new Map<string, NodeType>(); this.idToNode = new Map<number, Node>(); this.idToLink = new Map<number, 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 { this.reset(); if (data.objectGroups === undefined) { this.createObjectGroupsFromStrings(data.nodes); } else { data.objectGroups.forEach((group) => this.createObjectGroup(group.name, group.color) ); } 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; } /** * 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) { 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, }); } }