uu-cubitt/graph

View on GitHub
src/Graph.ts

Summary

Maintainability
F
4 days
Test Coverage
import * as Common from "cubitt-common";
import * as Collections from "typescript-collections";
import {AbstractElement} from "./AbstractElement";
import {NodeElement} from "./NodeElement";
import {EdgeElement} from "./EdgeElement";
import {ModelElement} from "./ModelElement";
import {ConnectorElement} from "./ConnectorElement";
import {GraphInterface} from "./GraphInterface";
import {ElementType} from "./ElementType";

/**
 * Graph containing nodes, connectors, edges and models
 *
 * NOTICE: this class is used internally in the Project class, always use the Project class
 */
export class Graph implements GraphInterface {
    private elements: Common.Dictionary<AbstractElement>;

    constructor() {
        this.elements = {};
    }

    /**
     * Returns an element given an GUID
     *
     * @param id GUID representing an element identifier
     */
    public getElement(id: Common.Guid): AbstractElement {
        let elem = this.elements[id.toString()];
        /* istanbul ignore if */
        if (elem === undefined) {
            /* istanbul ignore next */
            throw new Error("Element with GUID " + id.toString() + " not found");
        }
        return elem;
    }

    /**
     * @inheritdoc
     */
    public hasElement(id: Common.Guid): boolean {
        return this.elements[id.toString()] !== undefined;
    }

    /**
     * @inheritdoc
     */
    public hasModel(id: Common.Guid): boolean {
        let elem = this.elements[id.toString()];
        return elem !== undefined && elem.getType() === ElementType.Model;
    }

    /**
     * @inheritdoc
     */
    public hasNode(id: Common.Guid): boolean {
        let elem = this.elements[id.toString()];
        return elem !== undefined && elem.getType() === ElementType.Node;
    }

    /**
     * @inheritdoc
     */
    public hasConnector(id: Common.Guid): boolean {
        let elem = this.elements[id.toString()];
        return elem !== undefined && elem.getType() === ElementType.Connector;
    }

    /**
     * @inheritdoc
     */
    public hasEdge(id: Common.Guid): boolean {
        let elem = this.elements[id.toString()];
        return elem !== undefined && elem.getType() === ElementType.Edge;
    }

    /**
     * Removes an element from the graph, WARNING: does not perform a cascading delete (i.e. no removal of orphan edges)
     *
     * @param id Identifier of the element to remove
     * @param ofType Only delete the element if it is of the matching Type, if undefined, this check will be skipped
     */
    public deleteElement(id: Common.Guid, ofType?: ElementType): void {
        if (ofType === undefined) {
            delete this.elements[id.toString()];
        } else {
            let elem = this.elements[id.toString()];
            if (elem !== undefined) {
                // It's fine if the elem did not exist (the endresult is the same)
                /* istanbul ignore if */
                if (elem.getType() !== ofType) {
                    throw new Error("Attempted to delete a " + elem.getType() + " with delete" + ofType.toString());
                }
                elem.delete(this);
            }
        }
    }

    /**
     * @inheritdoc
     */
    public addNode(id: Common.Guid, type: string, modelId: Common.Guid, properties: Common.Dictionary<any>): void {
        /* istanbul ignore if */
        if (this.hasElement(id)) {
            throw new Error("An Element with GUID " + id.toString() + " already exists");
        }
        let model = this.elements[modelId.toString()];
        /* istanbul ignore if */
        if (model === undefined) {
            throw new Error("No model with GUID " + modelId + " could be found");
        }
        /* istanbul ignore if */
        if (model.getType() !== ElementType.Model) {
            throw new Error("GUID " + modelId.toString() + " does not belong to a model");
        }
        /* istanbul ignore if */
        if (properties === null || properties === undefined) {
            properties = {};
        }
        properties["type"] = type;
        let node = new NodeElement(id, properties);
        node.addParentModelNeighbour(modelId);
        model.addChildNodeNeighbour(id);

        this.elements[node.Id.toString()] = node;
    }

