Developing new tools

Daria Zenkova

2018-12-21

Introduction to project architecture

Project architecture includes two main components:

Those two components are connected through OpenCPU as a bridge, and inside the JavaScript implementation the RPC-call to server is used.

Consequently, in order to add new tool one needs:

All of these steps will be described in details in this vignette.

R-side implementation

Each analysis method in this package takes three compulsory arguments:

Also, some methods require to replace NA values in series matrix in order to be used, so usually also replacena argument is needed.

Other arguments depend on the method’s specifics.

Before calculating the method, the considered data from the whole series matrix needs to be extracted. For that reason, the package has non-exported method prepareData, that takes as arguments es, columns, rows,replacena`.

After the method is used, the result can be sent back in two ways:

The approximate code structure is demonstrated here:

# instead of ellipsis would be specific arguments
method <- function(es,  rows = c(), columns = c(), replacea = "mean", ...) {
    # Here may be some assertions 

    # Data preparation
    data <- prepareData(es, rows, columns, replacena)

    # Using method
    res <- ...

    # Sending back the result as JSON:
    return(jsonlite::toJSON(res))

    # Or as ProtoBuf
    f <- tempfile(pattern = "pat", tmpdir = getwd(), fileext = ".bin")
    writeBin(protolite::serialize_pb(res), f)
    jsonlite::toJSON(f)
}

Remember, that your tool must be exported, fully documented and tested.

JavaScript-side implementation

The structure of the tool description:

Here is the approximate JavaScript description of the tool:

phantasus.NewTool = function () {
};
phantasus.NewTool.prototype = {
  // Tool name:
  toString: function () {
    return 'new';
  },
  
  // Initialization of tool's input fields
  init: function (project, form) {
    // Here is your initialization code
  },
  
  // Description of tool' GUI
  gui: function (project) {
    return [{
      name : 'fieldName',
      type : 'type',
      options : [],
      value : 'default_value'
    }];
  },
  
  // Main function of the Tool
  execute: function (options) {
    var project = options.project;
    
    // Getting the input
    var field = options.input.fieldName;

    // Reading actual dataset
    var dataset = project.getSortedFilteredDataset();
  
    // Each dataset has es session as a field
    var es = dataset.getESSession();

    // Get indices of selected rows and columns if they are selected
    var trueIndices = phantasus.Util.getTrueIndices(dataset);

    // Further calculation may proceed only when esSession is ready
    es.then(function (essession) {
      // Function arguments, there also should be method-specific arguments
      var args = {
        es: essession
      };
      if (trueIndices.rows.length > 0) {
        args.rows = trueIndices.rows;
      }
      if (trueIndices.columns.length > 0) {
        args.columns = trueIndices.columns;
      }
      
      // RPC-call to OpenCPU-server
      var req = ocpu.call("methodName", args, function (session) {
        session.getObject(function (success) {
          // success -- returned result, needs to be processed
          // Result getting depends on its type

          // JSON:
          var result = JSON.parse(success); 
          // after that you can proceed with result
          
          // ProtoBuf:
          var r = new FileReader();
          var filePath = phantasus.Util.getFilePath(session, 
                                                    JSON.parse(success)[0]);

          r.onload = function (e) {
            var contents = e.target.result;
            var ProtoBuf = dcodeIO.ProtoBuf;
            
            // message.proto is file with specified protocol
            ProtoBuf.protoFromFile("./message.proto", 
                                   function (error, success) {
              if (error) {
                throw new Error(error);
              }
              var builder = success,
                rexp = builder.build("rexp"),
                REXP = rexp.REXP,
                rclass = REXP.RClass;
              var res = REXP.decode(contents);
              var data = phantasus.Util.getRexpData(res, rclass);
              var names = phantasus.Util.getFieldNames(res, rclass);
              
              // here you can proceed with result
            })
          };
          phantasus.BlobFromPath.getFileObject(filePath, function (file) {
            r.readAsArrayBuffer(file);
          });
        })
      }, false, '::' + dataset.getESVariable());
      
      req.fail(function () {
        // failed request procession
        throw new Error("Method call failed" + req.responseText);
      });
    });
  }
};