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;
}
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
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
/**
* Calculates the pythagoras distance.
* @param nodeA One node.
* @param nodeB The other node.
* @returns Distance between both nodes.
*/
private nodeDistance(nodeA: Node, nodeB: Node): number {
const a = nodeA as any;
const b = nodeB as any;
const xDistance = Math.abs(a.x - b.x);
const yDistance = Math.abs(a.y - b.y);
return Math.sqrt(Math.pow(xDistance, 2) + Math.pow(yDistance, 2));
}
/**
* Goes over all nodes and finds the closest node based on distance, that is not the given reference node.
* @param referenceNode Reference node to get closest other node to.
* @returns Closest node and distance. Undefined, if no closest node can be found.
*/
public getClosestOtherNode(referenceNode: Node): {
node: Node;
distance: number;
} {
if (referenceNode == undefined || this.nodes.length < 2) {
return undefined;
}
// Iterate over all nodes, keep the one with the shortest distance
let closestDistance: number = undefined;
let closestNode: Node = undefined;
this.nodes.forEach((node) => {
if (node.equals(referenceNode)) {
return; // Don't compare to itself
}
const currentDistance = this.nodeDistance(node, referenceNode);
if (
closestDistance == undefined ||
closestDistance > currentDistance
) {
closestDistance = currentDistance;
closestNode = node;
}
});
return { node: closestNode, distance: closestDistance };
}
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;