Skip to content
Snippets Groups Projects
editor.tsx 14.5 KiB
Newer Older
import React from "react";
import { DynamicGraph } from "./graph";
import {
    listAllSpaces,
    loadGraphJson,
    saveGraphJson,
} from "../common/datasets";
import SpaceSelect from "./components/spaceselect";
import "./editor.css";
import * as Helpers from "../common/helpers";
import { Node, NodeProperties } from "../common/graph/node";
import { SpaceManager } from "./components/spacemanager";
import SelectLayer from "./components/selectlayer";
import { Coordinate2D, GraphData, SimGraphData } from "../common/graph/graph";
import { NodeType } from "../common/graph/nodetype";
import { GraphRenderer2D } from "./renderer";
import * as Config from "../config";
import Sidepanel from "./components/sidepanel";
import { Link } from "../common/graph/link";
import { Checkpoint } from "../common/history";
export interface NodeDataChangeRequest extends NodeProperties {
    id: number;
    type: NodeType;
}

export interface EditorSettings {
    /**
     * Should labels on nodes be rendered, or none at all.
     */

    /**
     * Should feature be enabled, that nodes get connected with a link of dragged close enough to each other?
     */
}

type stateTypes = {
    /**
     * Graph structure holding the basic information.
     */
    graph: DynamicGraph;

    settings: EditorSettings;
     * Current width of graph object. Used to specifically adjust and correct the graph size.

    /**
     * True for each key, that is currently considered pressed. If key has not been pressed yet, it will not exist as dict-key.
     */
     * Collection of all currently selected nodes. Can also be undefined or empty.

    spaces: string[];

    spaceId: string;
/**
 * Knowledge space graph editor. Allows easy editing of the graph structure.
 */
export class Editor extends React.PureComponent<any, stateTypes> {
    private rendererRef: React.RefObject<GraphRenderer2D>;
    constructor(props: any) {

        // Making sure, all functions retain the proper this-bind
        this.loadGraph = this.loadGraph.bind(this);
        this.loadSpace = this.loadSpace.bind(this);
        this.saveSpace = this.saveSpace.bind(this);
        this.forceUpdate = this.forceUpdate.bind(this);
        this.handleNodeTypeSelect = this.handleNodeTypeSelect.bind(this);
        this.handleBoxSelect = this.handleBoxSelect.bind(this);
        this.selectNodes = this.selectNodes.bind(this);
        this.handleNodeDataChange = this.handleNodeDataChange.bind(this);
        this.handleNodeCreation = this.handleNodeCreation.bind(this);
        this.handleNodeDeletion = this.handleNodeDeletion.bind(this);
        this.handleLinkCreation = this.handleLinkCreation.bind(this);
        this.handleLinkDeletion = this.handleLinkDeletion.bind(this);
        this.handleCheckpointRequest = this.handleCheckpointRequest.bind(this);
        this.handleUndo = this.handleUndo.bind(this);
        this.handleRedo = this.handleRedo.bind(this);
        this.createCheckpoint = this.createCheckpoint.bind(this);

        document.addEventListener("keydown", (e) => {
            this.keyPressed(e.key);
        });
        document.addEventListener("keyup", (e) => {
            this.keyReleased(e.key);
        });
        this.rendererRef = React.createRef();
        listAllSpaces().then((spaces) => this.setState({ spaces: spaces }));

        // Set as new state
        this.state = {
            graph: undefined,
            settings: {
                visibleLabels: true,
                connectOnDrag: false,
            },
            spaces: [],
            spaceId: Config.SPACE,
    keyPressed(key: string) {
        const keys = this.state.keys;
        keys[key] = true;
        this.setState({ keys: { ...keys } });
    }

    keyReleased(key: string) {
        const keys = this.state.keys;
        keys[key] = false;
        this.setState({ keys: { ...keys } });
    /**
     * Tries to load initial graph after webpage finished loading.
     */
    componentDidMount() {
        if (this.state.spaceId !== undefined) {
            // Load initial space
            this.loadSpace(this.state.spaceId);
    }

    /**
     * Loads a space from the database to the editor.
     * @param spaceId Id of space to load.
     * @returns Promise with boolean value that is true, if successful.
     */
    public loadSpace(spaceId: string): any {
        return loadGraphJson(spaceId).then((data: GraphData) =>
            this.loadGraph(data, spaceId)
        );
    }

    public saveSpace() {
        return saveGraphJson(
            this.state.spaceId,
            this.state.graph.toJSONSerializableObject()
        ).then(
            () => console.log("Successfully saved space!"),
            () => console.log("Something went wrong when saving the space! :(")
        );
    }

    /**
     * Loads another graph based on the data supplied. Note: Naming currently suggests that this only loads a GRAPH, not a SPACE. Needs further work and implementation to see if that makes sense or not.
     * @param data Serialized graph data.
     * @returns True, if successful.
     */
    public loadGraph(data: GraphData, id: string): boolean {
        console.log("Starting to load new graph ...");
        console.log(data);
        // Allow a single phase of force simulation when loading the graph.
        if (this.rendererRef.current != undefined) {
            this.rendererRef.current.allowForceSimulation();
        }

        const graph = new DynamicGraph();
        graph.fromSerializedObject(data);
            spaceId: id,
        //graph.onChangeCallbacks.push(this.onGraphDataChange);
        // Subscribe to global events
        window.addEventListener("resize", () => this.handleResize());
    /**
     * Processes resize window event. Focusses on resizing the graph accordingly.
     */
        const newGraphWidth = Helpers.getClientWidth("knowledge-space-editor");
        this.setState({
            graphWidth: newGraphWidth,
        });
    }

    handleBoxSelect(selectedNodes: Node[]) {
        if (selectedNodes !== undefined && selectedNodes.length <= 0) {
        this.selectNodes([
            ...new Set(selectedNodes.concat(this.state.selectedNodes)),
        ]);
    /**
     * Selects multiple nodes, or clears selection if given undefined or empty array.
     * @param nodes Multiple nodes to mark as selected.
     */
    public selectNodes(nodes: Node[]) {
        this.setState({
            selectedNodes: nodes,
        });
    private handleNodeTypeSelect(type: NodeType) {
        const nodesWithType = this.state.graph.nodes.filter((n: Node) =>
            n.type.equals(type)
        );
        this.selectNodes(nodesWithType);
    }

    private handleNodeDataChange(
        nodeData: NodeDataChangeRequest[],
        createCheckpoint = true
    ) {
        if (nodeData.length == 0) {
            return;
        }

        // Create a shallow copy of the graph object to trigger an update over setState
        const graph = Object.assign(new DynamicGraph(), this.state.graph);

        // Modify node
        for (const request of nodeData) {
            const node = graph.node(request.id);
            Object.assign(node, request);
        }

        // Create checkpoint
        if (createCheckpoint) {
            graph.createCheckpoint(`Modified ${nodeData.length} node(s) data.`);
        }
        // Push shallow copy to state
        this.setState({ graph: graph });
    }
    private handleNodeCreation(
        position?: Coordinate2D,
        createCheckpoint = true
    ): Node {
        const graph = Object.assign(new DynamicGraph(), this.state.graph);
        const node = graph.createNode(undefined, position.x, position.y, 0, 0);

        if (createCheckpoint) {
            graph.createCheckpoint("Created new node.");
        }
    private handleNodeDeletion(ids: number[], createCheckpoint = true) {
        const graph = Object.assign(new DynamicGraph(), this.state.graph);
        ids.forEach((id) => graph.deleteNode(id));
        const selectedNodes = this.state.selectedNodes.filter(
            (node) => !ids.includes(node.id)
        );

        if (createCheckpoint) {
            graph.createCheckpoint(`Deleted ${ids.length} nodes.`);
        }

        this.setState({ graph: graph, selectedNodes: selectedNodes });
    private handleLinkCreation(
        source: number,
        target: number,
        createCheckpoint = true
    ): Link {
        const graph = Object.assign(new DynamicGraph(), this.state.graph);
        const link = graph.createLink(source, target);

        if (createCheckpoint) {
            graph.createCheckpoint(
                `Created link between ${graph.node(source).name} and ${
                    graph.node(target).name
                }.`
            );
        }
    private handleLinkDeletion(ids: number[], createCheckpoint = true) {
        const graph = Object.assign(new DynamicGraph(), this.state.graph);
        ids.forEach((id) => graph.deleteLink(id));

        if (createCheckpoint) {
            graph.createCheckpoint(`Deleted ${ids.length} link(s).`);
        }

        this.setState({ graph: graph });
    }

    private loadGraphFromCheckpoint(checkpoint: Checkpoint<SimGraphData>) {
        const graph = new DynamicGraph();
        graph.fromSerializedObject(checkpoint.data);

        // Transfer checkpoints to new graph object
        graph.history.copyCheckpointsFromHistory(this.state.graph.history);
        // Restore selected nodes
        const selectedNodes = this.state.selectedNodes
            .map((node) => graph.node(node.id))
            .filter((node) => node != undefined);

        this.setState({ graph: graph, selectedNodes: selectedNodes });
    }

    private handleCheckpointRequest(id: number) {
        const history = this.state.graph.history;
        const checkpoint = history.resetToCheckpoint(id);
        this.loadGraphFromCheckpoint(checkpoint);
    }

    private handleUndo() {
        const history = this.state.graph.history;
        const checkpoint = history.undo();
        this.loadGraphFromCheckpoint(checkpoint);
    }

    private handleRedo() {
        const history = this.state.graph.history;
        const checkpoint = history.redo();
        this.loadGraphFromCheckpoint(checkpoint);
    }

    private clearHistory(description = "") {
        const graph = Object.assign(new DynamicGraph(), this.state.graph);
        graph.history.clearHistory(description);
    /**
     * Creates a new Checkpoint in the graph history
     * @param description Checkpoint description.
     */
    private createCheckpoint(description: string) {
        const graph = Object.assign(new DynamicGraph(), this.state.graph);
        graph.createCheckpoint(description);
        this.setState({ graph: graph });
    }

    render(): React.ReactNode {
        return (
            <div id="ks-editor">
                <h1>Interface</h1>
                <SpaceSelect
                    onLoadSpace={this.loadSpace}
                    spaces={this.state.spaces}
                    spaceId={this.state.spaceId}
                />
                {this.state.graph && (
                    <div id="content">
                                nodes={this.state.graph.nodes}
                                        ? this.rendererRef.current
                                              .screen2GraphCoords
                                        : undefined
                                }
                                isEnabled={this.state.keys["Shift"]}
                                onBoxSelect={this.handleBoxSelect}
                            >
                                <GraphRenderer2D
                                    ref={this.rendererRef}
                                    graph={this.state.graph}
Maximilian Giller's avatar
Maximilian Giller committed
                                    width={this.state.graphWidth}
                                    onNodeSelectionChanged={this.selectNodes}
                                    onNodeCreation={this.handleNodeCreation}
                                    onNodeDeletion={this.handleNodeDeletion}
                                    onLinkCreation={this.handleLinkCreation}
                                    onLinkDeletion={this.handleLinkDeletion}
                                    onEngineStop={() =>
                                        this.clearHistory(
                                            `Loaded graph ${this.state.spaceId}.`
                                        )
                                    }
                                    selectedNodes={this.state.selectedNodes}
                                    settings={this.state.settings}
Maximilian Giller's avatar
Maximilian Giller committed
                                />

                        <Sidepanel
                            graph={this.state.graph}
                            onCheckpointRequest={this.handleCheckpointRequest}
                            onUndo={this.handleUndo}
                            onRedo={this.handleRedo}
                            onNodeTypeSelect={this.handleNodeTypeSelect}
                            onSettingsChange={(settings) =>
                                this.setState({ settings: settings })
                            }
                            selectedNodes={this.state.selectedNodes}
                            settings={this.state.settings}
                            onNodeDataChange={this.handleNodeDataChange}
                            onSave={this.saveSpace}
                            createCheckpoint={this.createCheckpoint}