Newer
Older
import { DynamicGraph } from "./graph";
import { listAllSpaces, loadGraphJson } from "../common/datasets";
import SpaceSelect from "./components/spaceselect";
import * as Helpers from "../common/helpers";

Matthias Konitzny
committed
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";

Matthias Konitzny
committed
export interface NodeDataChangeRequest extends NodeProperties {
id: number;
type: NodeType;
}

Maximilian Giller
committed
/**
* Graph structure holding the basic information.
*/
graph: DynamicGraph;

Maximilian Giller
committed
/**
* Should labels on nodes be rendered, or none at all.
*/

Maximilian Giller
committed
visibleLabels: boolean;

Maximilian Giller
committed
/**
* Should feature be enabled, that nodes get connected with a link of dragged close enough to each other?
*/

Maximilian Giller
committed
connectOnDrag: boolean;

Maximilian Giller
committed
/**
* Current width of graph object. Used to specifically adjust and correct the graph size.

Maximilian Giller
committed
*/
graphWidth: number;

Maximilian Giller
committed
/**
* True for each key, that is currently considered pressed. If key has not been pressed yet, it will not exist as dict-key.
*/

Maximilian Giller
committed
keys: { [name: string]: boolean };

Maximilian Giller
committed
/**
* Collection of all currently selected nodes. Can also be undefined or empty.

Maximilian Giller
committed
*/
selectedNodes: Node[];
spaces: string[];
spaceId: string;

Maximilian Giller
committed
/**
* 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>;

Maximilian Giller
committed

Maximilian Giller
committed
// 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);

Matthias Konitzny
committed
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,

Maximilian Giller
committed
visibleLabels: true,
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 });
}

Maximilian Giller
committed
/**
* Tries to load initial graph after webpage finished loading.
*/
if (this.state.spaceId !== undefined) {
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 ...");
const graph = new DynamicGraph();
graph.fromSerializedObject(data);
// Set as new state
console.log(graph);
graph: graph,
//graph.onChangeCallbacks.push(this.onGraphDataChange);

Maximilian Giller
committed
// Subscribe to global events
window.addEventListener("resize", () => this.handleResize());
this.handleResize();
return true;
}

Maximilian Giller
committed
/**
* Processes resize window event. Focusses on resizing the graph accordingly.
*/
private handleResize() {
const newGraphWidth = Helpers.getClientWidth("knowledge-space-editor");
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;
// }

Maximilian Giller
committed
handleBoxSelect(selectedNodes: Node[]) {
if (selectedNodes !== undefined && selectedNodes.length <= 0) {

Maximilian Giller
committed
return;
}
this.selectNodes(selectedNodes.concat(this.state.selectedNodes));

Maximilian Giller
committed
}
/**
* 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,
});

Maximilian Giller
committed
}
private handleNodeTypeSelect(type: NodeType) {
const nodesWithType = this.state.graph.nodes.filter((n: Node) =>
n.type.equals(type)
);
this.selectNodes(nodesWithType);
}

Matthias Konitzny
committed
private handleNodeDataChange(nodeData: NodeDataChangeRequest[]) {
// Make 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}
/>
{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}
onNodeSelectionChanged={this.selectNodes}
selectedNodes={this.state.selectedNodes}
</SelectLayer>
</div>
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
{this.state.graph && (
<Sidepanel
graph={this.state.graph}
onCheckpointLoad={(checkpoint) => {
const graph = new DynamicGraph();
this.setState({
graph: graph.fromSerializedObject(
checkpoint.data
),
});
}}
onNodeTypeSelect={this.handleNodeTypeSelect}
onConnectOnDragChange={(connectOnDrag) =>
this.setState({
connectOnDrag: connectOnDrag,
})
}
onLabelVisibilityChange={(visible) =>
this.setState({
visibleLabels: visible,
})
}
selectedNodes={this.state.selectedNodes}
visibleLabels={this.state.visibleLabels}
connectOnDrag={this.state.connectOnDrag}
onNodeDataChange={this.handleNodeDataChange}
/>
)}