import React from "react"; import { DynamicGraph } from "./graph"; import { loadGraphJson } from "../common/datasets"; import { NodeDetails } from "./components/nodedetails"; import { SpaceSelect } from "./components/spaceselect"; import "./editor.css"; import { Node } from "../common/graph/node"; import { NodeTypesEditor } from "./components/nodetypeseditor"; import { SpaceManager } from "./components/spacemanager"; import { SelectLayer } from "./components/selectlayer"; import { GraphData } from "../common/graph/graph"; import { NodeType } from "../common/graph/nodetype"; import { GraphRenderer2D } from "./renderer"; import Instructions from "./components/instructions"; import Settings from "./components/settings"; type propTypes = { spaceId: string; }; type stateTypes = { /** * Graph structure holding the basic information. */ graph: DynamicGraph; /** * Should labels on nodes be rendered, or none at all. */ visibleLabels: boolean; /** * Should feature be enabled, that nodes get connected with a link of dragged close enough to each other? */ connectOnDrag: boolean; /** * Current width of graph object. Used to specifically adjust and correct the graph size. */ graphWidth: number; /** * True for each key, that is currently considered pressed. If key has not been pressed yet, it will not exist as dict-key. */ keys: { [name: string]: boolean }; /** * Collection of all currently selected nodes. Can also be undefined or empty. */ selectedNodes: Node[]; }; /** * Knowledge space graph editor. Allows easy editing of the graph structure. */ export class Editor extends React.PureComponent<propTypes, stateTypes> { private graphContainer: React.RefObject<HTMLDivElement>; private rendererRef: React.RefObject<GraphRenderer2D>; constructor(props: propTypes) { super(props); // 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); document.addEventListener("keydown", (e) => { this.keyPressed(e.key); }); document.addEventListener("keyup", (e) => { this.keyReleased(e.key); }); this.graphContainer = React.createRef(); // Set as new state this.state = { graph: undefined, visibleLabels: true, connectOnDrag: false, graphWidth: 1000, selectedNodes: [], keys: {}, }; } 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.props.spaceId !== undefined) { // Load initial space this.loadSpace(this.props.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); // Create graph const graph = new DynamicGraph(); graph.fromSerializedObject(data); // Set as new state console.log(graph); this.setState({ graph: graph, }); //graph.onChangeCallbacks.push(this.onGraphDataChange); // Subscribe to global events window.addEventListener("resize", () => this.handleResize()); this.handleResize(); return true; } /** * Processes resize window event. Focusses on resizing the graph accordingly. */ private handleResize() { const newGraphWidth = this.graphContainer.current.clientWidth; this.setState({ graphWidth: newGraphWidth, }); } // /** // * Makes sure to always offer a valid format of the selected nodes. Is either undefined or contains at least one valid node. An empty array is never returned. // */ // private get selectedNodes(): Node[] { // // TODO: Here are a lot of things that should not be possible by design // // // Remove undefines // let selectedNodes = this.state.selectedNodes.filter( // (n: Node) => n !== undefined // ); // // // Remove duplicates // selectedNodes = [...new Set(selectedNodes)]; // // if (selectedNodes.length > 0) { // return selectedNodes; // } // // return undefined; // } handleBoxSelect(selectedNodes: Node[]) { if (selectedNodes !== undefined && selectedNodes.length <= 0) { return; } 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); } render(): React.ReactNode { return ( <div id="ks-editor"> <h1>Interface</h1> <SpaceSelect onLoadSpace={this.loadSpace} /> <SpaceManager /> <div id="content"> {this.state.graph && ( <div id="force-graph-renderer" ref={this.graphContainer} > <SelectLayer allNodes={ this.state.graph ? this.state.graph.nodes : [] } screen2GraphCoords={ this.rendererRef ? this.rendererRef.current .screen2GraphCoords : undefined } isEnabled={this.state.keys["Shift"]} onBoxSelect={this.handleBoxSelect} > <GraphRenderer2D ref={this.rendererRef} graph={this.state.graph} width={this.state.graphWidth} onNodeSelectionChanged={this.selectNodes} selectedNodes={this.state.selectedNodes} /> </SelectLayer> </div> )} {this.state.graph && ( <div id="sidepanel"> {/*<HistoryNavigator*/} {/* spaceId="space"*/} {/* history={this.state.graph.history}*/} {/* onChange={this.onGraphDataChange}*/} {/*/>*/} <hr /> <NodeDetails selectedNodes={this.state.selectedNodes} allTypes={ this.state.graph ? this.state.graph.objectGroups : [] } onChange={this.forceUpdate} /> <hr /> <h3>Node types</h3> <NodeTypesEditor onChange={this.forceUpdate} graph={this.state.graph} onSelectAll={this.handleNodeTypeSelect} /> <hr /> <Settings labelVisibility={this.state.visibleLabels} onLabelVisibilityChange={(visible) => this.setState({ visibleLabels: visible }) } connectOnDrag={this.state.connectOnDrag} onConnectOnDragChange={(connectOnDrag) => this.setState({ connectOnDrag: connectOnDrag, }) } /> <hr /> <Instructions connectOnDragEnabled={this.state.connectOnDrag} /> </div> )} </div> </div> ); } }