import { Link } from "../common/graph/link"; import { NodeType } from "../common/graph/nodetype"; import { Node, NodeData, SimNodeData } from "../common/graph/node"; import * as Common from "../common/graph/graph"; import { History } from "../common/history"; import { GraphContent, GraphData, SimGraphData } from "../common/graph/graph"; export class DynamicGraph extends Common.Graph { public history: History<SimGraphData>; // Callbacks public onChangeCallbacks: { (data: DynamicGraph): void }[]; constructor(data?: GraphContent) { super(data); this.onChangeCallbacks = []; super.deleteNode = super.deleteNode.bind(this); super.deleteLink = super.deleteLink.bind(this); super.deleteNodeType = super.deleteNodeType.bind(this); if (data != undefined) { this.history = new History<SimGraphData>( this, 20, "Created new graph." ); } } public fromSerializedObject(data: GraphData | SimGraphData): DynamicGraph { super.fromSerializedObject(data); this.history = new History<SimGraphData>( this, 20, "Created new graph." ); return this; } /** * Calls all registered callbacks for the onChange event. * @private */ private triggerOnChange() { this.onChangeCallbacks.forEach((fn) => fn(this)); } /** * Triggers change event on data-redo. */ protected onRedo() { if (this.history.hasRedoCheckpoints()) { const checkpoint = this.history.redo(); this.fromSerializedObject(checkpoint.data); this.triggerOnChange(); } } /** * Triggers change event on data-undo. */ protected onUndo() { if (this.history.hasUndoCheckpoints()) { const checkpoint = this.history.undo(); this.fromSerializedObject(checkpoint.data); this.triggerOnChange(); } } public createObjectGroup(name?: string, color?: string): NodeType { if (name == undefined) { name = "Unnamed"; } if (color == undefined) { color = "#000000"; } const objectGroup = super.createObjectGroup(name, color); this.triggerOnChange(); return objectGroup; } public createNode( data?: NodeData | SimNodeData, x?: number, y?: number, vx?: number, vy?: number ): Node { if (data == undefined) { data = { id: 0, name: "Undefined", type: this.objectGroups[0].name, // TODO: Change to id x: x, y: y, vx: vx, vy: vy, }; } return super.createNode(data); } private delete(id: string | number, fn: (id: string | number) => boolean) { if (fn(id)) { this.triggerOnChange(); return true; } return false; } public deleteNodeType(id: string): boolean { return this.delete(id, super.deleteNodeType); } public deleteNode(id: number): boolean { return this.delete(id, super.deleteNode); } public deleteLink(id: number): boolean { return this.delete(id, super.deleteLink); } 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 return ( !directionSensitive && l.sourceId === targetId && l.targetId === sourceId ); }); } public createLink(source: number, target: number): Link { const link = this.getLink(source, target, false); if (link !== undefined) { return link; // Already exists in graph. } return super.createLink(source, target); } /** * Goes over all nodes and finds the closest node based on distance. * @returns Closest node and distance. Undefined, if no closest node can be found. */ public getClosestNode( x: number, y: number, exclude?: Node ): { node: Node; distance: number; } { // Iterate over all nodes, keep the one with the shortest distance let closestDistance = Number.MAX_VALUE; let closestNode: Node = undefined; this.nodes.forEach((node) => { if (node.equals(exclude)) { return; // Don't compare to itself } const currentDistance = Math.hypot(x - node.x, y - node.y); if (closestDistance > currentDistance) { closestDistance = currentDistance; closestNode = node; } }); return { node: closestNode, distance: closestDistance }; } }