Skip to content
Snippets Groups Projects
graph.ts 8.72 KiB
Newer Older
import * as Config from "../../config";
import { Node, NodeData, SimNodeData } from "./node";
import { Link, LinkData, SimLinkData } from "./link";
import { NodeType, NodeTypeData } from "./nodetype";
import { SerializableItem } from "../serializableitem";

export interface Coordinate {
    x: number;
    y: number;
    z: number;
}

export interface GraphData {
    nodes: NodeData[];
    links: LinkData[];
    objectGroups?: NodeTypeData[];
}

export interface SimGraphData {
    nodes: SimNodeData[];
    links: SimLinkData[];
    objectGroups: NodeTypeData[];
}

export interface GraphContent {
    nodes: Node[];
    links: Link[];
/**
 * Basic graph data structure.
 */
export class Graph
    extends SerializableItem<GraphData, SimGraphData>
    implements GraphContent
{
    public objectGroups: NodeType[];
    public nameToObjectGroup: Map<string, NodeType>;
    public initialized: boolean;

    private idToNode: Map<number, Node>;
    private idToLink: Map<number, Link>;
    /**
     * Creates a new Graph object.
     * Make sure the nodes and links are connected to the correct objects before calling this method!
     */
    constructor(data?: GraphContent) {
        super(0);

        if (data === undefined) {
            this.initialized = false;
            return;
        }
        this.createDefaultObjectGroupIfNeeded();
        this.objectGroups.forEach((group) =>
            this.nameToObjectGroup.set(group.name, group)
        );
        this.nodes.forEach((node) => {
            this.idToNode.set(node.id, node);
        });
        this.links.forEach((link) => {
            this.idToLink.set(link.id, link);
        this.nodes = [];
        this.links = [];
        this.nameToObjectGroup = new Map<string, NodeType>();
        this.idToNode = new Map<number, Node>();
        this.idToLink = new Map<number, Link>();
    }

    /**
     * Sets the correct graph object for all the graph elements in data.
     */
    connectElementsToGraph() {
        this.nodes.forEach((n) => (n.graph = this));
        this.links.forEach((l) => {
            l.graph = this;
        });
        this.objectGroups.forEach((t) => (t.graph = this));
    }

    public toJSONSerializableObject(): GraphData {
        return {
            nodes: this.nodes.map((node) => node.toJSONSerializableObject()),
            links: this.links.map((link) => link.toJSONSerializableObject()),
            objectGroups: this.objectGroups.map((group) =>
                group.toJSONSerializableObject()
            ),
        };
    public toHistorySerializableObject(): SimGraphData {
        return {
            nodes: this.nodes.map((node) => node.toHistorySerializableObject()),
            links: this.links.map((link) => link.toHistorySerializableObject()),
            objectGroups: this.objectGroups.map((group) =>
                group.toHistorySerializableObject()
            ),
        };
    }

    public fromSerializedObject(data: GraphData | SimGraphData): Graph {

        if (data.objectGroups === undefined) {
            this.createObjectGroupsFromStrings(data.nodes);
            data.objectGroups.forEach((group) =>
                this.createObjectGroup(group.name, group.color)
        this.createDefaultObjectGroupIfNeeded();
        data.nodes.forEach((node) => this.createNode(node));
        data.links.forEach((link) => this.createLink(link.source, link.target));
    private createObjectGroupsFromStrings(nodes: NodeData[]): Array<NodeType> {
        const objectGroups: NodeType[] = [];
        const nodeClasses: string[] = [];
        nodes.forEach((node) => nodeClasses.push(node.type));
        const nodeTypes = [...new Set(nodeClasses)].map((c) => String(c));

        for (let i = 0; i < nodeTypes.length; i++) {
            this.createObjectGroup(
                nodeTypes[i],
                Config.COLOR_PALETTE[i % Config.COLOR_PALETTE.length]
            );
    private createDefaultObjectGroupIfNeeded() {
        if (this.objectGroups.length == 0) {
            this.createObjectGroup("Default", "#000000");
        }
    }

    /**
     * Updates the graph data structure to contain additional values.
     * Creates a 'neighbors' and 'links' array for each node object.
     */
    private updateNodeData(): Link[] {
        this.links.forEach((link) => {
            const a = link.source;
            const b = link.target;
            a.neighbors.push(b);
            b.neighbors.push(a);
            a.links.push(link);
            b.links.push(link);
        });
        return this.links;
    }

    public removeFloatingNodes() {
        this.nodes = this.nodes.filter((node) => node.neighbors.length > 0);
    public node(id: number): Node {
        return this.idToNode.get(id);
    }

    private addNode(node: Node) {
        node.id = this.nodes.length;
        this.nodes.push(node);
        this.idToNode.set(node.id, node);
    public createNode(data?: NodeData): Node {
        const node = new Node(this);
        node.fromSerializedObject(data);
        node.type = this.nameToObjectGroup.get(data.type);
        node.neighbors = [];
        node.links = [];
        this.addNode(node);
        return node;
    public createLink(source: number, target: number): Link {
        if (source === target) {
            console.warn(
                "Attempting to create a link where source equals target"
            );
            return;
        }
        const sourceNode = this.idToNode.get(source);
        const targetNode = this.idToNode.get(target);

        if (sourceNode === undefined || targetNode === undefined) {
            console.warn("Tried to create a link between nonexisting nodes!");
            return;
        const link = new Link(sourceNode, targetNode, this);
        sourceNode.links.push(link);
        targetNode.links.push(link);
        this.addLink(link);
        return link;
    public createObjectGroup(name?: string, color?: string): NodeType {
        const group = new NodeType(name, color, this);
        this.addObjectGroup(group);
        return group;
    }
    private addObjectGroup(group: NodeType) {
        group.id = this.objectGroups.length;
        this.objectGroups.push(group);
        this.nameToObjectGroup.set(group.name, group); // TODO: Replace with id
    }
    private addLink(link: Link) {
        link.id = this.links.length;
        this.links.push(link);
        this.idToLink.set(link.id, link);
    }

    public deleteLink(id: number): boolean {
        // Remove link from node data structures
        const link = this.idToLink.get(id);
        link.source.links.filter((l) => l.id != id);
        link.target.links.filter((l) => l.id != id);

        // Remove link from graph data structures
        this.links = this.links.filter((l: Link) => l.id != id);
    public deleteNode(id: number): boolean {
        const node = this.idToNode.get(id);
        this.idToNode.delete(id);

        for (const link of node.links) {
            this.deleteLink(link.id);
        }
        this.nodes = this.nodes.filter((n: Node) => n.id !== id);
    public deleteNodeType(id: string): boolean {
        if (this.objectGroups.length <= 1) {
            // Do not allow to delete the last node type.
            return false;
        }

        // TODO: Change to id/number
        const nodeType = this.nameToObjectGroup.get(id);

        for (const node of this.nodes) {
            if (node.type.id === nodeType.id) {
        nodeTypes: Map<string, boolean>,
        linkTypes?: Map<string, boolean>
        const nodes = this.nodes.filter((l) => nodeTypes.get(l.type.name));
        let links;
        if (linkTypes === undefined) {
            links = this.links;
        } else {
            links = this.links.filter((l) => linkTypes.get(l.type.name));
        // Filter links which are connected to an invisible node
        links = links.filter(
            (l) =>
                nodeTypes.get(l.source.type.name) &&
                nodeTypes.get(l.target.type.name)
        return new Graph({
            nodes: nodes,
            links: links,
            objectGroups: this.objectGroups,
        });