src/graph/builders/GraphBuilder.ts
import { DataFrame, DataObject, ReferenceSpace } from '../../data';
import { GraphNode } from '../_internal/GraphNode';
import { TimeUnit } from '../../utils';
import { GraphShape } from '../_internal/implementations/GraphShape';
import { PlaceholderNode } from '../../nodes/_internal/PlaceholderNode';
import { SinkNode } from '../../nodes/SinkNode';
import { SourceNode } from '../../nodes/SourceNode';
import { Edge } from '../Edge';
import { Graph } from '../Graph';
import { GraphValidator } from '../_internal/implementations';
import { Model } from '../../Model';
/**
* Graph builder
* @category Graph
*/
export class GraphBuilder<In extends DataFrame, Out extends DataFrame> {
public graph: GraphShape<In, Out>;
protected constructor(graph: GraphShape<In, Out> = new GraphShape()) {
this.graph = graph;
this.graph.name = 'graph';
}
public static create<In extends DataFrame, Out extends DataFrame>(): GraphBuilder<In, Out> {
return new GraphBuilder();
}
/**
* Event when graph is ready
* @param {string} name ready
* @param {Function} listener Event callback
*/
public on(name: 'ready', listener: (model: Model) => Promise<void> | void): this;
/**
* Event before building the graph
* @param {string} name prebuild
* @param {Function} listener Event callback
*/
public on(name: 'prebuild', listener: () => Promise<void> | void): this;
/**
* Event after building the graph
* @param {string} name postbuild
* @param {Function} listener Event callback
*/
public on(name: 'postbuild', listener: (model: GraphShape<any, any>) => Promise<void> | void): this;
public on(name: string | symbol, listener: (...args: any[]) => void): this {
this.graph.once(name, listener);
return this;
}
public from(...nodes: Array<GraphNode<any, any> | string>): GraphShapeBuilder<any> {
const selectedNodes: Array<GraphNode<any, any>> = [];
nodes.forEach((node: GraphNode<any, any> | string) => {
if (node === undefined) {
throw new Error('Undefined node was provided as a source!');
} else if (typeof node === 'string') {
let nodeObject = this.graph.findNodeByUID(node) || this.graph.findNodeByName(node);
if (nodeObject === undefined) {
// Add a placeholder
nodeObject = new PlaceholderNode(node);
}
this.graph.addNode(nodeObject);
selectedNodes.push(nodeObject);
} else {
this.graph.addNode(node);
if (node instanceof SourceNode) {
this.graph.addEdge(new Edge(this.graph.internalSource, node));
}
selectedNodes.push(node);
}
});
return new GraphShapeBuilder(
this,
this.graph,
selectedNodes.length === 0 ? [this.graph.internalSource] : selectedNodes,
);
}
public addNode(node: GraphNode<any, any>): this {
this.graph.addNode(node);
return this;
}
public addEdge(edge: Edge<any>): this {
this.graph.addEdge(edge);
return this;
}
public deleteEdge(edge: Edge<any>): this {
this.graph.deleteEdge(edge);
return this;
}
public deleteNode(node: GraphNode<any, any>): this {
this.graph.deleteNode(node);
return this;
}
/**
* Add graph shape to graph
* @param {GraphBuilder | GraphShape} shape Graph builder or abstract graph
* @returns {GraphBuilder} Current graph builder instance
*/
public addShape(shape: GraphBuilder<any, any> | GraphShape<any, any>): this {
let graph: GraphShape<any, any>;
if (shape instanceof GraphBuilder) {
graph = shape.graph;
} else {
graph = shape;
}
// Add the graph node and edges
graph.nodes.forEach((node) => {
// Check if the node is a placeholder
if (node instanceof PlaceholderNode) {
// Try to find a node with the same uid/name as the placeholder node
const existingNode = this.graph.findNodeByUID(node.name) || this.graph.findNodeByName(node.name);
if (existingNode) {
// Edit the edges connected to this placeholder
const outputEdges = graph.edges.filter((edge) => edge.inputNode === node);
const inputEdges = graph.edges.filter((edge) => edge.outputNode === node);
outputEdges.map((edge) => (edge.inputNode = existingNode));
inputEdges.map((edge) => (edge.outputNode = existingNode));
this.addNode(existingNode);
} else {
// Add the node as a placeholder
this.addNode(node);
}
} else {
this.addNode(node);
}
});
graph.edges.forEach((edge) => {
this.addEdge(edge);
});
// Connect internal and external output to shape
this.graph.addEdge(new Edge(this.graph.internalSource, graph.internalSource));
this.graph.addEdge(new Edge(graph.internalSink, this.graph.internalSink));
return this;
}
// worker(options?: WorkerOptions): Promise<Graph<In, Out>> {
// return new Promise((resolve, reject) => {
// this.build()
// .then((graph) => {
// const worker = new WorkerNode(graph, options);
// resolve(GraphBuilder.create().from().via(worker).to());
// })
// .catch(reject);
// });
// }
public build(): Promise<Graph<In, Out>> {
return new Promise((resolve, reject) => {
GraphValidator.validate(this.graph);
this.graph.once('ready', () => {
resolve(this.graph);
});
this.graph.emitAsync('build', this).catch((ex) => {
// Destroy model
this.graph.emit('destroy');
reject(ex);
});
});
}
}
export class GraphShapeBuilder<Builder extends GraphBuilder<any, any>> {
protected graphBuilder: Builder;
protected previousNodes: Array<GraphNode<any, any>>;
protected graph: GraphShape<any, any>;
protected static shapes: Map<string, (...args: any[]) => GraphNode<any, any>> = new Map();
constructor(graphBuilder: Builder, graph: GraphShape<any, any>, nodes: Array<GraphNode<any, any>>) {
this.graphBuilder = graphBuilder;
this.previousNodes = nodes;
this.graph = graph;
}
protected viaGraph(graph: GraphShape<any, any>): GraphNode<any, any> {
// Add graph as node
graph.nodes.forEach((node) => {
node.graph = this.graph;
this.graph.addNode(node);
});
graph.edges.forEach((edge) => {
this.graph.addEdge(edge);
});
this._insertNode(graph.internalSource);
return graph.internalSink;
}
public via(...nodes: Array<GraphNode<any, any> | string | GraphShape<any, any> | GraphBuilder<any, any>>): this {
const selectedNodes: Array<GraphNode<any, any>> = [];
nodes.forEach((node) => {
if (node === undefined) {
throw new Error('Undefined node was provided!');
} else if (node instanceof GraphBuilder) {
selectedNodes.push(this.viaGraph(node.graph));
} else if (node instanceof GraphShape) {
selectedNodes.push(this.viaGraph(node));
} else {
let nodeObject: GraphNode<any, any>;
if (typeof node === 'string') {
nodeObject = this.graph.findNodeByUID(node) || this.graph.findNodeByName(node);
if (nodeObject === undefined) {
// Add a placeholder
nodeObject = new PlaceholderNode(node);
}
} else {
nodeObject = node as GraphNode<any, any>;
}
this.graph.addNode(nodeObject);
this._insertNode(nodeObject);
selectedNodes.push(nodeObject);
}
});
this.previousNodes = selectedNodes;
return this;
}
/**
* Insert a new node in the existing graph
* @param {Node} node Node to insert
*/
private _insertNode(node: GraphNode<any, any>): void {
this.previousNodes.forEach((prevNode) => {
this.graph.addEdge(new Edge(prevNode, node));
});
}
public static registerShape(key: string, fn: (...args: any[]) => GraphNode<any, any>): void {
GraphShapeBuilder.shapes.set(key, fn);
}
public chunk(size: number, timeout?: number, timeoutUnit?: TimeUnit): this {
return this.via(GraphShapeBuilder.shapes.get('chunk')(size, timeout, timeoutUnit));
}
public flatten(): this {
return this.via(GraphShapeBuilder.shapes.get('flatten')());
}
/**
* Filter frames based on function
* @param {Function} filterFn Filter function (true to keep, false to remove)
* @returns {GraphShapeBuilder} Current graph builder instance
*/
public filter(filterFn: (frame: DataFrame) => boolean): this;
public filter(filterFn: (_?: any) => boolean): this {
return this.via(GraphShapeBuilder.shapes.get('filter')(filterFn));
}
/**
* Filter objects inside frames
* @param {Function} filterFn Filter function (true to keep, false to remove)
* @returns {GraphShapeBuilder} Current graph builder instance
*/
public filterObjects(filterFn: (object: DataObject, frame?: DataFrame) => boolean): this {
return this.via(GraphShapeBuilder.shapes.get('filterObjects')(filterFn));
}
/**
* Merge objects
* @param {Function} by Merge key
* @param {number} timeout Timeout
* @param {TimeUnit} timeoutUnit Timeout unit
* @returns {GraphShapeBuilder} Current graph shape builder
*/
public merge(
by: (frame: DataFrame) => boolean = () => true,
timeout = 100,
timeoutUnit = TimeUnit.MILLISECOND,
): this {
return this.via(GraphShapeBuilder.shapes.get('merge')(by, timeout, timeoutUnit));
}
public debounce(timeout = 100, timeoutUnit = TimeUnit.MILLISECOND): this {
return this.via(GraphShapeBuilder.shapes.get('debounce')(timeout, timeoutUnit));
}
public delay(timeout = 100, timeoutUnit = TimeUnit.MILLISECOND): this {
return this.via(GraphShapeBuilder.shapes.get('delay')(timeout, timeoutUnit));
}
/**
* Clone frames
* @returns {GraphShapeBuilder} Current graph shape builder
*/
public clone(): this {
return this.via(GraphShapeBuilder.shapes.get('clone')());
}
/**
* Convert positions of all objects to a certain reference space
* @param {ReferenceSpace | string} referenceSpace Reference space to convert to
* @returns {GraphShapeBuilder} Current graph shape builder
*/
public convertToSpace(referenceSpace: ReferenceSpace | string): this {
return this.via(GraphShapeBuilder.shapes.get('convertToSpace')(referenceSpace));
}
/**
* Convert positions of all objects from a certain reference space
* @param {ReferenceSpace | string} referenceSpace Reference space to convert from
* @returns {GraphShapeBuilder} Current graph shape builder
*/
public convertFromSpace(referenceSpace: ReferenceSpace | string): this {
return this.via(GraphShapeBuilder.shapes.get('convertFromSpace')(referenceSpace));
}
/**
* Buffer pushed objects
* @returns {GraphShapeBuilder} Current graph shape builder
*/
public buffer(): this {
return this.via(GraphShapeBuilder.shapes.get('buffer')());
}
/**
* Storage as sink node
* @returns {GraphBuilder} Graph builder
*/
public store(): Builder {
return this.to(GraphShapeBuilder.shapes.get('store')() as SinkNode);
}
public to(...nodes: Array<SinkNode<any> | string>): Builder {
if (nodes.length !== 0) {
const selectedNodes: Array<SinkNode<any>> = [];
nodes.forEach((node) => {
let nodeObject: GraphNode<any, any>;
if (node === undefined) {
throw new Error('Undefined node was provided as a sink!');
} else if (typeof node === 'string') {
nodeObject = this.graph.findNodeByUID(node) || this.graph.findNodeByName(node);
if (nodeObject === undefined) {
// Add a placeholder
nodeObject = new PlaceholderNode(node);
}
} else {
nodeObject = node;
}
this.graph.addNode(nodeObject);
this._insertNode(nodeObject);
this.graph.addEdge(new Edge(nodeObject, this.graph.internalSink));
selectedNodes.push(nodeObject as SinkNode<any>);
});
this.previousNodes = selectedNodes;
} else {
this._insertNode(this.graph.internalSink);
}
return this.graphBuilder;
}
}