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 { 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"; export interface NodeDataChangeRequest extends NodeProperties { id: number; type: NodeType; } export interface EditorSettings { /** * 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; } 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. */ 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[]; spaces: string[]; spaceId: string; }; /** * Knowledge space graph editor. Allows easy editing of the graph structure. */ export class Editor extends React.PureComponent<any, stateTypes> { private graphContainer: React.RefObject<HTMLDivElement>; private rendererRef: React.RefObject<GraphRenderer2D>; constructor(props: any) { 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); this.handleNodeDataChange = this.handleNodeDataChange.bind(this); document.addEventListener("keydown", (e) => { this.keyPressed(e.key); }); document.addEventListener("keyup", (e) => { this.keyReleased(e.key); }); this.graphContainer = React.createRef(); listAllSpaces().then((spaces) => this.setState({ spaces: spaces })); // Set as new state this.state = { graph: undefined, settings: { visibleLabels: true, connectOnDrag: false, }, graphWidth: 1000, selectedNodes: [], keys: {}, 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); // 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 = Helpers.getClientWidth("knowledge-space-editor"); this.setState({ graphWidth: newGraphWidth, }); } 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); } 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 }); } render(): React.ReactNode { return ( <div id="ks-editor"> <h1>Interface</h1> <SpaceSelect onLoadSpace={this.loadSpace} spaces={this.state.spaces} spaceId={this.state.spaceId} /> <SpaceManager /> {this.state.graph && ( <div id="content"> <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} settings={this.state.settings} /> </SelectLayer> </div> <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} /> </div> )} </div> ); } }