    /**
     * @inheritdoc
     */
    public addEdge(id: Common.Guid, type: string, modelId: Common.Guid, startConnectorId: Common.Guid, endConnectorId: Common.Guid, properties: Common.Dictionary<any> = {}) {
        // Validate GUID
        /* istanbul ignore if */
        if (this.hasElement(id)) {
            throw new Error("An Element with GUID " + id.toString() + " already exists");
        }

        // Validate modelID
        let model = this.elements[modelId.toString()];
        /* istanbul ignore if */
        if (model === undefined) {
            throw new Error("No model with GUID " + modelId + " could be found");
        }
        /* istanbul ignore if */
        if (model.getType() !== ElementType.Model) {
            throw new Error("Element with GUID " + modelId.toString() + " is not a Model");
        }

        // Validate startConnector
        let startConnector = this.elements[startConnectorId.toString()];
        /* istanbul ignore if */
        if (startConnector === undefined) {
            throw new Error("No startConnector with GUID " + startConnectorId + " could be found");
        }
        /* istanbul ignore if */
        if (startConnector.getType() !== ElementType.Connector) {
            throw new Error("Invalid startConnectorId, " + startConnectorId + " does not belong to a connector");
        }

        // Validate endConnectorId
        let endConnector = this.elements[endConnectorId.toString()];
        /* istanbul ignore if */
        if (endConnector === undefined) {
            throw new Error("No endConnector with GUID " + endConnectorId + " could be found");
        }
        /* istanbul ignore if */
        if (endConnector.getType() !== ElementType.Connector) {
            throw new Error("Invalid endConnectorId, " + endConnectorId + " does not belong to a connector");
        }
        /* istanbul ignore if */
        if (properties === null || properties === undefined) {
            properties = {};
        }
        properties["type"] = type;
        let edge = new EdgeElement(id, properties);

        edge.addStartConnector(startConnectorId);
        edge.addEndConnector(endConnectorId);

        startConnector.addChildEdgeNeighbour(id);
        endConnector.addChildEdgeNeighbour(id);

        model.addChildEdgeNeighbour(id);
        edge.addParentModelNeighbour(modelId);

        this.elements[id.toString()] = edge;
    }

    /**
     * @inheritdoc
     */
    public addConnector(id: Common.Guid, type: string, nodeId: Common.Guid, properties: Common.Dictionary<any>) {
        // Validate GUID
        /* istanbul ignore if */
        if (this.hasElement(id)) {
            throw new Error("An Element with GUID " + id.toString() + " already exists");
        }

        // Validate nodeId exists
        let node = this.elements[nodeId.toString()];
        /* istanbul ignore if */
        if (node === undefined) {
            throw new Error("No node with GUID " + nodeId + " could be found");
        }
        /* istanbul ignore if */
        if (node.getType() !== ElementType.Node) {
            throw new Error("Invalid nodeId, " + nodeId + " does not belong to a Node");
        }
        /* istanbul ignore if */
        if (properties === null || properties === undefined) {
            properties = {};
        }
        properties["type"] = type;
        let connector = new ConnectorElement(id, properties);
        node.addChildConnectorNeighbour(id);
        connector.addParentNodeNeighbour(nodeId);

        this.elements[id.toString()] = connector;
    }

    /**
     * @inheritdoc
     */
    public addModel(id: Common.Guid, type: string, properties: Common.Dictionary<any>, parentId?: Common.Guid) {
        // Validate GUID
        /* istanbul ignore if */
        if (this.hasElement(id)) {
            throw new Error("An Element with GUID " + id.toString() + " already exists");
        }
        // If parentId is set validate it
        if (parentId !== null && parentId !== undefined) {

            // Validate if there is an node or edge with the provided GUID
            /* istanbul ignore if */
            if ((this.hasNode(parentId) || this.hasEdge(parentId)) === false) {
                throw new Error("No Node or Edge with GUID " + parentId.toString() + " could be found");
            }
        }
        /* istanbul ignore if */
        if (properties === null || properties === undefined) {
            properties = {};
        }
        properties["type"] = type;
        let model = new ModelElement(id, properties);
        // Attach it to parent if available
        if (parentId !== null && parentId !== undefined) {
            let parent = this.getElement(parentId);
            parent.addChildModelNeighbour(id);
            if (this.hasEdge(parentId)) {
                model.addParentEdgeNeighbour(parentId);
            }
            if (this.hasNode(parentId)) {
                model.addParentNodeNeighbour(parentId);
            }

        }
        this.elements[id.toString()] = model;
    }

