Skip to content
Snippets Groups Projects
editor.tsx 10.8 KiB
Newer Older
  • Learn to ignore specific revisions
  • import React from "react";
    
    import { DynamicGraph } from "./graph";
    
    import { listAllSpaces, loadGraphJson } 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 } 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";
    
    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.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);
    
    
            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(this.loadGraph);
        }
    
        /**
         * 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): 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);
    
            //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(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);
            }
    
            // 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);
    
            this.setState({
                graph: graph,
            });
            return 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)
            );
    
            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);
            this.setState({ graph: graph });
    
            return link;
        }
    
    
        private handleLinkDeletion(ids: number[]) {
    
            const graph = Object.assign(new DynamicGraph(), this.state.graph);
    
            ids.forEach((id) => graph.deleteLink(id));
    
        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}
    
                                        selectedNodes={this.state.selectedNodes}
    
                                        settings={this.state.settings}
    
    Maximilian Giller's avatar
    Maximilian Giller committed
                                    />
    
    
                            <Sidepanel
                                graph={this.state.graph}
                                onCheckpointLoad={(checkpoint) => {
                                    const graph = new DynamicGraph();
                                    this.setState({
                                        graph: graph.fromSerializedObject(
                                            checkpoint.data
                                        ),
                                    });
                                }}
                                onNodeTypeSelect={this.handleNodeTypeSelect}
    
                                onSettingsChange={(settings) =>
                                    this.setState({ settings: settings })
    
                                }
                                selectedNodes={this.state.selectedNodes}
    
                                settings={this.state.settings}
    
                                onNodeDataChange={this.handleNodeDataChange}
                            />