Skip to content
Snippets Groups Projects
graph.ts 8.38 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;
        }
        Object.assign(this, data);

        this.nameToObjectGroup = new Map<string, NodeType>();
        this.objectGroups.forEach((group) =>
            this.nameToObjectGroup.set(group.name, group)
        );

        this.idToNode = new Map<number, Node>();
        this.nodes.forEach((node) => {
            this.idToNode.set(node.id, node);
        });
        this.idToLink = new Map<number, Link>();
        this.links.forEach((link) => {
            this.idToLink.set(link.id, link);
    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 {
        let objectGroups: Array<NodeType>;

        if (data.objectGroups === undefined) {
            objectGroups = this.createObjectGroupsFromStrings(data.nodes);
        } else {
            objectGroups = this.createObjectGroupsFromObjects(
                data.objectGroups
            );
        this.nameToObjectGroup = new Map<string, NodeType>();
        objectGroups.forEach((group) =>
            this.nameToObjectGroup.set(group.name, group)
        );

        this.nodes = this.createNodes(data.nodes);
        this.idToNode = new Map<number, Node>();
        this.nodes.forEach((node) => {
            this.idToNode.set(node.id, node);
        });

        this.links = data.links.map((link, i) => {
            const l = new Link();
            l.id = i;
            l.source = this.idToNode.get(link.source);
            l.target = this.idToNode.get(link.target);
            return l;
        });
        this.idToLink = new Map<number, Link>();
        this.links.forEach((link) => {
            this.idToLink.set(link.id, link);
        });

        this.updateNodeData();

        return this;
    private createNodes(nodeJSONData: NodeData[]): Array<Node> {
        const nodes: Array<Node> = [];
        for (const nodeData of nodeJSONData) {
            const node = new Node();
            node.fromSerializedObject(nodeData);
            node.type = this.nameToObjectGroup.get(nodeData.type);
            node.neighbors = [];
            node.links = [];
            nodes.push(node);
        }
        return nodes;
    }

    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++) {
            const nodeType = new NodeType();
            nodeType.fromSerializedObject({
                id: i,
                name: nodeTypes[i],
                color: Config.COLOR_PALETTE[i % Config.COLOR_PALETTE.length],
            });
        }
        return objectGroups;
    }

    private createObjectGroupsFromObjects(
        groups: NodeTypeData[]
    ): Array<NodeType> {
        return groups.map((group) => {
            const t = new NodeType();
            t.fromSerializedObject(group);
            return t;
        });
    /**
     * 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 checkNode(node: Node) {
        for (const neighbor of node.neighbors) {
            if (this.idToNode.get(neighbor.id) === undefined) {
                return false;
            }
        }
        for (const link of node.links) {
            if (this.idToLink.get(link.id) === undefined) {
                return false;
            }
        }
        return node.isInitialized();
    }

    private checkLink(link: Link) {
        return (
            link.isInitialized() &&
            !(
                this.idToNode.get(link.source.id) === undefined ||
                this.idToNode.get(link.target.id) === undefined
            )
        );
    }

    public addNode(node: Node) {
        node.id = this.nodes.length;

        if (!this.checkNode(node)) {
            return false;
        }

        this.nodes.push(node);
        this.idToNode.set(node.id, node);
        return true;
    }

    public addLink(link: Link) {
        link.id = this.links.length;

        if (!this.checkLink(link)) {
            return false;
        }

        this.links.push(link);
        this.idToLink.set(link.id, link);
        return true;
    }

    public deleteLink(id: number) {
        this.links = this.links.filter((l: Link) => l.id !== id);
        this.idToLink.delete(id);
    }

    public deleteNode(id: number) {
        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) {
        // TODO: Change to id/number
        const nodeType = this.nameToObjectGroup.get(id);

        for (const node of this.nodes) {
            if (node.type.id === nodeType.id) {
                node.type = undefined;
            }
        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,
        });