    /**
     * @inheritdoc
     */
    public setProperty(id: Common.Guid, name: string, value: any) {
        /* istanbul ignore if */
        if (this.hasElement(id) === false) {
            throw new Error("An Element with GUID " + id.toString() + " could not be found");
        }
        this.elements[id.toString()].setProperty(name, value);
    }

    /**
     * @inheritdoc
     */
    public deleteNode(id: Common.Guid) {
        this.deleteElement(id, ElementType.Node);
    }

    /**
     * @inheritdoc
     */
    public deleteEdge(id: Common.Guid) {
        this.deleteElement(id, ElementType.Edge);
    }

    /**
     * @inheritdoc
     */
    public deleteConnector(id: Common.Guid) {
        this.deleteElement(id, ElementType.Connector);
    }

    /**
     * @inheritdoc
     */
    public deleteModel(id: Common.Guid) {
        this.deleteElement(id, ElementType.Model);
    }

    /**
     * @inheritdoc
     */
    public deleteProperty(id: Common.Guid, name: string): void {
        let elem = this.elements[id.toString()];
        /* istanbul ignore if */
        if (elem === undefined) {
            throw new Error("Element not found");
        }
        elem.deleteProperty(name);
    }

    /**
     * @inheritdoc
     */
    public deserialize(jsonObject: Object): GraphInterface {
        let graph = new Graph();
        let models = jsonObject["models"];
        let queue: Collections.Queue<Object> = new Collections.Queue<Object>();
        let inQueue: Common.Dictionary<Boolean> = {};
        // Find root models
        for (let modelKey in models) {
            let model = models[modelKey];

            if (Object.keys(model.neighbours.nodes.parent).length === 0 && Object.keys(model.neighbours.edges.parent).length === 0) {
                queue.enqueue({ "type": "model", "element": model.id.toString() });
                inQueue[model.id.toString()] = true;
            }
        }
        while (queue.isEmpty() === false) {
            let obj = queue.dequeue();
            let type = obj["type"];
            let elemId = obj["element"];
            let elem = jsonObject[type + "s"][elemId.toString()];
            let properties = this.propertiesFromJSON(elem["properties"]);
            switch (type) {
                case "model":
                    if (elem["neighbours"]["nodes"]["parent"].length > 0) {
                        graph.addModel(Common.Guid.parse(elem.id), properties["type"], properties, Common.Guid.parse(elem["neighbours"]["nodes"]["parent"][0]));
                    } else if (elem["neighbours"]["edges"]["parent"].length > 0) {
                        graph.addModel(Common.Guid.parse(elem.id), properties["type"], properties, Common.Guid.parse(elem["neighbours"]["edges"]["parent"][0]));
                    } else {
                        graph.addModel(Common.Guid.parse(elem.id), properties["type"], properties);
                    }
                    break;
                case "node":
                    graph.addNode(Common.Guid.parse(elem.id), properties["type"], Common.Guid.parse(elem["neighbours"]["models"]["parent"][0]), properties);
                    // Add Connectors
                    let connectors = elem["neighbours"]["connectors"]["child"];
                    for (let connectorKey of connectors) {
                        let connector = jsonObject["connectors"][connectorKey];
                        let id = Common.Guid.parse(connector["id"]);
                        let connectorProperties = this.propertiesFromJSON(connector["properties"]);
                        graph.addConnector(id, connectorProperties["type"], Common.Guid.parse(elem.id), connectorProperties);
                        inQueue[id.toString()] = true;
                        let edgeIds = connector["neighbours"]["edges"]["child"];
                        let edges: string[] = [];
                        for (let edgeId of edgeIds) {
                            let edge = jsonObject["edges"][edgeId];
                            let isInQueue = true;
                            for (let connectorId of edge["neighbours"]["connectors"]["parent"]) {
                                if (inQueue[connectorId] !== true) {
                                    isInQueue = false;
                                }
                            }
                            if (isInQueue === true) {
                                edges.push(edgeId);
                            }
                        }
                        this.enqueueChildElement("edge", edges, queue, inQueue);
                        continue;
                    }
                    break;
                case "edge":
                    graph.addEdge(
                        Common.Guid.parse(elem.id),
                        properties["type"],
                        Common.Guid.parse(elem["neighbours"]["models"]["parent"][0]),
                        Common.Guid.parse(elem["neighbours"]["connectors"]["parent"][0]),
                        Common.Guid.parse(elem["neighbours"]["connectors"]["parent"][1]),
                        properties
                    );
                    break;
                /* istanbul ignore next */
                default:
                    throw new Error("Invalid element type");
            }
            // Enqueue child elements, we directly add the connectors when processing the node
            this.enqueueChildElement("model", elem["neighbours"]["models"]["child"], queue, inQueue);
            this.enqueueChildElement("node", elem["neighbours"]["nodes"]["child"], queue, inQueue);

        }
        return graph;
    }

