Newer
Older
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 {
private nextNodeId = 0;
private nextLinkId = 0;
private nextTypeId = 0;

Maximilian Giller
committed
public onChangeCallbacks: { (data: GraphData): void }[];
super(data);
this.onChangeCallbacks = [];

Maximilian Giller
committed
this.connectElementsToGraph(this.data);
/**
* Sets the correct graph object for all the graph elements in data.

Maximilian Giller
committed
* @param data Datastructure to connect.

Maximilian Giller
committed
connectElementsToGraph(data: GraphData) {
data.nodes.forEach((n) => (n.graph = this));
data.links.forEach((l) => {

Maximilian Giller
committed
l.source = data.nodes.find((node) => node.id === l.sourceId);
l.target = data.nodes.find((node) => node.id === l.targetId);

Maximilian Giller
committed
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 {
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: [],
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;

Maximilian Giller
committed
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;
}
return this.serializeData(this.data);
}
/**
* Takes a data object and serializes it.
* @param data GraphData object to serialize.
* @returns Serialized data.
*/
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) {
// 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 {
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"
);
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 {
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 {
return true; // Doesn't even exist in graph to begin with.
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 {

Maximilian Giller
committed
return new Graph(this.parseData(raw));
}
public static parseData(raw: any): GraphData {
const data: GraphData = {
nodes: [],
links: [],
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
};
// 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);
});

Maximilian Giller
committed
return data;