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);
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;
// Is valid node?
if (node.label == undefined) {
node.label = "Unnamed";
}
if (node.type == undefined) {
if (this.types.length > 0) {
// Just give first type in list
node.type = this.types[0];
} else {
// Give empty type
// TODO: Properly add new type, with proper ID. Implemented this.addType(..);
node.type = new NodeType(this);
}
}
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.equals(node));
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.getElementWithId(this.nodes, id);
}
getType(id: number): NodeType {
return this.getElementWithId(this.types, id);
}
getElementWithId(elements: GraphElement[], id: number): any {
const numberId = Number(id);
if (isNaN(numberId)) {
return undefined;
}
return elements.find((e) => e.id === numberId);
/**
* 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.equals(link));
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: [],
};
// 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 and give id if none given yet
let typeId: number = undefined;
if (data.nodes.length > 0 && data.nodes[0].type.id === undefined) {
// TODO: Remove, when types are directly parsed and not just implicit
data.nodes.forEach((node) => {
const sharedType: NodeType = data.types.find((type) =>
type.equals(node.type)
);
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;
}
if (typeId !== undefined) {
node.type.id = typeId;
typeId += 1;
}
// Doesn't exist in list yet, so add
data.types.push(node.type);
});

Maximilian Giller
committed
return data;