    /**
     * Iterate over the child elements to add them
     *
     * @param type Type of the child elements
     * @param children the Children to iterate over
     * @param queue The queue to add elements to
     * @param inQueue register which elements are processed
     */
    private enqueueChildElement(type: string, children: string[], queue: Collections.Queue<Object>, inQueue: Common.Dictionary<Boolean>) {
        for (let childElem of children) {
            /* istanbul ignore else */
            if (inQueue[childElem] !== true) {
                queue.enqueue({ "type": type, "element": childElem });
                inQueue[childElem] = true;
            }
        }
    }

    /**
     * Creates a Property dictionary from JSON
     *
     * @param jsonProperties JSON object that contains the properties
     */
    private propertiesFromJSON(jsonProperties: Object): Common.Dictionary<any> {
        let properties: Common.Dictionary<any> = {};
        for (let propertyKey in jsonProperties) {
            properties[propertyKey] = jsonProperties[propertyKey];
        }
        return properties;
    }

    /**
     * @inheritdoc
     */
    public serialize() {
        let graph = {
            "models": {},
            "nodes": {},
            "edges": {},
            "connectors": {}
        };

        let elements = this.elements;
        for (let key in elements) {
            let elem = elements[key];
            let obj = {
                "id": elem.Id.toString(),
                "properties": elem.getProperties(),
                "neighbours": {
                    "models": {
                        "parent": elem.getParentModelNeighbours().map(g => g.toString()),
                        "child": elem.getChildModelNeighbours().map(g => g.toString())
                    },
                    "nodes": {
                        "parent": elem.getParentNodeNeighbours().map(g => g.toString()),
                        "child": elem.getChildNodeNeighbours().map(g => g.toString())
                    },
                    "edges": {
                        "parent": elem.getParentEdgeNeighbours().map(g => g.toString()),
                        "child": elem.getChildEdgeNeighbours().map(g => g.toString())
                    },
                    "connectors": {
                        "parent": elem.getParentConnectorNeighbours().map(g => g.toString()),
                        "child": elem.getChildConnectorNeighbours().map(g => g.toString())
                    }
                }
            };
            if (elem.getType() === ElementType.Node) {
                graph.nodes[elem.Id.toString()] = obj;
            } else if (elem.getType() === ElementType.Edge) {
                graph.edges[elem.Id.toString()] = obj;
            } else if (elem.getType() === ElementType.Connector) {
                graph.connectors[elem.Id.toString()] = obj;
            } else {
                graph.models[elem.Id.toString()] = obj;
            }
        }
        return graph;
    }
}