Skip to content
Snippets Groups Projects
graph.ts 9.05 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);

        // 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.
     */
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;

        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.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
Maximilian Giller's avatar
Maximilian Giller committed
        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 {
        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.
Maximilian Giller's avatar
Maximilian Giller committed
        // 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);
        });