OpenHPS/openhps-core

View on GitHub
src/ModelBuilder.ts

Summary

Maintainability
A
55 mins
Test Coverage
import 'reflect-metadata';
import { DataFrame, DataObject, ReferenceSpace } from './data';
import { GraphValidator, ModelGraph } from './graph/_internal/implementations';
import { Model } from './Model';
import { GraphBuilder } from './graph/builders/GraphBuilder';
import { GraphShape } from './graph/_internal/implementations/GraphShape';
import { Service, NodeData, TimeService, DataObjectService, MemoryDataService, NodeDataService } from './service';

/**
 * Model builder to construct and build a {@link Model} consisting of graph shapes and services.
 *
 * ## Usage
 * Models can be created using the {@link ModelBuilder}. Once you have added all services and constructed the graph, you can build the model using the ```build()``` function. A promise will be returned with the created model.
 *
 * ```typescript
 * import { ModelBuilder } from '@openhps/core';
 *
 * ModelBuilder.create()
 *     .build().then(model => {
 *         // ...
 *     });
 * ```
 * The graph shape of a model is immutable and can not be altered after building.
 *
 * ### Shape Builder
 * Shapes can be created by starting with the ```from()``` function. This function takes an optional
 * parameter of one or multiple [source nodes](#sourcenode).
 *
 * In order to end a shape, the ```to()``` function needs to be called with one or more optional [sink nodes](#sinknode).
 * ```typescript
 * import { ModelBuilder } from '@openhps/core';
 *
 * ModelBuilder.create()
 *     .from()
 *     .to()
 *     .build().then(model => {
 *         // ...
 *     });
 * ```
 *
 * Alternatively for readability with multiple shapes, the shapes can individually be created using the ```addShape()``` function as shown below.
 * ```typescript
 * import { ModelBuilder, GraphBuilder } from '@openhps/core';
 *
 * ModelBuilder.create()
 *     .addShape(
 *       GraphBuilder.create()
 *         .from()
 *         .to())
 *     .build().then(model => {
 *         // ...
 *     });
 * ```
 *
 * #### Building Source Processors
 * It is possible to have multiple processing nodes between the source and sink. These processing nodes can manipulate the data frame
 * when it traverses from node to node.
 * ```typescript
 * import { ModelBuilder } from '@openhps/core';
 *
 * ModelBuilder.create()
 *     .from(...)
 *     .via(new ComputingNode())
 *     .via(new AnotherComputingNode())
 *     .to(...)
 *     .build().then(model => {
 *         // ...
 *     });
 * ```
 *
 * #### Helper Functions
 * Helper functions can replace the ```via()``` function. Commonly used nodes such as frame filters, merging of data frames from
 * multiple sources, ... can be replaced with simple functions as ```filter()``` or ```merge()``` respectively.
 * ```typescript
 * import { ModelBuilder } from '@openhps/core';
 * import { CSVSourceNode, CSVSinkNode } from '@openhps/csv';
 *
 * ModelBuilder.create()
 *     .from(
 *         new CSVSourceNode('scanner1.csv', ...),
 *         new CSVSourceNode('scanner2.csv', ...),
 *         new CSVSourceNode('scanner3.csv', ...)
 *     )
 *     .filter((frame: DataFrame) => true)
 *     .merge((frame: DataFrame) => frame.source.uid)
 *     .via(new ComputingNode())
 *     .via(new AnotherComputingNode())
 *     .to(new CSVSinkNode('output.csv', ...))
 *     .build().then(model => {
 *         // ...
 *     });
 * ```
 *
 * ### Debug Logging
 * When building the model, you can provide a logger callback that has two arguments. An error level complying
 * with normal log levels and a log object that represents an object.
 * ```typescript
 * import { ModelBuilder } from '@openhps/core';
 *
 * ModelBuilder.create()
 *     // Set the logger that will be used by all nodes and services
 *     .withLogger((level: string, log: any) => {
 *         console.log(log);
 *     })
 *     // ...
 *     .build().then(model => {
 *      // ...
 *     });
 * ```
 *
 * ### Adding Services
 * Adding services can be done using the ```addService()``` function in the model builder.
 * ```typescript
 * import { ModelBuilder } from '@openhps/core';
 *
 * ModelBuilder.create()
 *     .addService(...)
 *     // ...
 *     .build().then(model => {
 *
 *     });
 * ```
 */
export class ModelBuilder<In extends DataFrame, Out extends DataFrame> extends GraphBuilder<In, Out> {
    public graph: ModelGraph<any, any>;

    protected constructor() {
        super(new ModelGraph<In, Out>());
        this.graph.name = 'model';
        // Store data objects
        this.graph.addService(
            new DataObjectService(
                new MemoryDataService(DataObject, {
                    keepChangelog: false,
                }),
            ),
        );
        // Store spaces in their own memory data object service
        this.graph.addService(
            new DataObjectService(
                new MemoryDataService(ReferenceSpace, {
                    keepChangelog: false,
                }),
            ),
        );
        // Store node data
        this.graph.addService(
            new NodeDataService(
                new MemoryDataService(NodeData, {
                    keepChangelog: false,
                }),
            ),
        );
        // Default time service using system time
        this.graph.addService(new TimeService());
    }

    public static create<In extends DataFrame, Out extends DataFrame>(): ModelBuilder<In, Out> {
        return new ModelBuilder();
    }

    /**
     * Model logger
     * @param {Function} logger Logging function
     * @returns {ModelBuilder} Model builder instance
     */
    public withLogger(logger: (level: string, message: string, data?: any) => void): this {
        this.graph.logger = logger;
        return this;
    }

    public withReferenceSpace(space: ReferenceSpace): this {
        (this.graph as ModelGraph<In, Out>).referenceSpace = space;
        return this;
    }

    /**
     * Add a service to the model
     * @param {Service} service Service to add
     * @param {ProxyHandler} [proxy] Proxy handler
     * @returns {ModelBuilder} Model builder instance
     */
    public addService(service: Service, proxy?: ProxyHandler<any>): this {
        (this.graph as ModelGraph<In, Out>).addService(service, proxy);
        return this;
    }

    /**
     * Add multiple services to the model
     * @param {Service[]} services Services to add
     * @returns {ModelBuilder} Model builder instance
     */
    public addServices(...services: Service[]): this {
        services.forEach((service) => this.addService(service));
        return this;
    }

    /**
     * Add graph shape to graph
     * @param {GraphBuilder | GraphShape | Model} shape Graph builder or abstract graph
     * @returns {GraphBuilder} Current graph builder instance
     */
    public addShape(shape: GraphBuilder<any, any> | GraphShape<any, any> | Model<any, any>): this {
        if (shape instanceof ModelGraph) {
            // Add services
            (shape as Model).findAllServices().forEach((service) => {
                this.addService(service);
            });
        } else if (shape instanceof ModelBuilder) {
            (shape.graph as Model).findAllServices().forEach((service) => {
                this.addService(service);
            });
        }
        return super.addShape(shape as GraphShape<any, any>);
    }

    public build(): Promise<Model<In, Out>> {
        return new Promise((resolve, reject) => {
            GraphValidator.validate(this.graph);
            this.graph.once('ready', () => {
                resolve(this.graph as Model<In, Out>);
            });
            this.graph.emitAsync('build', this).catch((ex) => {
                // Destroy model
                this.graph.emit('destroy');
                reject(ex);
            });
        });
    }
}