import ManagedData from "../manageddata"; import { Link } from "./link"; import { NodeType } from "./nodetype"; import { Node } from "./node"; import { GLOBAL_PARAMS } from "../helper/serializableitem"; import { GraphElement } from "./graphelement"; const GRAPH_PARAMS = [...GLOBAL_PARAMS]; const GRAPH_DATA_PARAMS = ["nodes", "links", "types"]; export type GraphData = { nodes: Node[]; links: Link[]; types: NodeType[] }; export class Graph extends ManagedData { public data: GraphData; private nextNodeId = 0; private nextLinkId = 0; private nextTypeId = 0; // Callbacks public onChangeCallbacks: { (data: GraphData): void }[]; constructor(data: GraphData) { super(data); this.onChangeCallbacks = []; this.connectElementsToGraph(this.data); this.prepareIds(data); this.storeCurrentData("Initial state", false); } /** * Sets the correct graph object for all the graph elements in data. * @param data Datastructure to connect. */ connectElementsToGraph(data: GraphData) { data.nodes.forEach((n) => (n.graph = this)); data.links.forEach((l) => { l.graph = this; l.source = data.nodes.find((node) => node.id === l.sourceId); l.target = data.nodes.find((node) => node.id === l.targetId); }); data.types.forEach((t) => (t.graph = this)); } /** * Intuitive getter for links. * @returns All links associated with the graph. */ public get links(): Link[] { return this.data.links; } /** * Intuitive getter for nodes. * @returns All nodes associated with the graph.number */ public get nodes(): Node[] { return this.data.nodes; } /** * Intuitive getter for node types. * @returns All node types associated with the graph. */ public get types(): NodeType[] { return this.data.types; } /** * Determines the highest, used ids for GraphElements in data for later use. * @param data Data to analyse. */ private prepareIds(data: GraphData) { if (data.links.length > 0) { this.nextLinkId = this.getHighestId(data.links) + 1; } if (data.nodes.length > 0) { this.nextNodeId = this.getHighestId(data.nodes) + 1; } if (data.types.length > 0) { this.nextTypeId = this.getHighestId(data.types) + 1; } } /** * Finds the highest id from a list of graph elements. * @param elements List of elements containing element with highest id. * @returns Highest id in list. */ private getHighestId(elements: GraphElement[]): number { let highest = 0; elements.forEach((element) => { if (highest < element.id) { highest = element.id; } }); return highest; } /** * Calls all registered callbacks for the onChange event. * @private */ private triggerOnChange() { this.onChangeCallbacks.forEach((fn) => fn(this.data)); } /** * Triggers change event on data-redo. */ protected onRedo() { this.triggerOnChange(); } /** * Triggers change event on data-undo. */ protected onUndo() { this.triggerOnChange(); } protected storableData(data: GraphData): any { const clean: GraphData = { nodes: [], links: [], types: [], }; clean.links = data.links.map((link) => link.getCleanInstance()); clean.nodes = data.nodes.map((node) => node.getCleanInstance()); clean.types = data.types.map((type) => type.getCleanInstance()); return clean; } protected restoreData(data: GraphData): any { const parsedData = Graph.parseData(data); this.connectElementsToGraph(parsedData); // TODO: Remove. Just testing for missing nodes parsedData.links.forEach((link) => { const sourceNode = link.source; const targetNode = link.target; if (sourceNode === undefined || targetNode === undefined) { console.log(link); } }); return parsedData; } serialize(): any { return this.serializeData(this.data); } /** * Takes a data object and serializes it. * @param data GraphData object to serialize. * @returns Serialized data. */ private serializeData(data: GraphData): any { return { ...this.serializeProperties(GRAPH_PARAMS), ...this.serializeProperties(GRAPH_DATA_PARAMS, data), }; } /** * Adds a pre-created node to the graph. * @param node New node object. * @returns True, if successful. */ public addNode(node: Node) { if (this.data.nodes.includes(node)) { return true; // Already exists in graph. } // Update id node.id = this.nextNodeId; this.nextNodeId += 1; this.data.nodes.push(node); this.triggerOnChange(); // TODO: Use toString implementation of node this.storeCurrentData("Added node [" + node + "]"); return true; } /** * Deletes a node from the graph. * @param node Node object to remove. * @returns True, if successful. */ public deleteNode(node: Node): boolean { if (!this.data.nodes.includes(node)) { return true; // Doesn't even exist in graph to begin with. } this.data.nodes = this.data.nodes.filter((n: Node) => n.id !== node.id); try { // No save points should be created when deleting the links this.disableStoring(); // Delete all the links that contain this node node.links.forEach((l) => { l.delete(); }); } finally { this.enableStoring(); } this.triggerOnChange(); // TODO: Use toString implementation of node this.storeCurrentData( "Deleted node [" + node + "] and all connected links" ); return true; } getNode(id: number): Node { return this.nodes.find((n) => n.id === id); } /** * Adds a pre-created link to the graph. * @param link New link object. * @returns True, if successful. */ public addLink(link: Link): boolean { if (this.data.links.includes(link)) { return true; // Already exists in graph. } // Update id link.id = this.nextLinkId; this.nextLinkId += 1; this.data.links.push(link); this.triggerOnChange(); // TODO: Use toString implementation of link this.storeCurrentData("Added link [" + link + "]"); return true; } /** * Deletes a link from the graph. * @param link Link object to remove. * @returns True, if successful. */ public deleteLink(link: Link): boolean { if (!this.data.links.includes(link)) { return true; // Doesn't even exist in graph to begin with. } // TODO: Filter links by link id this.data.links = this.data.links.filter( (l: Link) => l.sourceId !== link.sourceId || l.targetId !== link.targetId ); this.triggerOnChange(); // TODO: Use toString implementation of link this.storeCurrentData("Deleted link [" + link + "]"); return true; } public static parse(raw: any): Graph { return new Graph(this.parseData(raw)); } public static parseData(raw: any): GraphData { const data: GraphData = { nodes: [], links: [], types: [], }; // Parse nodes if (raw.nodes === undefined) { throw new Error( "Invalid graph data format. Could not find any nodes." ); } raw.nodes.forEach((rawNode: any) => { data.nodes.push(Node.parse(rawNode)); }); // Parse links if (raw.links === undefined) { throw new Error( "Invalid graph data format. Could not find any links." ); } raw.links.forEach((rawLink: any) => { data.links.push(Link.parse(rawLink)); // No need to replace node ids with proper node objects, since that should be done in the graph itself. Only have to prepare valid GraphData }); // Collect all node types // TODO: Remove, when types are directly parsed and not just implicit data.nodes.forEach((node) => { const sharedType: NodeType = data.types.find( // TODO: Use id instead, but not defined at the moment (type) => type.name === node.type.name ); if (sharedType !== undefined) { node.type = sharedType; // Assign it the stored type, to make sure that it has the same reference as every other node to this type return; } // Doesn't exist in list yet, so add data.types.push(node.type); }); return data; } }