Skip to content
Snippets Groups Projects
graph.ts 9.65 KiB
Newer Older
import ManagedData from "../manageddata";
Maximilian Giller's avatar
Maximilian Giller committed
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;

Maximilian Giller's avatar
Maximilian Giller committed
    private nextNodeId = 0;
    private nextLinkId = 0;
    private nextTypeId = 0;
    public onChangeCallbacks: { (data: GraphData): void }[];
    constructor(data: GraphData) {
        super(data);
        this.onChangeCallbacks = [];

        this.prepareIds(data);
    }

    /**
     * Sets the correct graph object for all the graph elements in data.
    connectElementsToGraph(data: GraphData) {
        data.nodes.forEach((n) => (n.graph = this));
        data.links.forEach((l) => {
            l.source = data.nodes.find((node) => node.id === l.sourceId);
            l.target = data.nodes.find((node) => node.id === l.targetId);
Maximilian Giller's avatar
Maximilian Giller committed
    /**
     * 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 {
Maximilian Giller's avatar
Maximilian Giller committed
        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);

        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.
     */
Maximilian Giller's avatar
Maximilian Giller committed
    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)) {
Maximilian Giller's avatar
Maximilian Giller committed
            return true; // Already exists in graph.
        // 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.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)) {
Maximilian Giller's avatar
Maximilian Giller committed
            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
Maximilian Giller's avatar
Maximilian Giller committed
        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 {
        if (this.data.links.includes(link)) {
Maximilian Giller's avatar
Maximilian Giller committed
            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)) {
Maximilian Giller's avatar
Maximilian Giller committed
            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 {
        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 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);
        });