Skip to content
Snippets Groups Projects
graph.ts 8.79 KiB
Newer Older
  • Learn to ignore specific revisions
  • import { Node, NodeData, SimNodeData } from "./node";
    import { Link, LinkData, SimLinkData } from "./link";
    import { NodeType, NodeTypeData } from "./nodetype";
    
    import { SerializableItem } from "../serializableitem";
    
    export interface Coordinate2D {
    
        x: number;
        y: number;
    
    }
    
    export interface Coordinate3D extends Coordinate2D {
    
    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 initialized: boolean;
    
    
        public idToObjectGroup: Map<number, NodeType>;
    
        private idToNode: Map<number, Node>;
    
        private idToLink: Map<number, Link>;
    
        private nextNodeId = 0;
        private nextLinkId = 0;
        private nextObjectGroupId = 0;
    
    
        /**
         * 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.idToObjectGroup.set(group.id, group)
    
            this.nodes.forEach((node) => {
                this.idToNode.set(node.id, node);
            });
            this.links.forEach((link) => {
                this.idToLink.set(link.id, link);
    
            this.updateNodeData();
            this.initializeIdGeneration();
    
            this.nodes = [];
            this.links = [];
    
            this.objectGroups = [];
    
            this.idToObjectGroup = new Map<number, NodeType>();
    
            this.idToNode = new Map<number, Node>();
            this.idToLink = new Map<number, Link>();
        }
    
    
        private initializeIdGeneration() {
    
            this.nextNodeId = Math.max(
                Math.max(...this.nodes.map((node) => node.id)) + 1,
                0
            );
            this.nextLinkId = Math.max(
                Math.max(...this.links.map((link) => link.id)) + 1,
                0
            );
    
            this.nextObjectGroupId = Math.max(
    
                Math.max(...this.objectGroups.map((group) => group.id), 0)
    
        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 {
    
            data.objectGroups.forEach((group) => this.createObjectGroup(group));
    
            this.createDefaultObjectGroupIfNeeded();
    
            data.nodes.forEach((node) => this.createNode(node));
            data.links.forEach((link) => this.createLink(link.source, link.target));
    
        private createDefaultObjectGroupIfNeeded() {
            if (this.objectGroups.length == 0) {
    
                this.createObjectGroup({
                    id: 0,
                    name: "Default",
                    color: "#000000",
                });
    
        /**
         * Updates the graph data structure to contain additional values.
         * Creates a 'neighbors' and 'links' array for each node object.
         */
    
        private updateNodeData(): Link[] {
    
            this.nodes.forEach((node) => {
                node.links = [];
                node.neighbors = [];
            });
    
    
            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) {
    
            if (this.idToNode.has(node.id)) {
    
                node.id = ++this.nextNodeId;
    
            this.nodes.push(node);
            this.idToNode.set(node.id, node);
    
    Matthias Konitzny's avatar
    Matthias Konitzny committed
        public createNode(data?: NodeData | SimNodeData): Node {
    
            node.fromSerializedObject(data);
    
            node.type = this.idToObjectGroup.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);
    
            sourceNode.links.push(link);
            targetNode.links.push(link);
            this.addLink(link);
            return link;
    
        public createObjectGroup(data: NodeTypeData): NodeType {
            const group = new NodeType(data.name, data.color);
            group.id = data.id;
    
            this.addObjectGroup(group);
            return group;
        }
    
        private addObjectGroup(group: NodeType) {
    
            if (this.idToObjectGroup.has(group.id)) {
                console.warn(
                    "Replacing id of object group. Node association to groups might not be correct."
                );
                group.id = ++this.nextObjectGroupId;
            }
    
    
            this.objectGroups.push(group);
    
            this.idToObjectGroup.set(group.id, group);
    
        private addLink(link: Link) {
    
            if (this.idToLink.has(link.id)) {
    
                link.id = ++this.nextLinkId;
    
            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 = link.source.links.filter((l) => l.id != id);
            link.target.links = 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: number): boolean {
    
            if (this.objectGroups.length <= 1) {
                // Do not allow to delete the last node type.
                return false;
            }
    
    
            const nodeType = this.idToObjectGroup.get(id);
    
    
            for (const node of this.nodes) {
                if (node.type.id === nodeType.id) {
    
            nodeTypes: Map<number, boolean>,
            linkTypes?: Map<number, boolean>
    
            const nodes = this.nodes.filter((l) => nodeTypes.get(l.type.id));
    
            let links;
            if (linkTypes === undefined) {
                links = this.links;
            } else {
    
                links = this.links.filter((l) => linkTypes.get(l.type.id));
    
            // Filter links which are connected to an invisible node
            links = links.filter(
    
                    nodeTypes.get(l.source.type.id) &&
                    nodeTypes.get(l.target.type.id)
    
            return new Graph({
                nodes: nodes,
                links: links,
                objectGroups: this.objectGroups,
            });