Skip to content
Snippets Groups Projects
graph.ts 8.31 KiB
Newer Older
  • Learn to ignore specific revisions
  • 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;
    
    Maximilian Giller's avatar
    Maximilian Giller committed
        public onChangeCallbacks: { (data: any): void }[];
    
        constructor(data: GraphData) {
    
            super(data);
            this.onChangeCallbacks = [];
    
    
            this.connectElementsToGraph();
    
            this.prepareIds(data);
        }
    
    
        /**
         * Sets the correct graph object for all the graph elements in data.
         */
        connectElementsToGraph() {
            this.data.nodes.forEach((n) => (n.graph = this));
            this.data.links.forEach((l) => {
                l.graph = this;
                l.source = this.getNode(l.sourceId);
                l.target = this.getNode(l.targetId);
            });
            this.data.types.forEach((t) => (t.graph = this));
        }
    
    
    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;
    
            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.
    
            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 {
            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);
            });
    
            return new Graph(data);
        }