MIRA
Tutorial: Creating an advanced 2D Visualization


Introduction

2D visualizations use the Qt GraphicsScene as render system. Therefore, all data can be displayed using QGraphicsItems. Here, step by step we will create a 2D visualization for a Point2f channel.

The full working example (source code, makefile) can be found in gui/examples/tutorials/Point2Visualization.C.

Creating the Visualization Class

First we will create our visualization class. We can do that by deriving a class from mira::Visualization2D. The meta information on that class will be used to group and display visualizations in a list view of all available visualizations. We can specify the name, the category, and a description that is displayed in the visualizations dialog.

We also need to implement the pure virtual method setupScene() from our base class to setup our graphics scene. We will fill it with life below.

using namespace mira;
class Point2Visualization : public Visualization2D
{
MIRA_META_OBJECT(Point2Visualization,
("Name", "Point")
("Category", "Geometry")
("Description", "Visualizes a point."))
public:
virtual void setupScene(IVisualization2DSite* site)
{
// TODO setup our scene here
}
};

In order to compile our visualization into a library, we need a CMakeLists.txt file:

# Include our dependencies
MIRA_REQUIRE_PACKAGE(GUIVisualization)
MIRA_ADD_LIBRARY(Point2Visualization
SHARED
PACKAGE Visualizations
SOURCE
Point2Visualization.C
LINK_LIBS
GUIVisualization
)

The class can now be used by adding it via the "+"-Button of the Visualization Control view even if if it will not render anything yet. To change that, we need to setup our scene.

Setup a Scene and Geometry

First, we need a member and initialize it in the setupScene method:

#include <QtGui/QGraphicsEllipseItem>
#include <QtGui/QGraphicsScene>
...
Point2Visualization() :
mEllipse(NULL) {}
virtual ~Point2Visualization()
{
delete mEllipse;
}
...
virtual void setupScene(IVisualization3DSite* site)
{
QGraphicsScene* sceneManager = site->getSceneManager();
// we will use an ellipse to render our point
mEllipse = new QGraphicsEllipseItem();
// give it a size of 1 cm
mEllipse->setRect(-0.05, -0.05, 0.1, 0.1);
sceneManager->addItem(mEllipse);
}
...
protected:
QGraphicsEllipseItem* mEllipse;

Now, an ellipse is rendered at the origin of the scenery. The position can be changed via:

mEllipse->setPos(x, y);

Add a Channel Property

But before changing the position of the ellipse, we first need to obtain it from a Point2f channel. Therefore, we add a ChannelProperty member and reflect it in our reflect() method:

template <typename Reflector>
void reflect(Reflector& r)
{
// call base class reflect
MIRA_REFLECT_BASE(r, Visualization2D);
// our channel property for a channel named "Point"
channelProperty(r, "Point", mPointChannel, "The point channel to visualize");
}
protected:
ChannelProperty<Point2f> mPointChannel;

To update the position of the ellipse corresponding to the channel, we need to listen for changes on that channel. This is done by specifying a callback, i.e. by binding a member method using setDataChangedCallback(). This is similar to subscribing to a "normal" channel:

Point2Visualization() :
mEllipse(NULL)
{
// register our callback function
mPointChannel.setDataChangedCallback(boost::bind(&Point2Visualization::dataChanged, this, _1));
}
...
void dataChanged(ChannelRead<Point2f> read)
{
// set position of our scene point
mEllipse->setPos(read->x(), read->y());
}

That was easy. But what if the coordinates in that point channel are given in a coordinate system defined by the publisher of that channel instead of the world coordinate frame?

Use the Correct Transformation

To render the ellipse at its real position corresponding to the current frame of the 2D editor view, we need to take the selected editor frame and the frame of the channel into account.

To be able to do this we need to create a new QGraphicsItem as a parent node item where we attach our ellipse item to.

Point2Visualization() :
mEllipse(NULL),
mNode(NULL)
{
// register our callback function
mPointChannel.setDataChangedCallback(boost::bind(&Point2Visualization::dataChanged, this, _1));
}
virtual ~Point2Visualization()
{
delete mEllipse;
delete mNode;
}
...
virtual void setupScene(IVisualization2DSite* site)
{
mNode = new QGraphicsItem();
mEllipse = new QGraphicsEllipseItem(mNode);
// give it a size of 1 cm
mEllipse->setRect(-0.05, -0.05, 0.1, 0.1);
sceneManager->addItem(mNode);
}
...
protected:
QGraphicsItem* mNode;

Done. The ellipse item is now a child of the new node item and will be positioned relative to that item. We can now also update the position of the node item according to the channel frame. But let's consider that the channel is only updated once in a minute.

Update the Visualization

So far, when the user changes the transform frame of the 2D editor view, the position of our ellipse ss not updated until the data in the channel changes. Besides, our data frame could be part of a transform chain where other frames get updated more often than our point channel. To overcome this limitation, we can overwrite another method of our base class - "onUpdate", that is called periodically by the GUI thread. The time span since the last call of update is passed as parameter dt.

...
void dataChanged(ChannelRead<Point2f> read)
{
...
// store the frame id and timestamp of our data
mFrameID = read->frameID;
mDataTimestamp = read->timestamp;
}
...
protected:
virtual void onUpdate(Duration dt)
{
if (!mPointChannel.isValid())
return;
// get the transformation between
// our data frame and the selected frame of the 2D editor view
RigidTransform2f d = getAuthority().getTransform<RigidTransform2f>(
mFrameID, mDataTimestamp,
getSite()->getCameraFrame(), Time::now(),
getSite()->getFixedFrame());
// set the transform on the node (indirectly move mEllipse)
}
...
protected:
std::string mFrameID;
Time mDataTimestamp;

Now our ellipse is positioned relative to a node item that gets translated to the selected frame of our editor.

Add Drag'n'Drop Support

At the moment our visualization can be added via the "Add" button of the visualization control only. To enable drag'n'drop support, allowing us to drag the Point2f channel directly into the 2D view, we need to implement the getDataConnection() method:

// this method is used to determine the data type of the channel
// we can visualize
virtual DataConnection getDataConnection()
{
return DataConnection(mPointChannel);
}

This method tells the visualization view which type of data we can visualize and what is the main data channel for visualization.