import React from "react"; import { State } from "../state"; import * as Interactions from "../interactions"; import { Graph } from "../structures/graph/graph"; import { loadGraphJson } from "../../../datasets"; import { NodeDetails } from "./nodedetails"; import { SpaceSelect } from "./spaceselect"; import "./editor.css"; import ReactForceGraph2d from "react-force-graph-2d"; import { Node } from "../structures/graph/node"; import { HistoryNavigator } from "./historynavigator"; type propTypes = any; type stateTypes = { graph: Graph; }; export class Editor extends React.PureComponent<propTypes, stateTypes> { private defaultWarmupTicks = 100; private warmupTicks = 100; private keyStates: { [name: string]: boolean } = {}; private selectedNode: Node; constructor(props: propTypes) { super(props); this.loadGraph = this.loadGraph.bind(this); this.loadSpace = this.loadSpace.bind(this); this.handleNodeClick = this.handleNodeClick.bind(this); this.onHistoryChange = this.onHistoryChange.bind(this); this.handleEngineStop = this.handleEngineStop.bind(this); this.handleKeyDown = this.handleKeyDown.bind(this); this.handleKeyUp = this.handleKeyUp.bind(this); this.forceUpdate = this.forceUpdate.bind(this); // Set as new state this.state = { graph: undefined, }; Interactions.initInteractions(); // Load initial space this.loadSpace("space"); } /** * 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: any): boolean { console.log("Starting to load new graph ..."); console.log(data); // Create graph const newGraph = Graph.parse(data); this.warmupTicks = this.defaultWarmupTicks; // Should only run for each newly initialized graph, but then never again // Is valid and parsed successfully? if (newGraph === undefined) { return false; } // .linkColor((link: any) => Editor.globalState.linkColor(link)) // .nodeColor((node: any) => Editor.globalState.nodeColor(node)) // .onNodeClick((node: any) => Editor.globalState.onNodeClick(node)) // .onNodeDragEnd((node: any, translate: any) => // Editor.globalState.onNodeDragEnd(node, translate) // ) // .linkWidth((link: any) => Editor.globalState.linkWidth(link)) // .linkDirectionalParticles( // Editor.globalState.linkDirectionalParticles() // ) // .linkDirectionalParticleWidth((link: any) => // Editor.globalState.linkDirectionalParticleWidth(link) // ) // .onBackgroundClick((event: any) => // Editor.globalState.onBackgroundClick( // event, // this.extractPositions(event) // ) // ) // .nodeCanvasObjectMode((node: any) => // Editor.globalState.nodeCanvasObjectMode(node) // ) // .nodeCanvasObject((node: any, ctx: any, globalScale: any) => // Editor.globalState.nodeCanvasObject(node, ctx, globalScale) // ) // .linkCanvasObjectMode((link: any) => // Editor.globalState.linkCanvasObjectMode(link) // ) // .linkCanvasObject((link: any, ctx: any, globalScale: any) => // Editor.globalState.linkCanvasObject(link, ctx, globalScale) // ) // .onLinkClick((link: any) => Editor.globalState.onLinkClick(link)); // Set as new state console.log(newGraph); this.setState({ graph: newGraph, }); this.state.graph.onChangeCallbacks.push(this.onHistoryChange); // Subscribe to global key-press events document.onkeydown = this.handleKeyDown; document.onkeyup = this.handleKeyUp; return true; } private handleKeyDown(event: KeyboardEvent) { const key: string = event.key; this.keyStates[key] = true; } private handleKeyUp(event: KeyboardEvent) { const key: string = event.key; this.keyStates[key] = false; } /** * Propagates the changed state of the graph. */ private onHistoryChange() { this.selectedNode = undefined; this.forceUpdate(); } /** * Calculates the corresponding coordinates for a click event for easier further processing. * @param event The corresponding click event. * @returns Coordinates in graph and coordinates in browser window. */ private extractPositions(event: any): { // graph: { x: number; y: number }; window: { x: number; y: number }; } { return { // graph: this.state.renderer.screen2GraphCoords( // event.layerX, // event.layerY // ), window: { x: event.clientX, y: event.clientY }, }; } private handleNodeClick(node: Node) { if (this.keyStates["Control"]) { node.delete(); } else { this.selectedNode = node; } this.forceUpdate(); } private handleEngineStop() { // Only do something on first stop for each graph if (this.warmupTicks <= 0) { return; } this.warmupTicks = 0; // Only warm up once, so stop warming up after the first freeze this.state.graph.storeCurrentData("Initial state", false); this.forceUpdate(); } render(): React.ReactNode { // The id "ks-editor" indicates, that the javascript associated with this should automatically be executed return ( <div id="ks-editor"> <h1>Interface</h1> <SpaceSelect onLoadSpace={this.loadSpace} /> <div id="content"> <div id="sidepanel"> <HistoryNavigator spaceId="space" historyObject={this.state.graph} onChange={this.onHistoryChange} /> <hr /> <NodeDetails selectedNode={this.selectedNode} allTypes={ this.state.graph ? this.state.graph.types : [] } onChange={this.forceUpdate} /> </div> {this.state.graph ? ( <ReactForceGraph2d graphData={this.state.graph.data} onNodeClick={this.handleNodeClick} autoPauseRedraw={false} cooldownTicks={0} warmupTicks={this.warmupTicks} onEngineStop={this.handleEngineStop} /> ) : undefined} </div> </div> ); } }