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";

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

Matthias Konitzny
committed
import { Link } from "../common/graph/link";

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

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;
}
type stateTypes = {
/**
* Graph structure holding the basic information.
*/
graph: DynamicGraph;
settings: EditorSettings;

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 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);

Matthias Konitzny
committed
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,
},
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 ...");
// 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);
// 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,
});
}
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[]) {
// Create a shallow copy of the graph object to trigger an update over setState

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

Matthias Konitzny
committed
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[]) {

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

Matthias Konitzny
committed
}
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[]) {

Matthias Konitzny
committed
const graph = Object.assign(new DynamicGraph(), this.state.graph);
ids.forEach((id) => graph.deleteLink(id));

Matthias Konitzny
committed
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">
<SelectLayer
screen2GraphCoords={
this.rendererRef.current
? this.rendererRef.current
.screen2GraphCoords
: undefined
}
isEnabled={this.state.keys["Shift"]}
onBoxSelect={this.handleBoxSelect}
>
<GraphRenderer2D
ref={this.rendererRef}
graph={this.state.graph}
onNodeSelectionChanged={this.selectNodes}

Matthias Konitzny
committed
onNodeCreation={this.handleNodeCreation}
onNodeDeletion={this.handleNodeDeletion}
onLinkCreation={this.handleLinkCreation}
onLinkDeletion={this.handleLinkDeletion}
selectedNodes={this.state.selectedNodes}
</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}
onNodeDataChange={this.handleNodeDataChange}
/>