From 9aeda83e89f25470430366bce480bd52f949fe3f Mon Sep 17 00:00:00 2001 From: Matthias Konitzny <konitzny@ibr.cs.tu-bs.de> Date: Thu, 8 Sep 2022 18:33:54 +0200 Subject: [PATCH] Created new merged structures. Still lots of refactoring left. --- src/common/datasets.js | 6 +- src/common/graph.ts | 318 ++++++++++++------ src/common/graphelement.ts | 38 +++ src/common/history.ts | 59 ++++ src/common/link.ts | 105 ++++++ src/common/node.ts | 177 ++++++++++ src/common/nodetype.ts | 47 +++ src/common/serializableitem.ts | 30 ++ .../components/nodefilter/filtermenu.tsx | 2 +- src/display/components/nodeinfo/neighbors.tsx | 2 +- .../components/nodeinfo/nodeinfobar.tsx | 2 +- src/display/display.tsx | 4 +- src/display/renderer.tsx | 2 +- src/editor/js/components/editor.tsx | 6 +- src/editor/js/components/nodedetails.tsx | 4 +- src/editor/js/components/nodetypeentry.tsx | 2 +- src/editor/js/components/nodetypeseditor.tsx | 2 +- src/editor/js/structures/graph/graph.ts | 45 ++- .../js/structures/graph/graphelement.ts | 65 ---- src/editor/js/structures/graph/link.ts | 144 -------- src/editor/js/structures/graph/node.ts | 182 ---------- src/editor/js/structures/graph/nodetype.ts | 60 ---- .../js/structures/helper/serializableitem.ts | 66 ---- src/editor/js/structures/manageddata.ts | 61 ++-- 24 files changed, 736 insertions(+), 693 deletions(-) create mode 100644 src/common/graphelement.ts create mode 100644 src/common/history.ts create mode 100644 src/common/link.ts create mode 100644 src/common/node.ts create mode 100644 src/common/nodetype.ts create mode 100644 src/common/serializableitem.ts delete mode 100644 src/editor/js/structures/graph/graphelement.ts delete mode 100644 src/editor/js/structures/graph/link.ts delete mode 100644 src/editor/js/structures/graph/node.ts delete mode 100644 src/editor/js/structures/graph/nodetype.ts delete mode 100644 src/editor/js/structures/helper/serializableitem.ts diff --git a/src/common/datasets.js b/src/common/datasets.js index a7d253e..8aa9d68 100644 --- a/src/common/datasets.js +++ b/src/common/datasets.js @@ -35,15 +35,15 @@ export function loadGraphJson(spaceId) { * Takes the graph json object and stores it in the backend. * * @param {String} spaceId Identification of graph to save. - * @param {object} json Graph object + * @param {object} object Graph object * * @returns Promise returning state of query. */ -export function saveGraphJson(spaceId, json) { +export function saveGraphJson(spaceId, object) { const data = new FormData(); data.append("action", "update_space"); data.append("space", spaceId); - data.append("graph", JSON.stringify(json)); + data.append("graph", JSON.stringify(object)); return ajaxCall(data); } diff --git a/src/common/graph.ts b/src/common/graph.ts index f846315..ee23da5 100644 --- a/src/common/graph.ts +++ b/src/common/graph.ts @@ -1,44 +1,8 @@ import * as Config from "../config"; - -interface LinkData { - source: number; - target: number; - type?: string; -} - -export interface Link { - source: Node; - target: Node; - type?: GraphObjectType; -} - -interface NodeContent { - name: string; - description?: string; - icon?: string; - banner?: string; - video?: string; - references?: string[]; -} - -interface NodeData extends NodeContent { - id: number; - type?: string; -} - -export interface Node extends NodeContent { - id: number; - type: GraphObjectType; - - neighbors: Node[]; - links: Link[]; -} - -export interface GraphObjectType { - id: number; - name: string; - color?: string; -} +import { Node, NodeData, SimNodeData } from "./node"; +import { Link, LinkData, SimLinkData } from "./link"; +import { NodeType, NodeTypeData } from "./nodetype"; +import { SerializableItem } from "./serializableitem"; export interface Coordinate { x: number; @@ -46,73 +10,174 @@ export interface Coordinate { z: number; } +export interface GraphData { + nodes: NodeData[]; + links: LinkData[]; + objectGroups?: NodeTypeData[]; +} + +export interface SimGraphData { + nodes: SimNodeData[]; + links: SimLinkData[]; + objectGroups: NodeTypeData[]; +} + export interface GraphContent { nodes: Node[]; links: Link[]; - objectGroups: GraphObjectType[]; + objectGroups?: NodeType[]; } /** * Basic graph data structure. */ -export default class Graph implements GraphContent { +export class Graph + extends SerializableItem<GraphData, SimGraphData> + implements GraphContent +{ public nodes: Node[]; public links: Link[]; - public objectGroups: GraphObjectType[]; - public nameToObjectGroup: Map<string, GraphObjectType>; + public objectGroups: NodeType[]; + public nameToObjectGroup: Map<string, NodeType>; + public initialized: boolean; + private idToNode: Map<number, Node>; + private idToLink: Map<number, Link>; - constructor( - nodes: NodeData[], - links: LinkData[], - objectGroups?: GraphObjectType[] - ) { - this.objectGroups = objectGroups ?? this.createObjectGroups(nodes); + /** + * Creates a new Graph object. + * Make sure the nodes and links are connected to the correct objects before calling this method! + */ + constructor(data?: GraphContent) { + super(0); + + if (data === undefined) { + this.initialized = false; + return; + } - this.nameToObjectGroup = new Map<string, GraphObjectType>(); + Object.assign(this, data); + + this.nameToObjectGroup = new Map<string, NodeType>(); this.objectGroups.forEach((group) => this.nameToObjectGroup.set(group.name, group) ); - this.createNodes(nodes); + this.idToNode = new Map<number, Node>(); + this.nodes.forEach((node) => { + this.idToNode.set(node.id, node); + }); - this.links = links.map((link) => { - return { - source: this.idToNode.get(link.source), - target: this.idToNode.get(link.target), - }; + this.idToLink = new Map<number, Link>(); + this.links.forEach((link) => { + this.idToLink.set(link.id, link); }); + } - this.updateNodeData(); - this.removeFloatingNodes(); + public toJSONSerializableObject(): GraphData { + return { + nodes: this.nodes.map((node) => node.toJSONSerializableObject()), + links: this.links.map((link) => link.toJSONSerializableObject()), + objectGroups: this.objectGroups.map((group) => + group.toJSONSerializableObject() + ), + }; } - private createNodes(nodes: NodeData[]) { - this.nodes = []; - for (const nodeData of nodes) { - const { type, ...nodeVars } = nodeData; - const node = { ...nodeVars } as Node; - node.type = this.nameToObjectGroup.get(type); - node.neighbors = []; - node.links = []; - this.nodes.push(node); + public toHistorySerializableObject(): SimGraphData { + return { + nodes: this.nodes.map((node) => node.toHistorySerializableObject()), + links: this.links.map((link) => link.toHistorySerializableObject()), + objectGroups: this.objectGroups.map((group) => + group.toHistorySerializableObject() + ), + }; + } + + public fromSerializedObject(data: GraphData | SimGraphData): Graph { + let objectGroups: Array<NodeType>; + + if (data.objectGroups === undefined) { + objectGroups = this.createObjectGroupsFromStrings(data.nodes); + } else { + objectGroups = this.createObjectGroupsFromObjects( + data.objectGroups + ); } + this.nameToObjectGroup = new Map<string, NodeType>(); + objectGroups.forEach((group) => + this.nameToObjectGroup.set(group.name, group) + ); + + this.nodes = this.createNodes(data.nodes); this.idToNode = new Map<number, Node>(); this.nodes.forEach((node) => { this.idToNode.set(node.id, node); }); + + this.links = data.links.map((link, i) => { + const l = new Link(); + l.id = i; + l.source = this.idToNode.get(link.source); + l.target = this.idToNode.get(link.target); + return l; + }); + this.idToLink = new Map<number, Link>(); + this.links.forEach((link) => { + this.idToLink.set(link.id, link); + }); + + this.updateNodeData(); + + return this; } - private removeFloatingNodes() { - this.nodes = this.nodes.filter((node) => node.neighbors.length > 0); + private createNodes(nodeJSONData: NodeData[]): Array<Node> { + const nodes: Array<Node> = []; + for (const nodeData of nodeJSONData) { + const node = new Node(); + node.fromSerializedObject(nodeData); + node.type = this.nameToObjectGroup.get(nodeData.type); + node.neighbors = []; + node.links = []; + nodes.push(node); + } + return nodes; + } + + private createObjectGroupsFromStrings(nodes: NodeData[]): Array<NodeType> { + const objectGroups: NodeType[] = []; + const nodeClasses: string[] = []; + nodes.forEach((node) => nodeClasses.push(node.type)); + const nodeTypes = [...new Set(nodeClasses)].map((c) => String(c)); + + for (let i = 0; i < nodeTypes.length; i++) { + const nodeType = new NodeType(); + nodeType.fromSerializedObject({ + id: i, + name: nodeTypes[i], + color: Config.COLOR_PALETTE[i % Config.COLOR_PALETTE.length], + }); + } + return objectGroups; + } + + private createObjectGroupsFromObjects( + groups: NodeTypeData[] + ): Array<NodeType> { + return groups.map((group) => { + const t = new NodeType(); + t.fromSerializedObject(group); + return t; + }); } /** * Updates the graph data structure to contain additional values. * Creates a 'neighbors' and 'links' array for each node object. */ - private updateNodeData() { + private updateNodeData(): Link[] { this.links.forEach((link) => { const a = link.source; const b = link.target; @@ -121,26 +186,90 @@ export default class Graph implements GraphContent { a.links.push(link); b.links.push(link); }); + return this.links; + } + + public removeFloatingNodes() { + this.nodes = this.nodes.filter((node) => node.neighbors.length > 0); } public node(id: number): Node { return this.idToNode.get(id); } - private createObjectGroups(nodes: NodeData[]): GraphObjectType[] { - const objectGroups: GraphObjectType[] = []; - const nodeClasses: string[] = []; - nodes.forEach((node) => nodeClasses.push(node.type)); - const nodeTypes = [...new Set(nodeClasses)].map((c) => String(c)); + private checkNode(node: Node) { + for (const neighbor of node.neighbors) { + if (this.idToNode.get(neighbor.id) === undefined) { + return false; + } + } - for (let i = 0; i < nodeTypes.length; i++) { - objectGroups.push({ - id: undefined, // Does not matter for display graph - name: nodeTypes[i], - color: Config.COLOR_PALETTE[i % Config.COLOR_PALETTE.length], - }); + for (const link of node.links) { + if (this.idToLink.get(link.id) === undefined) { + return false; + } + } + return node.isInitialized(); + } + + private checkLink(link: Link) { + return ( + link.isInitialized() && + !( + this.idToNode.get(link.source.id) === undefined || + this.idToNode.get(link.target.id) === undefined + ) + ); + } + + public addNode(node: Node) { + node.id = this.nodes.length; + + if (!this.checkNode(node)) { + return false; + } + + this.nodes.push(node); + this.idToNode.set(node.id, node); + return true; + } + + public addLink(link: Link) { + link.id = this.links.length; + + if (!this.checkLink(link)) { + return false; + } + + this.links.push(link); + this.idToLink.set(link.id, link); + return true; + } + + public deleteLink(id: number) { + this.links = this.links.filter((l: Link) => l.id !== id); + this.idToLink.delete(id); + } + + public deleteNode(id: number) { + const node = this.idToNode.get(id); + this.idToNode.delete(id); + + for (const link of node.links) { + this.deleteLink(link.id); + } + this.nodes = this.nodes.filter((n: Node) => n.id !== id); + } + + public deleteNodeType(id: string) { + // TODO: Change to id/number + const nodeType = this.nameToObjectGroup.get(id); + + for (const node of this.nodes) { + if (node.type.id === nodeType.id) { + node.type = undefined; + } } - return objectGroups; } public view( @@ -165,23 +294,10 @@ export default class Graph implements GraphContent { nodeTypes.get(l.target.type.name) ); - // Convert to data objects and create new graph. - // Using spread syntax to simplify object copying. - return new Graph( - nodes.map((node) => { - // eslint-disable-next-line no-unused-vars - const { type, neighbors, links, ...nodeVars } = node; - const nodeData = { ...nodeVars } as NodeData; - nodeData.type = type.name; - return nodeData; - }), - links.map((link) => { - return { - source: link.source.id, - target: link.target.id, - }; - }), - this.objectGroups - ); + return new Graph({ + nodes: nodes, + links: links, + objectGroups: this.objectGroups, + }); } } diff --git a/src/common/graphelement.ts b/src/common/graphelement.ts new file mode 100644 index 0000000..199107a --- /dev/null +++ b/src/common/graphelement.ts @@ -0,0 +1,38 @@ +import { Graph } from "./graph"; +import { SerializableItem } from "./serializableitem"; + +export class GraphElement<JSONType, HistoryType> extends SerializableItem< + JSONType, + HistoryType +> { + public graph: Graph; + + constructor(id = -1, graph: Graph = undefined) { + super(id); + this.graph = graph; + } + + /** + * Removes element from its parent graph. + * @returns True, if successful. + */ + public delete() { + throw new Error('Function "delete()" has not been implemented.'); + } + + public isInitialized(): boolean { + return this.id != -1; + } + + public toString(): string { + throw new Error('Function "toString()" has not been implemented.'); + } + + /** + * Compares two objects. Can be a custom implementation. + * @returns True, if given object is identical. + */ + public equals(other: GraphElement<JSONType, HistoryType>): boolean { + return other.constructor == this.constructor && other.id == this.id; + } +} diff --git a/src/common/history.ts b/src/common/history.ts new file mode 100644 index 0000000..966489b --- /dev/null +++ b/src/common/history.ts @@ -0,0 +1,59 @@ +import { SerializableItem } from "./serializableitem"; + +interface SavePoint<DataType> { + description: string; + data: DataType; +} + +export class History<HistoryDataType> { + public maxCheckpoints: number; + public currentCheckpoint: number; + + private data: SerializableItem<never, HistoryDataType>; + private checkpoints: SavePoint<HistoryDataType>[]; + + constructor( + data: SerializableItem<never, HistoryDataType>, + maxCheckpoints = 20 + ) { + this.data = data; + this.maxCheckpoints = maxCheckpoints; + this.checkpoints = []; + this.currentCheckpoint = -1; + this.checkpoint("New History"); + } + + checkpoint(description: string) { + const checkpointData = this.data.toHistorySerializableObject(); + const checkpoint = { + description: description, + data: JSON.parse(JSON.stringify(checkpointData)), // deepcopy + }; + + // Remove potential history which is not relevant anymore (maybe caused by undo ops) + this.currentCheckpoint++; + this.checkpoints.length = this.currentCheckpoint; + + this.checkpoints.push(checkpoint); + } + + historyDescription(): Array<string> { + return this.checkpoints.map((savepoint) => savepoint.description); + } + + undo(): SavePoint<HistoryDataType> { + if (this.currentCheckpoint > 0) { + return this.checkpoints[this.currentCheckpoint--]; + } else { + return this.checkpoints[0]; + } + } + + redo(): SavePoint<HistoryDataType> { + if (this.currentCheckpoint < this.checkpoints.length) { + return this.checkpoints[this.currentCheckpoint++]; + } else { + return this.checkpoints[this.checkpoints.length - 1]; + } + } +} diff --git a/src/common/link.ts b/src/common/link.ts new file mode 100644 index 0000000..78d6324 --- /dev/null +++ b/src/common/link.ts @@ -0,0 +1,105 @@ +import { GraphElement } from "./graphelement"; +import { Node } from "./node"; +import { NodeType } from "./nodetype"; +import { Graph } from "./graph"; + +export interface LinkData { + source: number; + target: number; + type?: string; +} + +export interface SimLinkData extends LinkData { + index: number; +} + +export interface GraphLink { + id: number; + source: Node; + target: Node; + type?: NodeType; + + // Properties used by the force graph simulation + index?: number; +} + +export class Link + extends GraphElement<LinkData, SimLinkData> + implements GraphLink +{ + public source: Node; + public target: Node; + + type?: NodeType; + + // These parameters will be added by the force graph implementation + public index?: number; + + constructor(graph: Graph = undefined) { + super(0, graph); + } + + /** + * Id of the source node. + * @returns Source id. + */ + public get sourceId(): number { + return this.source.id; + } + + /** + * Id of the target node. + * @returns Target id. + */ + public get targetId(): number { + return this.target.id; + } + + public delete() { + return this.graph.deleteLink(this.id); + } + + /** + * Determines if the given node is part of the link structure. + * @param node Node to check for. + * @returns True, if node is either source or target node of link. + */ + public contains(node: Node): boolean { + return this.source === node || this.target === node; + } + + public toJSONSerializableObject(): LinkData { + return { + source: this.source.id, + target: this.target.id, + }; + } + + public toHistorySerializableObject(): SimLinkData { + return { ...this.toJSONSerializableObject(), index: this.index }; + } + + public toString(): string { + return this.source.toString() + " -> " + this.target.toString(); + } + + public isInitialized(): boolean { + return ( + super.isInitialized() && + this.source != undefined && + this.target != undefined + ); + } + + public equals(other: GraphElement<LinkData, SimLinkData>): boolean { + if (other.constructor != this.constructor) { + return false; + } + + const link = other as Link; + + return ( + link.sourceId === this.sourceId && link.targetId === this.targetId + ); + } +} diff --git a/src/common/node.ts b/src/common/node.ts new file mode 100644 index 0000000..076293d --- /dev/null +++ b/src/common/node.ts @@ -0,0 +1,177 @@ +import { Graph } from "./graph"; +import { GraphElement } from "./graphelement"; +import { NodeType } from "./nodetype"; +import { Link } from "./link"; + +interface NodeProperties { + name: string; + description?: string; + icon?: string; + banner?: string; + video?: string; + references?: string[]; +} + +export interface NodeData extends NodeProperties { + /** + * This interfaces provides a data representation for a simple "flat" node without object pointers. + * Can be used to store nodes in JSON format. + */ + id: number; + type?: string; +} + +// Based on https://github.com/d3/d3-force#simulation_nodes +export interface SimNodeData extends NodeData { + /** + * This interface serves as a data representation for the history of the editor. + * Same as the JSON representation + additional parameters related to the simulation. + * This ensures that nodes from the history can are restored with the same visual state. + */ + index: number; + x: number; + y: number; + vx: number; + vy: number; + fx: number; + fy: number; +} + +export interface GraphNode extends NodeProperties { + /** + * Node representation in a Graph. Contains values for easy traversal and force graph simulation properties. + */ + id: number; + type: NodeType; + + // Properties used for graph traversal + neighbors: Node[]; + links: Link[]; + + // Properties used by the force graph simulation + index?: number; + x?: number; + y?: number; + vx?: number; + vy?: number; + fx?: number; + fy?: number; +} + +export class Node + extends GraphElement<NodeData, SimNodeData> + implements GraphNode +{ + public name: string; + public description: string; + public type: NodeType; + public icon: string; + public banner: string; + public video: string; + public references: string[]; + + public neighbors: Node[]; + public links: Link[]; + + // These parameters will be added by the force graph implementation + public index?: number; + public x?: number; + public y?: number; + public vx?: number; + public vy?: number; + public fx?: number; + public fy?: number; + + constructor(graph: Graph = undefined) { + super(0, graph); + this.neighbors = []; + this.links = []; + } + + public setType(typeId: number) { + // Is it even different? + if (this.type.id === typeId) { + return; + } + + const newType = this.graph.getType(typeId); + + // Exists? + if (newType === undefined) { + return; + } + + this.type = newType; + + // Store change + this.graph.storeCurrentData( + "Set type [" + + newType.toString() + + "] for [" + + this.toString() + + "]" + ); + } + + public delete() { + return this.graph.deleteNode(this.id); + } + + /** + * Connects this node to a given node. Only works if they are in the same graph. + * @param node Other node to connect. + * @returns The created link, if successful, otherwise undefined. + */ + public connect(node: Node): Link { + if (this.graph !== node.graph) { + throw new Error("The connected nodes are not on the same graph!"); + } + + const link = new Link(this.graph); + + link.source = this; + link.target = node; + + if (this.graph.addLink(link)) { + this.neighbors.push(node); + node.neighbors.push(this); + return link; + } + } + + public toJSONSerializableObject(): NodeData { + return { + id: this.id, + name: this.name, + description: this.description, + icon: this.icon, + banner: this.banner, + video: this.video, + references: this.references, + type: this.type.name, + }; + } + + public toHistorySerializableObject(): SimNodeData { + return { + ...this.toJSONSerializableObject(), + index: this.index, + x: this.x, + y: this.y, + vx: this.vx, + vy: this.vy, + fx: this.fx, + fy: this.fy, + }; + } + + public fromSerializedObject(data: NodeData | SimNodeData): Node { + Object.assign(this, data); + this.type = undefined; // Remove string type again if undefined as it may be a string. + return this; + } + + public toString(): string { + return this.name; + } +} diff --git a/src/common/nodetype.ts b/src/common/nodetype.ts new file mode 100644 index 0000000..c160ec0 --- /dev/null +++ b/src/common/nodetype.ts @@ -0,0 +1,47 @@ +import { GraphElement } from "./graphelement"; + +export interface NodeTypeData { + id: number; + name: string; + color?: string; +} + +export class NodeType + extends GraphElement<NodeTypeData, NodeTypeData> + implements NodeTypeData +{ + public id: number; + public name: string; + public color: string; + + toJSONSerializableObject(): NodeTypeData { + return { id: this.id, name: this.name, color: this.color }; + } + + toHistorySerializableObject(): NodeTypeData { + return this.toJSONSerializableObject(); + } + + public fromSerializedObject(data: NodeTypeData): NodeType { + Object.assign(this, data); + return this; + } + + public delete() { + return this.graph.deleteNodeType(this.name); // TODO: Change to id + } + + public toString(): string { + return this.name; + } + + public equals(other: GraphElement<NodeTypeData, NodeTypeData>): boolean { + if (other.constructor != this.constructor) { + return false; + } + + const type = other as NodeType; + + return type.id === this.id; + } +} diff --git a/src/common/serializableitem.ts b/src/common/serializableitem.ts new file mode 100644 index 0000000..c726c27 --- /dev/null +++ b/src/common/serializableitem.ts @@ -0,0 +1,30 @@ +/** + * Provides the basic interface for unique, serializable objects. + */ +export class SerializableItem<JSONType, HistoryType> { + public id: number; // Serialized objects need to be unique. + + constructor(id = 0) { + this.id = id; + } + + public toJSONSerializableObject(): JSONType { + throw new Error( + "Method 'toJSONSerializableObject()' is not implemented." + ); + } + + public toHistorySerializableObject(): HistoryType { + throw new Error( + "Method 'toHistorySerializableObject()' is not implemented." + ); + } + + public fromSerializedObject( + data: JSONType | HistoryType + ): SerializableItem<JSONType, HistoryType> { + throw new Error( + "Method 'fromSerializableObject()' is not implemented." + ); + } +} diff --git a/src/display/components/nodefilter/filtermenu.tsx b/src/display/components/nodefilter/filtermenu.tsx index f38c8af..6c64b53 100644 --- a/src/display/components/nodefilter/filtermenu.tsx +++ b/src/display/components/nodefilter/filtermenu.tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; import "./filtermenu.css"; import Label from "./label"; -import { GraphObjectType } from "../../../common/graph"; +import { NodeTypeData } from "../../../common/graph"; interface FilterMenuProps { classes: Map<string, GraphObjectType>; diff --git a/src/display/components/nodeinfo/neighbors.tsx b/src/display/components/nodeinfo/neighbors.tsx index 5a899cc..760ebdc 100644 --- a/src/display/components/nodeinfo/neighbors.tsx +++ b/src/display/components/nodeinfo/neighbors.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { GraphObjectType, Node } from "../../../common/graph"; +import { NodeTypeData, Node } from "../../../common/graph"; import FancyScrollbar from "../fancyscrollbar"; import Collapsible from "../collapsible"; diff --git a/src/display/components/nodeinfo/nodeinfobar.tsx b/src/display/components/nodeinfo/nodeinfobar.tsx index 4965e73..ffde37c 100644 --- a/src/display/components/nodeinfo/nodeinfobar.tsx +++ b/src/display/components/nodeinfo/nodeinfobar.tsx @@ -1,7 +1,7 @@ import React from "react"; import "./nodeinfobar.css"; -import { GraphObjectType, Node } from "../../../common/graph"; +import { NodeTypeData, Node } from "../../../common/graph"; import TitleArea from "./titlearea"; import FancyScrollbar from "../fancyscrollbar"; import MediaArea from "./mediaarea"; diff --git a/src/display/display.tsx b/src/display/display.tsx index 992a73e..44e5c6d 100644 --- a/src/display/display.tsx +++ b/src/display/display.tsx @@ -5,7 +5,7 @@ import PropTypes, { InferType } from "prop-types"; import "./display.css"; import { GraphNode, GraphRenderer } from "./renderer"; import * as Helpers from "./helpers"; -import Graph, { Node } from "../common/graph"; +import { Graph, Node } from "../common/graph"; import { loadGraphJson } from "../common/datasets"; import NodeInfoBar from "./components/nodeinfo/nodeinfobar"; import FilterMenu from "./components/nodefilter/filtermenu"; @@ -63,7 +63,7 @@ class Display extends React.Component< const fetchGraph = async () => { const graphData = await loadGraphJson(this.props.spaceId); - this.graph = new Graph(graphData.nodes, graphData.links); + this.graph = Graph.fromSerializedObject(graphData); // console.log(this.graph); this.setState({ graph: this.graph }); }; diff --git a/src/display/renderer.tsx b/src/display/renderer.tsx index 294868e..7759238 100644 --- a/src/display/renderer.tsx +++ b/src/display/renderer.tsx @@ -10,7 +10,7 @@ import React from "react"; import PropTypes, { InferType } from "prop-types"; import SpriteText from "three-spritetext"; import { Object3D, Sprite } from "three"; -import Graph, { Coordinate, Link, Node } from "../common/graph"; +import { Graph, Coordinate, Link, Node } from "../common/graph"; export interface GraphNode extends Node { x: number; diff --git a/src/editor/js/components/editor.tsx b/src/editor/js/components/editor.tsx index cbabc02..bb125ec 100644 --- a/src/editor/js/components/editor.tsx +++ b/src/editor/js/components/editor.tsx @@ -5,10 +5,10 @@ import { NodeDetails } from "./nodedetails"; import { SpaceSelect } from "./spaceselect"; import "./editor.css"; import { ForceGraph2D } from "react-force-graph"; -import { Node } from "../structures/graph/node"; +import { Node } from "../../../common/node"; import { HistoryNavigator } from "./historynavigator"; -import { GraphElement } from "../structures/graph/graphelement"; -import { Link } from "../structures/graph/link"; +import { GraphElement } from "../../../common/graphelement"; +import { Link } from "../../../common/link"; import { NodeTypesEditor } from "./nodetypeseditor"; import { SpaceManager } from "./spacemanager"; diff --git a/src/editor/js/components/nodedetails.tsx b/src/editor/js/components/nodedetails.tsx index 2a619ab..b4d1f32 100644 --- a/src/editor/js/components/nodedetails.tsx +++ b/src/editor/js/components/nodedetails.tsx @@ -1,7 +1,7 @@ import React from "react"; import { ReactNode } from "react"; -import { Node } from "../structures/graph/node"; -import { NodeType } from "../structures/graph/nodetype"; +import { Node } from "../../../common/node"; +import { NodeType } from "../../../common/nodetype"; import "./nodedetails.css"; type propTypes = { diff --git a/src/editor/js/components/nodetypeentry.tsx b/src/editor/js/components/nodetypeentry.tsx index a5a1cc3..997142e 100644 --- a/src/editor/js/components/nodetypeentry.tsx +++ b/src/editor/js/components/nodetypeentry.tsx @@ -1,7 +1,7 @@ import React from "react"; import { ReactNode } from "react"; import { Graph } from "../structures/graph/graph"; -import { NodeType } from "../structures/graph/nodetype"; +import { NodeType } from "../../../common/nodetype"; import "./nodetypeentry.css"; type propTypes = { diff --git a/src/editor/js/components/nodetypeseditor.tsx b/src/editor/js/components/nodetypeseditor.tsx index b649a1d..4028281 100644 --- a/src/editor/js/components/nodetypeseditor.tsx +++ b/src/editor/js/components/nodetypeseditor.tsx @@ -3,7 +3,7 @@ import { ReactNode } from "react"; import { Graph } from "../structures/graph/graph"; import "./nodetypeseditor.css"; import { NodeTypeEntry } from "./nodetypeentry"; -import { NodeType } from "../structures/graph/nodetype"; +import { NodeType } from "../../../common/nodetype"; type propTypes = { graph: Graph; diff --git a/src/editor/js/structures/graph/graph.ts b/src/editor/js/structures/graph/graph.ts index a164292..c393bfb 100644 --- a/src/editor/js/structures/graph/graph.ts +++ b/src/editor/js/structures/graph/graph.ts @@ -1,27 +1,28 @@ -import ManagedData from "../manageddata"; -import { Link } from "./link"; -import { NodeType } from "./nodetype"; -import { Node } from "./node"; -import { GLOBAL_PARAMS } from "../helper/serializableitem"; -import { GraphElement } from "./graphelement"; +import { Link } from "../../../../common/link"; +import { NodeType } from "../../../../common/nodetype"; +import { Node } from "../../../../common/node"; +import { GraphElement } from "../../../../common/graphelement"; +import * as Common from "../../../../common/graph"; +import { History } from "../../../../common/history"; +import { GraphContent } from "../../../../common/graph"; const GRAPH_PARAMS = [...GLOBAL_PARAMS]; const GRAPH_DATA_PARAMS = ["nodes", "links", "types"]; -export type GraphData = { nodes: Node[]; links: Link[]; types: NodeType[] }; +// export type GraphData = { nodes: Node[]; links: Link[]; types: NodeType[] }; -export class Graph extends ManagedData { - public data: GraphData; +export class DynamicGraph extends Common.Graph { + private history: History<Common.Graph>; private nextNodeId = 0; private nextLinkId = 0; private nextTypeId = 0; // Callbacks - public onChangeCallbacks: { (data: GraphData): void }[]; + public onChangeCallbacks: { (data: GraphContent): void }[]; - constructor(data: GraphData) { - super(data); + constructor(data: GraphContent) { + super(); this.onChangeCallbacks = []; this.connectElementsToGraph(this.data); @@ -239,7 +240,9 @@ export class Graph extends ManagedData { return true; // Doesn't even exist in graph to begin with. } - this.data.types = this.data.types.filter((n: NodeType) => !n.equals(nodeType)); + this.data.types = this.data.types.filter( + (n: NodeType) => !n.equals(nodeType) + ); try { // No save points should be created when replacing usages @@ -295,14 +298,22 @@ export class Graph extends ManagedData { return true; } - getLink(sourceId: number, targetId: number, directionSensitive = true): Link { + getLink( + sourceId: number, + targetId: number, + directionSensitive = true + ): Link { return this.links.find((l) => { if (l.sourceId === sourceId && l.targetId === targetId) { return true; } // Check other direction if allowed - if (!directionSensitive && (l.sourceId === targetId && l.targetId === sourceId)) { + if ( + !directionSensitive && + l.sourceId === targetId && + l.targetId === sourceId + ) { return true; } @@ -458,8 +469,8 @@ export class Graph extends ManagedData { // TODO: Remove, when types are directly parsed and not just implicit data.nodes.forEach((node) => { - const sharedType: NodeType = data.types.find((type) => - type.name === node.type.name || type.equals(node.type) + const sharedType: NodeType = data.types.find( + (type) => type.name === node.type.name || type.equals(node.type) ); if (sharedType !== undefined) { diff --git a/src/editor/js/structures/graph/graphelement.ts b/src/editor/js/structures/graph/graphelement.ts deleted file mode 100644 index 6a572b9..0000000 --- a/src/editor/js/structures/graph/graphelement.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { Graph } from "./graph"; -import { SerializableItem } from "../helper/serializableitem"; - -export class GraphElement extends SerializableItem { - protected isNode: boolean; - protected isLink: boolean; - - public graph: Graph; - - constructor(graph: Graph = undefined) { - super(); - this.graph = graph; - this.isNode = false; - this.isLink = false; - - this.equals = this.equals.bind(this); - } - - public get node(): boolean { - return this.isNode; - } - - public get link(): boolean { - return this.isLink; - } - - /** - * Removes element from its parent graph. - * @returns True, if successful. - */ - public delete(): boolean { - throw new Error('Function "delete()" has not been implemented.'); - } - - /** - * Adds the element to the given graph. - * @param graph Graph to add element to. - * @returns True, if successful. - */ - public add(graph: Graph = this.graph): boolean { - throw new Error('Function "add(graph)" has not been implemented.'); - } - - /** - * Needs to be implemented to create a filtered version for storing in the data history. - * @returns Filtered object. - */ - public getCleanInstance(): any { - throw new Error( - 'Function "getCleanInstance()" has not been implemented.' - ); - } - - /** - * Compares to objects. Can be a custom implementation. - * @returns True, if given object is identical. - */ - public equals(other: GraphElement): boolean { - return ( - other.node == this.node && - other.link == this.link && - other.id == this.id - ); - } -} diff --git a/src/editor/js/structures/graph/link.ts b/src/editor/js/structures/graph/link.ts deleted file mode 100644 index e8f7dfc..0000000 --- a/src/editor/js/structures/graph/link.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { GraphElement } from "./graphelement"; -import { Graph } from "./graph"; -import { Node } from "./node"; -import { GLOBAL_PARAMS } from "../helper/serializableitem"; -import * as Common from "../../../../common/graph"; - -const LINK_PARAMS = ["source", "target", ...GLOBAL_PARAMS]; -const LINK_SIM_PARAMS = ["index"]; - -export class Link extends GraphElement implements Common.Link { - public source: Node; - public target: Node; - - private _sourceId: number; - private _targetId: number; - - constructor(graph: Graph = undefined) { - super(graph); - this.isLink = true; - } - - /** - * Id of the source node. - * @returns Source id. - */ - public get sourceId(): number { - if (this.source == undefined) { - return this._sourceId; - } - - return this.source.id; - } - - /** - * Removes stored node object and just saves the id instead. - * @param value New source id value. - */ - public set sourceId(value: number) { - this._sourceId = value; - this.source = undefined; - } - - /** - * Id of the target node. - * @returns Target id. - */ - public get targetId(): number { - if (this.target == undefined) { - return this._targetId; - } - - return this.target.id; - } - - /** - * Removes stored node object and just saves the id instead. - * @param value New target id value. - */ - public set targetId(value: number) { - this._targetId = value; - this.target = undefined; - } - - public delete() { - return this.graph.deleteLink(this); - } - - public add(graph: Graph = this.graph) { - this.graph = graph; - if (this.graph == undefined) { - return false; - } - - return this.graph.addLink(this); - } - - /** - * Determines if the given node is part of the link structure. - * @param node Node to check for. - * @returns True, if node is either source or target node of link. - */ - public contains(node: Node): boolean { - return this.source === node || this.target === node; - } - - public serialize(): any { - return this.serializeProperties(LINK_PARAMS); - } - - public getCleanInstance(): any { - return { - ...this.serialize(), - ...this.serializeProperties(LINK_SIM_PARAMS), - }; - } - - public static parse(raw: any): Link { - const link: Link = new Link(); - - if (isNaN(Number(raw.source))) { - // Source not given as id, but probably as node object - link.sourceId = raw.source.id; - link.targetId = raw.target.id; - } else { - link.sourceId = Number(raw.source); - link.targetId = Number(raw.target); - } - - // Try to parse simulation parameters - LINK_SIM_PARAMS.forEach((param) => { - if (raw[param] === undefined) { - return; - } - - (link as any)[param] = raw[param]; - }); - - return link; - } - - public toString(): string { - let source: any = this.source; - let target: any = this.target; - - if (this.source == undefined || this.target == undefined) { - source = this.sourceId; - target = this.targetId; - } - - return source.toString() + " -> " + target.toString(); - } - - public equals(other: GraphElement): boolean { - if (!other.link || other.node) { - return false; - } - - const link = other as Link; - - return ( - link.sourceId === this.sourceId && link.targetId === this.targetId - ); - } -} diff --git a/src/editor/js/structures/graph/node.ts b/src/editor/js/structures/graph/node.ts deleted file mode 100644 index dfa16a2..0000000 --- a/src/editor/js/structures/graph/node.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { Graph } from "./graph"; -import { GraphElement } from "./graphelement"; -import { NodeType } from "./nodetype"; -import { Link } from "./link"; -import { GLOBAL_PARAMS } from "../helper/serializableitem"; -import * as Common from "../../../../common/graph"; - -const NODE_PARAMS = [ - "name", - "icon", - "description", - "references", - "video", - "type", - "banner", - ...GLOBAL_PARAMS, -]; -const NODE_SIM_PARAMS = ["index", "x", "y", "vx", "vy", "fx", "fy"]; // Based on https://github.com/d3/d3-force#simulation_nodes - -export class Node extends GraphElement implements Common.Node { - public name: string; - public description: string; - public type: NodeType; - public icon: string; - public banner: string; - public video: string; - public references: string[]; - - constructor(graph: Graph = undefined) { - super(graph); - this.isNode = true; - } - - public setType(typeId: number) { - // Is it even different? - if (this.type.id === typeId) { - return; - } - - const newType = this.graph.getType(typeId); - - // Exists? - if (newType === undefined) { - return; - } - - this.type = newType; - - // Store change - this.graph.storeCurrentData( - "Set type [" + - newType.toString() + - "] for [" + - this.toString() + - "]" - ); - } - - public delete() { - return this.graph.deleteNode(this); - } - - public add(graph: Graph = this.graph) { - this.graph = graph; - if (this.graph == undefined) { - return false; - } - - return this.graph.addNode(this); - } - - /** - * Calculates a list of all connected links to the current node. - * @returns Array containing all connected links. - */ - public get links(): Link[] { - const links: Link[] = []; - - this.graph.links.forEach((link) => { - if (link.contains(this)) { - links.push(link); - } - }); - - return links; - } - - /** - * Calculates a list of all connected nodes to the current node. - * @returns Array containing all connected nodes. - */ - public get neighbors(): Node[] { - const nodes: Node[] = []; - - this.links.forEach((link) => { - // Find "other" node - let otherNode = link.source; - if (this.equals(otherNode)) { - otherNode = link.target; - } - - // Still undefined? - if (otherNode == undefined) { - // Link apparently not properly set up - return; - } - - // Add to list if doesn't exist - if (!nodes.includes(otherNode)) { - nodes.push(otherNode); - } - }); - - return nodes; - } - - /** - * Connects a given node to itself. Only works if they are in the same graph. - * @param node Other node to connect. - * @returns The created link, if successful, otherwise undefined. - */ - public connect(node: Node): Link { - if (this.graph !== node.graph) { - return undefined; - } - - const link = new Link(this.graph); - - link.source = this; - link.target = node; - - if (link.add()) { - return link; - } - } - - public serialize(): any { - return this.serializeProperties(NODE_PARAMS); - } - - public getCleanInstance(): any { - return { - ...this.serialize(), - ...this.serializeProperties(NODE_SIM_PARAMS), - }; - } - - public static parse(raw: any): Node { - const node: Node = new Node(); - - if (raw.label !== undefined) { - node.name = raw.label; - } else { - node.name = raw.name; - } - - node.id = Number(raw.id); - node.description = raw.description; - node.type = NodeType.parse(raw.type); - - // Defaults - node.icon = raw.icon ? raw.icon : ""; - node.banner = raw.banner ? raw.banner : ""; - node.video = raw.video ? raw.video : ""; - node.references = raw.references ? raw.references : []; - - // Try to parse simulation parameters - NODE_SIM_PARAMS.forEach((param) => { - if (raw[param] === undefined) { - return; - } - - (node as any)[param] = raw[param]; - }); - - return node; - } - - public toString(): string { - return this.name; - } -} diff --git a/src/editor/js/structures/graph/nodetype.ts b/src/editor/js/structures/graph/nodetype.ts deleted file mode 100644 index 83f026b..0000000 --- a/src/editor/js/structures/graph/nodetype.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { GLOBAL_PARAMS } from "../helper/serializableitem"; -import { Graph } from "./graph"; -import { GraphElement } from "./graphelement"; -import * as Common from "../../../../common/graph"; - -const NODE_TYPE_PARAMS = ["name", "color", ...GLOBAL_PARAMS]; - -export class NodeType extends GraphElement implements Common.GraphObjectType { - public name: string; - public color: string; - - serialize(): any { - return this.serializeProperties(NODE_TYPE_PARAMS); - } - - public delete(): boolean { - return this.graph.deleteNodeType(this); - } - - public add(graph: Graph = this.graph): boolean { - this.graph = graph; - if (this.graph == undefined) { - return false; - } - - return this.graph.addNodeType(this); - } - - public getCleanInstance(): any { - return this.serialize(); - } - - public static parse(raw: any): NodeType { - const type: NodeType = new NodeType(); - - if (typeof raw === "string" || raw instanceof String) { - type.name = raw as string; - type.color = "#ff0000"; - } else { - type.name = raw.name; - type.color = raw.color; - } - - return type; - } - - public toString(): string { - return this.name; - } - - public equals(other: GraphElement): boolean { - if (other.link || other.node) { - return false; - } - - const type = other as NodeType; - - return type.id === this.id; - } -} diff --git a/src/editor/js/structures/helper/serializableitem.ts b/src/editor/js/structures/helper/serializableitem.ts deleted file mode 100644 index a5e2313..0000000 --- a/src/editor/js/structures/helper/serializableitem.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Provides the basic interface for unique, serializable objects. - */ -import {array} from "prop-types"; - -export const GLOBAL_PARAMS = ["id"]; - -export class SerializableItem { - public id: number; // Serialized objects need to be unique. - - /** - * Returns the current object in its serialized form. - * @returns The serialized object. - */ - public serialize(): any { - throw new Error("Method 'serialize()' must be implemented."); - } - - /** - * Creates the current object based on raw, serialized data. - * @param raw The serialized data. - * @returns Parsed data in final form. Could be the finalisd object. - */ - public static parse(raw: any): any { - throw new Error("Method 'parse()' must be implemented."); - } - - /** - * A generic way to create a new object with all the desired parameters of the current one. - * @param params List of parameters to include in the new object. - * @param data The data object to be serialized. The current object by default. - * @protected - * @returns New object containing all the desired properties. - */ - protected serializeProperties(params: string[], data: any = this): any { - const serialized: any = {}; - - params.forEach((param) => { - serialized[param] = this.serializeItem((this as any)[param]); - }); - - return serialized; - } - - /** - * Recursively serializes an object. Handles serializable items and lists properly. - * @param value Object to be serialized. - * @private - * @returns Serialized item. - */ - private serializeItem(value: any): any { - if (value instanceof SerializableItem) { - // If is also serializable, use the serialized form - return value.serialize(); - } else if (value instanceof array) { - // If is some kind of list, convert the objects in the list recursively - const serializedList: any = []; - (value as []).forEach((item) => { - serializedList.push(this.serializeItem(item)); - }) - return serializedList; - } else { - return value; - } - } -} diff --git a/src/editor/js/structures/manageddata.ts b/src/editor/js/structures/manageddata.ts index 65c6eeb..1f775d4 100644 --- a/src/editor/js/structures/manageddata.ts +++ b/src/editor/js/structures/manageddata.ts @@ -1,40 +1,41 @@ -import { SerializableItem } from "./helper/serializableitem"; +import { SerializableItem } from "../../../common/serializableitem"; const SAVE_BUTTON_ID = "div#ks-editor #toolbar-save"; -type SavePoint = { - description: string; - data: string; +interface SavePoint<DataType> { id: number; -}; + description: string; + data: DataType; +} /** * Allows objects to have undo/redo functionality in their data and custom save points. */ -export default class ManagedData extends SerializableItem { - public data: any; // The data to be stored in a history. - public history: SavePoint[]; // All save points of the data. +export default class ManagedData<HistoryDataType> { + public data: SerializableItem<never, HistoryDataType>; // Object that will be serialized to history on save. + public history: SavePoint<HistoryDataType>[]; // All save points of the data. public historyPosition: number; // Currently selected save point in history. Latest always at index 0. private savedHistoryId: number; // Id of save point that is considered saved. private storingEnabled: boolean; // To internally disable saving of objects on save call. + public hasNewData: boolean; // True if there is new data to save in the history /** * Sets initial states. * @param data Initial state of data to be stored. */ - constructor(data: any) { - super(); + constructor(data: SerializableItem<never, HistoryDataType>) { this.data = data; this.history = []; // Newest state is always at 0 this.historyPosition = 0; this.savedHistoryId = 0; this.storingEnabled = true; + this.hasNewData = false; } /** * @returns SavePoint of current history position. Gives access to meta data of current data. */ - public get currentSavePoint(): SavePoint { + public get currentSavePoint(): SavePoint<HistoryDataType> { return this.history[this.historyPosition]; } @@ -43,11 +44,12 @@ export default class ManagedData extends SerializableItem { * @private */ private updateUnsavedChangesHandler() { - if (this.hasUnsavedChanges()) { - jQuery(SAVE_BUTTON_ID).removeClass("hidden"); + // TODO: Remove jQuery! + if (this.hasNewData) { + //jQuery(SAVE_BUTTON_ID).removeClass("hidden"); window.addEventListener("beforeunload", this.handleBeforeUnload); } else { - jQuery(SAVE_BUTTON_ID).addClass("hidden"); + //jQuery(SAVE_BUTTON_ID).addClass("hidden"); window.removeEventListener("beforeunload", this.handleBeforeUnload); } } @@ -58,6 +60,7 @@ export default class ManagedData extends SerializableItem { * @private */ private handleBeforeUnload(e: any) { + // TODO: Remove any and deprecated window.event check const confirmationMessage = "If you leave before saving, unsaved changes will be lost."; @@ -65,21 +68,11 @@ export default class ManagedData extends SerializableItem { return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc. } - /** - * Returns true, if data has unsaved changes. - */ - public hasUnsavedChanges(): boolean { - if (this.history[this.historyPosition] === undefined) { - return this.data !== undefined; - } - - return this.history[this.historyPosition].id !== this.savedHistoryId; - } - /** * Internally marks the current save point as saved. */ - public markChangesAsSaved() { + private markChangesAsSaved() { + // TODO: Remove if unessesary. Changed accessor from public to private this.savedHistoryId = this.history[this.historyPosition].id; this.updateUnsavedChangesHandler(); } @@ -98,20 +91,6 @@ export default class ManagedData extends SerializableItem { this.storingEnabled = true; } - /** - * Event triggered after undo. - */ - protected onUndo() { - // No base implementation. - } - - /** - * Event triggered after redo. - */ - protected onRedo() { - // No base implementation. - } - /** * Go to one step back in the stored history, if available. * @returns True, if successful. @@ -119,7 +98,6 @@ export default class ManagedData extends SerializableItem { public undo(): boolean { if (this.step(1)) { this.updateUnsavedChangesHandler(); - this.onUndo(); return true; } else { return false; @@ -133,7 +111,6 @@ export default class ManagedData extends SerializableItem { public redo(): boolean { if (this.step(-1)) { this.updateUnsavedChangesHandler(); - this.onRedo(); return true; } else { return false; -- GitLab