Newer
Older
import { DynamicGraph } from "./graph";
import {
listAllSpaces,
loadGraphJson,
saveGraphJson,
} 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 { Coordinate2D, GraphData, SimGraphData } 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";
import { Checkpoint } from "../common/history";

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.saveSpace = this.saveSpace.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);
this.handleCheckpointRequest = this.handleCheckpointRequest.bind(this);
this.handleUndo = this.handleUndo.bind(this);
this.handleRedo = this.handleRedo.bind(this);
this.createCheckpoint = this.createCheckpoint.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((data: GraphData) =>
this.loadGraph(data, spaceId)
);
}
public saveSpace() {
return saveGraphJson(
this.state.spaceId,
this.state.graph.toJSONSerializableObject()
).then(
() => console.log("Successfully saved space!"),
() => console.log("Something went wrong when saving the space! :(")
);
}
/**
* 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, id: string): 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;
}

Matthias Konitzny
committed
this.selectNodes([
...new Set(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);
}
private handleNodeDataChange(
nodeData: NodeDataChangeRequest[],
createCheckpoint = true
) {
if (nodeData.length == 0) {
return;
}
// 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);
}
// Create checkpoint
if (createCheckpoint) {
graph.createCheckpoint(`Modified ${nodeData.length} node(s) data.`);
}

Matthias Konitzny
committed
// Push shallow copy to state
this.setState({ graph: graph });
}
private handleNodeCreation(
position?: Coordinate2D,
createCheckpoint = true
): Node {

Matthias Konitzny
committed
const graph = Object.assign(new DynamicGraph(), this.state.graph);
const node = graph.createNode(undefined, position.x, position.y, 0, 0);
if (createCheckpoint) {
graph.createCheckpoint("Created new node.");
}

Matthias Konitzny
committed
this.setState({
graph: graph,
});
return node;
}
private handleNodeDeletion(ids: number[], createCheckpoint = true) {
if (ids.length == 0) {
return;
}

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)
);
if (createCheckpoint) {
graph.createCheckpoint(`Deleted ${ids.length} nodes.`);
}
this.setState({ graph: graph, selectedNodes: selectedNodes });

Matthias Konitzny
committed
}
private handleLinkCreation(
source: number,
target: number,
createCheckpoint = true
): Link {

Matthias Konitzny
committed
const graph = Object.assign(new DynamicGraph(), this.state.graph);
const link = graph.createLink(source, target);
if (createCheckpoint) {
graph.createCheckpoint(
`Created link between ${graph.node(source).name} and ${
graph.node(target).name
}.`
);
}

Matthias Konitzny
committed
this.setState({ graph: graph });
return link;
}
private handleLinkDeletion(ids: number[], createCheckpoint = true) {

Matthias Konitzny
committed
const graph = Object.assign(new DynamicGraph(), this.state.graph);
ids.forEach((id) => graph.deleteLink(id));
if (createCheckpoint) {
graph.createCheckpoint(`Deleted ${ids.length} link(s).`);
}
this.setState({ graph: graph });
}
private loadGraphFromCheckpoint(checkpoint: Checkpoint<SimGraphData>) {
const graph = new DynamicGraph();
graph.fromSerializedObject(checkpoint.data);
// Transfer checkpoints to new graph object
graph.history.copyCheckpointsFromHistory(this.state.graph.history);

Matthias Konitzny
committed
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
const selectedNodes = this.state.selectedNodes
.map((node) => graph.node(node.id))
.filter((node) => node != undefined);
this.setState({ graph: graph, selectedNodes: selectedNodes });
}
private handleCheckpointRequest(id: number) {
const history = this.state.graph.history;
const checkpoint = history.resetToCheckpoint(id);
this.loadGraphFromCheckpoint(checkpoint);
}
private handleUndo() {
const history = this.state.graph.history;
const checkpoint = history.undo();
this.loadGraphFromCheckpoint(checkpoint);
}
private handleRedo() {
const history = this.state.graph.history;
const checkpoint = history.redo();
this.loadGraphFromCheckpoint(checkpoint);
}
private clearHistory(description = "") {
const graph = Object.assign(new DynamicGraph(), this.state.graph);
graph.history.clearHistory(description);

Matthias Konitzny
committed
this.setState({ graph: graph });
}
/**
* Creates a new Checkpoint in the graph history
* @param description Checkpoint description.
*/
private createCheckpoint(description: string) {
const graph = Object.assign(new DynamicGraph(), this.state.graph);
graph.createCheckpoint(description);
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}
onEngineStop={() =>
this.clearHistory(
`Loaded graph ${this.state.spaceId}.`
)
}
selectedNodes={this.state.selectedNodes}
</SelectLayer>
</div>
<Sidepanel
graph={this.state.graph}
onCheckpointRequest={this.handleCheckpointRequest}
onUndo={this.handleUndo}
onRedo={this.handleRedo}
onNodeTypeSelect={this.handleNodeTypeSelect}
onSettingsChange={(settings) =>
this.setState({ settings: settings })
}
selectedNodes={this.state.selectedNodes}
onNodeDataChange={this.handleNodeDataChange}
createCheckpoint={this.createCheckpoint}