Workspace 6.21.5
Writing a Workspace Polymorphic Data Operation - looking at the code

In this section, we're going to understand the code that was automatically generated by Workspace so that we know how to customise our operation, and define its execute() function: the function that all operations use to do their work.


Creating and running a simple test workflow (continue on with tutorial Writing a Workspace Polymorphic Data Operation )


calculateshapearea.h

Again, the header file should contain a header guard followed by any other header files this one requires. The Workspace/DataExecution/Operations/Builtin/polymorphicdataoperation.h header provides the DataExecution::PolymorphicDataOperation base class that we have to inherit from. The Workspace/DataExecution/Operations/operationfactorytraits.h header provides the DECLARE_WORKSPACE_OPERATION_FACTORY macro which is the Workspace operation analogy of the DECLARE_WORKSPACE_DATA_FACTORY macro for Workspace data types.

#ifndef CSIRO_CALCULATESHAPEAREA_H
#define CSIRO_CALCULATESHAPEAREA_H
#include <memory>
#include "simpleplugin.h"

The next thing to do is to define our implementation class CalculateShapeAreaImpl.

namespace CSIRO
{
class DataFactory;
class CalculateShapeAreaImpl;
Top level namespace for all Workspace code.
Definition: applicationsupportplugin.cpp:32

For the purpose of this tutorial, we will create an operation that takes a particular shape as input and provides the area as an output. The class is defined like this:

class SIMPLEPLUGIN_API CalculateShapeArea : public CSIRO::DataExecution::PolymorphicDataOperation
{
// Allow string translation to work properly
Q_DECLARE_TR_FUNCTIONS(CSIRO::CalculateShapeArea)
std::unique_ptr<CalculateShapeAreaImpl> pImpl_;
// Prevent copy and assignment - these should not be implemented
CalculateShapeArea(const CalculateShapeArea&) = delete;
CalculateShapeArea& operator=(const CalculateShapeArea&) = delete;
public:
CalculateShapeArea();
~CalculateShapeArea() override;
bool canChangeDataFactory(const CSIRO::DataExecution::DataFactory& factory) const override;
bool canChangeDataName(const QString& name) const override;
void dataObjectChanged(CSIRO::DataExecution::DataObject& oldDataObject) override;
bool execute() override;
};
Base class for all data type factories.
Definition: datafactory.h:71
Base class for all data objects.
Definition: dataobject.h:56
Base class for operations using a polymorphic type.
Definition: polymorphicdataoperation.h:51
virtual void dataObjectChanged(DataObject &oldDataObject)
Definition: polymorphicdataoperation.cpp:375
virtual bool canChangeDataName(const QString &name) const =0
virtual bool canChangeDataFactory(const DataFactory &factory) const =0
QString name
Definition: packagethirdparty.cpp:173

There are a few things to note in this class definition. Firstly, there is no sign of any inputs or outputs anywhere. This is deliberate, because we don't want other code to know about how the operation does what it does, nor what data it stores internally. These are implementation details that should be hidden, which is why we have the CalculateShapeAreaImpl implementation class. We will define CalculateShapeAreaImpl in the calculateshapearea.cpp file shortly, but we don't need to know anything about it for now because we only store a pointer to a CalculateShapeAreaImpl object, which we call pImpl_. This is a common name given to this sort of "pointer to implementation" object within Workspace.

The second thing to note is that, apart from the constructors and destructor and the execute() function, this class has three additional methods canChangeDataFactory(), canChangeDataName() and dataObjectChanged. We will talk some more about this shortly.

At the end of the file is the DECLARE_WORKSPACE_OPERATION_FACTORY macro. Just like its analogous macro for Workspace data types, this macro defines the things needed by Workspace when it uses the factory. The first parameter is the fully scoped name of the operation and the second parameter should be the export symbol used between the class keyword and the class name in the class definition.

DECLARE_WORKSPACE_OPERATION_FACTORY(CSIRO::CalculateShapeArea, SIMPLEPLUGIN_API)
#endif
#define DECLARE_WORKSPACE_OPERATION_FACTORY(T, WORKSPACE_EXPORT_SYMBOL)
Definition: operationfactorytraits.h:166

Creating and running a simple test workflow (continue on with tutorial Writing a Workspace Polymorphic Data Operation )


calculateshapearea.cpp

We will show this implementation file in stages and talk about each part as we go. As usual, we start by including the required headers. This time, we have a few more that we need. For now, we'll just list them and hold off talking about them until we meet the things they define for us. We also include the usual namespace scopes and make things from the CSIRO::DataExecution namespace visible to make our code clearer:

#include "calculateshapearea.h"

The next thing we need to do is define our private implementation class:

class CalculateShapeAreaImpl : public CSIRO::Application::TextLogger
{
// Allow string translation to work properly
Q_DECLARE_TR_FUNCTIONS(CalculateShapeArea)
public:
CalculateShapeArea& op_;
// Data objects
// Inputs and outputs
CalculateShapeAreaImpl(CalculateShapeArea& op, DataObject& obj);
bool execute();
};
Convenience base class for anything associated with an operation that needs to write text to the log ...
Definition: textlogger.h:66
Scalar input class (only one output can connect to it at a time).
Definition: inputscalar.h:50
Definition: simpleoperationio.h:230

The SimpleOutput class is analogous, provided by the same Workspace/DataExecution/InputOutput/simpleoperationio.h header and with area_ acting as a wrapper around the area data object.

So: Inputs and Outputs wrap the data, giving it a name and exposing it to Workspace.

The definition of the CalculateShapeAreaImpl constructor makes this clearer:

CalculateShapeAreaImpl::CalculateShapeAreaImpl(CalculateShapeArea& op, DataObject& obj) :
op_(op),
area_("Area", op_),
shape_("Shape", obj, op_, true)
{
// Make sure all of our inputs have data by default. If your operation accepts a
// large data structure as input, you may wish to remove this call and replace it
// with constructors for each input in the initialisation list above.
op_.ensureHasData();
// Recommend setting a description of the operation and each input / output here:
// op_.setDescription(tr("My operation does this, that and this other thing."));
// input1_.input_.setDescription(tr("Used for such and such."));
// output1_.output_.setDescription(tr("Results of the blah-di-blah."));
}

The first parameter to area_ provides the name the input/output will be known by and the second parameter is the operation to add the input/output to.

For shape_, which is of type InputScalar and is a polymorphic input, the first parameter again provides the name the input will be known by, the second parameter is a reference to the underlying data object, the third is the operation to add the input/output to and the last parameter indicates if the input can be modified in place.

bool CalculateShapeAreaImpl::execute()
{
auto& area = *area_;
// =======================================================
// Put the implementation of your task or algorithm here
// =======================================================
DataObject& dataObj = shape_.getDataObject();
QString shapeType = dataObj.getFactory().getTypeName();
logLine(LOG_INFO, "Shape type: " + shapeType);
if (shapeType == "CSIRO::Circle")
{
Circle circle = dataObj.getRawData<Circle>();
area = M_PI * pow(circle.getRadius(), 2);
}
else if (shapeType == "CSIRO::Rectangle")
{
Rectangle rectangle = dataObj.getRawData<Rectangle>();
area = rectangle.getWidth() * rectangle.getHeight();
}
else if (shapeType == "CSIRO::Square")
{
Square square = dataObj.getRawData<Square>();
area = pow(square.getLength(), 2);
}
else
{
logLine(LOG_ERROR, "Unrecognised shape. Cannot calculate area.");
area = 0;
return false;
}

As you can see, all we have needed to do was add to our auto-generated execute function is:

DataObject& dataObj = shape_.getDataObject();
QString shapeType = dataObj.getFactory().getTypeName();
logLine(LOG_INFO, "Shape type: " + shapeType);
if (shapeType == "CSIRO::Circle")
{
Circle circle = dataObj.getRawData<Circle>();
area = M_PI * pow(circle.getRadius(), 2);
}
else if (shapeType == "CSIRO::Rectangle")
{
Rectangle rectangle = dataObj.getRawData<Rectangle>();
area = rectangle.getWidth() * rectangle.getHeight();
}
else if (shapeType == "CSIRO::Square")
{
Square square = dataObj.getRawData<Square>();
area = pow(square.getLength(), 2);
}
else
{
logLine(LOG_ERROR, "Unrecognised shape. Cannot calculate area.");
area = 0;
return false;
}
Note
Note that for long-running operations, you will want to provide feedback to users during execution. To do that, see Providing User Feedback During Execution

The InputScalar class is provided by the Workspace/DataExecution/InputOutput/inputscalar.h header. InputScalar acts as a wrapper around a data object and presents it to an operation as a single named input. In the above example, shape_ will act as a wrapper around radius_.

So: DataObjects point to our data, and Inputs and Outputs wrap the data, giving it a name and exposing it to Workspace.

The CalculateShapeArea constructor and destructor come next:

CalculateShapeArea::CalculateShapeArea() :
CSIRO::DataExecution::PolymorphicDataOperation(
CSIRO::DataExecution::OperationFactoryTraits<CalculateShapeArea>::getInstance(),
tr("Calculate shape area"),
DataFactoryTraits<CSIRO::Circle>::getInstance(),
tr("Calculate shape area"))
{
pImpl_ = std::make_unique<CalculateShapeAreaImpl>(*this, getDataObject());
}
CalculateShapeArea::~CalculateShapeArea() = default;

The CalculateShapeArea constructor passes several important pieces of information to its base class. First, it passes the factory associated with the operation in the OperationFactoryTraits<CalculateShapeArea>::getInstance() parameter. The second parameter specifies the default label to give to all new operations. This will be shown to the user and can contain more or less any text, although shorter is better. Spaces are allowed in the label, as the "Calculate shape area" example shows. The next parameter specifies the default data factory that this operation uses. In this example, the shape_ input is initialised to a Circle.

The constructor creates the private implementation object pImpl_. The destructor simply deletes what the constructor created.

Then we define the execute() function:

bool CalculateShapeArea::execute()
{
return pImpl_->execute();
}

The PolymorphicDataOperation has a few pure virtual functions. We need to define these in our class.

bool CalculateShapeArea::canChangeDataFactory(const CSIRO::DataExecution::DataFactory& factory) const
{
if (&factory == &DataFactoryTraits<CSIRO::Circle>::getInstance())
{
return true;
}
if (&factory == &DataFactoryTraits<CSIRO::Rectangle>::getInstance())
{
return true;
}
if (&factory == &DataFactoryTraits<CSIRO::Square>::getInstance())
{
return true;
}
return false;
}

The above code restricts the polymorphic data input shape_ to Circle, Square or Rectangle. Other data types are not permitted and will return false. If the operation's polymorphic data input/output allows all data types, then the function is greatly simplified and the programmer only needs to return true. For example:

// Allow all data types
bool CalculateShapeArea::canChangeDataFactory(const CSIRO::DataExecution::DataFactory& factory) const
{
return true;
}
bool CalculateShapeArea::canChangeDataName(const QString& name) const
{
// Can never create a data source for connecting to the
// dependency input, since that input carries no data
if (name == "Dependencies")
{
return false;
}
return true;
}

canChangeDataName returns true if the subclass allows the data name to be set to name. The subclass can return false to indicate that the data name cannot be changed to name, or indeed that it cannot be changed at all.

void CalculateShapeArea::dataObjectChanged(DataObject& oldDataObject)
{
Q_UNUSED(oldDataObject);
DataObject& dataObject = getDataObject();
pImpl_->shape_.setDataObject(dataObject);
}

The default implementation of this function (in the base class) does nothing. This function is called by setDataFactory once the data object has been changed. It allows the subclass to take additional action to update itself before the process of changing the data factory is completed. For example, changing the polymorphic data type from Circle to Square triggers a call to this function and shape_ can be set to the new data object Square.

The last thing we do is close off our namespaces and use a macro to define the factory for our operation:

The DEFINE_WORKSPACE_OPERATION_FACTORY macro is just like the one we used for the data factory. It takes care of defining the operation factory for us. We have to supply three things to the macro: the operation it is for (CalculateShapeArea), the plugin it belongs to (SimplePlugin::getInstance() ) and a category to file it under ("Simple plugin" in this example).


Creating and running a simple test workflow (continue on with tutorial Writing a Workspace Polymorphic Data Operation )


simpleplugin.cpp

The operation creator wizard has automatically updated the simpleplugin.cpp file so that the plugin exposes the CalculateShapeArea operation to Workspace. Two changes have been made to this file, the first of which is to include the new operation header at the top.

#include "simpleplugin.h"
#include <memory>
#include <QString>
#include <QStringList>
#include "calculaterectanglearea.h"
#include "calculateshapearea.h"

Secondly, a line of code has been added to the SimplePlugin::Setup function. This line will register our new OperationFactory (an auto-generated class which knows how to create our new operation) with Workspace, so that it can create CalculateRectangleArea operations as the user requests them.

bool SimplePlugin::setup()
{
// Add your data factories like this:
//addFactory( CSIRO::DataExecution::DataFactoryTraits<MyDataType>::getInstance() );
// Add your operation factories like this:
//addFactory( CSIRO::DataExecution::OperationFactoryTraits<MyOperation>::getInstance() );
// Add your widget factories like this:
//addFactory( MyNamespace::MyWidgetFactory::getInstance() );
// Add your adaptor factories like this:
//addFactory( CSIRO::DataExecution::SimpleAdaptorFactory<MyDataType, OtherDataType>::getInstance());
// Add your collection workflows here like this:
//addWorkspaceCollection(":/appdata/<MyPlugin>/myworkflow.wsx");
return true;
}
Traits class for data objects of type T.
Definition: datafactorytraits.h:143
Traits class for operations of type T.
Definition: operationfactorytraits.h:64

Creating and running a simple test workflow (continue on with tutorial Writing a Workspace Polymorphic Data Operation )


CMakeLists.txt

Lastly, the operation creator wizard has automatically updated the plugin CMakeLists.txt file to add the operation's header and source files in the three set functions:

set(HEADERS
${SIMPLEPLUGIN_SOURCE_DIR}/calculateshapearea.h
${SIMPLEPLUGIN_SOURCE_DIR}/calculaterectanglearea.h
${SIMPLEPLUGIN_SOURCE_DIR}/square.h
${SIMPLEPLUGIN_SOURCE_DIR}/circle.h
${SIMPLEPLUGIN_SOURCE_DIR}/rectangle.h
${SIMPLEPLUGIN_SOURCE_DIR}/simpleplugin_api.h
${SIMPLEPLUGIN_SOURCE_DIR}/simpleplugin.h
)
set(INSTALL_HEADERS
${SIMPLEPLUGIN_SOURCE_DIR}/calculateshapearea.h
${SIMPLEPLUGIN_SOURCE_DIR}/calculaterectanglearea.h
${SIMPLEPLUGIN_SOURCE_DIR}/square.h
${SIMPLEPLUGIN_SOURCE_DIR}/circle.h
${SIMPLEPLUGIN_SOURCE_DIR}/rectangle.h
${SIMPLEPLUGIN_SOURCE_DIR}/simpleplugin_api.h
${SIMPLEPLUGIN_SOURCE_DIR}/simpleplugin.h
)
set(SOURCES
${SIMPLEPLUGIN_SOURCE_DIR}/calculateshapearea.cpp
${SIMPLEPLUGIN_SOURCE_DIR}/calculaterectanglearea.cpp
${SIMPLEPLUGIN_SOURCE_DIR}/square.cpp
${SIMPLEPLUGIN_SOURCE_DIR}/circle.cpp
${SIMPLEPLUGIN_SOURCE_DIR}/rectangle.cpp
${SIMPLEPLUGIN_SOURCE_DIR}/simpleplugin.cpp
)

Creating and running a simple test workflow (continue on with tutorial Writing a Workspace Polymorphic Data Operation )