Skip to content
Snippets Groups Projects
editor.tsx 13.4 KiB
Newer Older
  • Learn to ignore specific revisions
  • 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);
    
    
            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[]) {
    
            // 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);
            }
    
    
            graph.createCheckpoint(`Modified ${nodeData.length} node(s) data.`);
    
    
            // Push shallow copy to state
            this.setState({ graph: graph });
        }
    
        private handleNodeCreation(position?: Coordinate2D): Node {
            const graph = Object.assign(new DynamicGraph(), this.state.graph);
            const node = graph.createNode(undefined, position.x, position.y, 0, 0);
    
            graph.createCheckpoint("Created new node.");
    
        private handleNodeDeletion(ids: number[]) {
    
            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)
            );
    
            graph.createCheckpoint(`Deleted ${ids.length} nodes.`);
    
    
            this.setState({ graph: graph, selectedNodes: selectedNodes });
    
        }
    
        private handleLinkCreation(source: number, target: number): Link {
            const graph = Object.assign(new DynamicGraph(), this.state.graph);
            const link = graph.createLink(source, target);
    
            graph.createCheckpoint(
                `Created link between ${graph.node(source).name} and ${
                    graph.node(target).name
                }.`
            );
    
    
        private handleLinkDeletion(ids: number[]) {
    
            const graph = Object.assign(new DynamicGraph(), this.state.graph);
    
            ids.forEach((id) => graph.deleteLink(id));
    
            graph.createCheckpoint(`Deleted ${ids.length} link(s).`);
    
            this.setState({ graph: graph });
        }
    
        private loadGraphFromCheckpoint(checkpoint: Checkpoint<SimGraphData>) {
            const graph = new DynamicGraph();
            graph.fromSerializedObject(checkpoint.data);
            graph.history.copyCheckpointsFromHistory(this.state.graph.history);
    
            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);
    
        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}