Skip to content
Snippets Groups Projects
graph.ts 4.84 KiB
Newer Older
import { Link } from "../common/graph/link";
import { NodeType } from "../common/graph/nodetype";
Matthias Konitzny's avatar
Matthias Konitzny committed
import { Node, NodeData, SimNodeData } from "../common/graph/node";
import * as Common from "../common/graph/graph";
import { History } from "../common/history";
import { GraphContent, GraphData, SimGraphData } from "../common/graph/graph";

export class DynamicGraph extends Common.Graph {
    public history: History<SimGraphData>;

    // Callbacks
    public onChangeCallbacks: { (data: DynamicGraph): void }[];

    constructor(data?: GraphContent) {
        super(data);
        this.onChangeCallbacks = [];

        if (data != undefined) {
            this.history = new History<SimGraphData>(
                this,
                20,
                "Created new graph."
            );
        }
    }

    public fromSerializedObject(data: GraphData | SimGraphData): DynamicGraph {
        super.fromSerializedObject(data);
        this.history = new History<SimGraphData>(
            this,
            20,
            "Created new graph."
        );

        return this;
    }

    /**
     * Calls all registered callbacks for the onChange event.
     * @private
     */
    private triggerOnChange() {
        this.onChangeCallbacks.forEach((fn) => fn(this));
    }

    /**
     * Triggers change event on data-redo.
     */
    protected onRedo() {
        if (this.history.hasRedoCheckpoints()) {
            const checkpoint = this.history.redo();
            this.fromSerializedObject(checkpoint.data);
            this.triggerOnChange();
        }
    }

    /**
     * Triggers change event on data-undo.
     */
    protected onUndo() {
        if (this.history.hasUndoCheckpoints()) {
            const checkpoint = this.history.undo();
            this.fromSerializedObject(checkpoint.data);
            this.triggerOnChange();
        }
    }

    public createObjectGroup(name?: string, color?: string): NodeType {
        if (name == undefined) {
            name = "Unnamed";
        }
        if (color == undefined) {
            color = "#000000";
        }
        const objectGroup = super.createObjectGroup(name, color);
        this.triggerOnChange();

        return objectGroup;
    }

Matthias Konitzny's avatar
Matthias Konitzny committed
    public createNode(
        data?: NodeData | SimNodeData,
        x?: number,
        y?: number,
        vx?: number,
        vy?: number
    ): Node {
        if (data == undefined) {
            data = {
                id: 0,
                name: "Undefined",
                type: this.objectGroups[0].name, // TODO: Change to id
Matthias Konitzny's avatar
Matthias Konitzny committed
                x: x,
                y: y,
                vx: vx,
                vy: vy,
            };
        }
        return super.createNode(data);
    }

    private delete(id: string | number, fn: (id: string | number) => boolean) {
        if (fn(id)) {
            this.triggerOnChange();
            return true;
        }
        return false;
    }

    public deleteNodeType(id: string): boolean {
        return this.delete(id, super.deleteNodeType);
    }

    public deleteNode(id: number): boolean {
        return this.delete(id, super.deleteNode);
    }

    public deleteLink(id: number): boolean {
        return this.delete(id, super.deleteNode);
    }

    getLink(
        sourceId: number,
        targetId: number,
        directionSensitive = true
    ): Link {
        return this.links.find((l) => {
            if (l.sourceId === sourceId && l.targetId === targetId) {
                return true;
            }

            // Check other direction if allowed
            return (
                !directionSensitive &&
                l.sourceId === targetId &&
                l.targetId === sourceId
            );
        });
    }

    public createLink(source: number, target: number): Link {
        const link = this.getLink(source, target, false);
        if (link !== undefined) {
            return link; // Already exists in graph.
        }

        return super.createLink(source, target);
    }

    /**
Matthias Konitzny's avatar
Matthias Konitzny committed
     * Goes over all nodes and finds the closest node based on distance.
     * @returns Closest node and distance. Undefined, if no closest node can be found.
     */
Matthias Konitzny's avatar
Matthias Konitzny committed
    public getClosestNode(
        x: number,
        y: number,
        exclude?: Node
    ): {
        node: Node;
        distance: number;
    } {
        // Iterate over all nodes, keep the one with the shortest distance
        let closestDistance = Number.MAX_VALUE;
        let closestNode: Node = undefined;
        this.nodes.forEach((node) => {
Matthias Konitzny's avatar
Matthias Konitzny committed
            if (node.equals(exclude)) {
Matthias Konitzny's avatar
Matthias Konitzny committed
            const currentDistance = Math.hypot(x - node.x, y - node.y);

            if (closestDistance > currentDistance) {
                closestDistance = currentDistance;
                closestNode = node;
            }
        });

        return { node: closestNode, distance: closestDistance };
    }
}