This commit is contained in:
2021-10-14 13:47:35 +02:00
commit 6625a8dfaa
4026 changed files with 844291 additions and 0 deletions
@@ -0,0 +1,157 @@
#include "ovpCAlgorithmLevelMeasure.h"
#include <iomanip>
#include <sstream>
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
static void ResetScoresButtonCB(GtkToolButton* /*button*/, gpointer data)
{
auto* levelMeasure = reinterpret_cast<CAlgorithmLevelMeasure*>(data);
for (auto& i : levelMeasure->m_ProgressBar) { i.score = 0; }
}
static void ThresholdSpinbuttonCB(GtkSpinButton* button, gpointer data)
{
auto* levelMeasure = reinterpret_cast<CAlgorithmLevelMeasure*>(data);
levelMeasure->m_Threshold = .01 * gtk_spin_button_get_value(button);
}
static void ShowPercentagesToggleButtonCB(GtkToggleToolButton* button, gpointer data)
{
auto* levelMeasure = reinterpret_cast<CAlgorithmLevelMeasure*>(data);
levelMeasure->m_ShowPercentages = (gtk_toggle_tool_button_get_active(button) != 0);
}
bool CAlgorithmLevelMeasure::initialize()
{
m_ipMatrix.initialize(getInputParameter(OVP_Algorithm_LevelMeasure_InputParameterId_Matrix));
m_opMainWidget.initialize(getOutputParameter(OVP_Algorithm_LevelMeasure_OutputParameterId_MainWidget));
m_opToolbarWidget.initialize(getOutputParameter(OVP_Algorithm_LevelMeasure_OutputParameterId_ToolbarWidget));
m_mainWidgetInterface = gtk_builder_new();
gtk_builder_add_from_file(m_mainWidgetInterface, Directories::getDataDir() + "/plugins/simple-visualization/openvibe-simple-visualization-LevelMeasure.ui",
nullptr);
m_toolbarWidgetInterface = gtk_builder_new();
gtk_builder_add_from_file(m_toolbarWidgetInterface,
Directories::getDataDir() + "/plugins/simple-visualization/openvibe-simple-visualization-LevelMeasure.ui", nullptr);
gtk_builder_connect_signals(m_mainWidgetInterface, nullptr);
gtk_builder_connect_signals(m_toolbarWidgetInterface, nullptr);
g_signal_connect(G_OBJECT(gtk_builder_get_object(m_toolbarWidgetInterface, "reset-score-button")), "clicked", G_CALLBACK(ResetScoresButtonCB), this);
g_signal_connect(G_OBJECT(gtk_builder_get_object(m_toolbarWidgetInterface, "show-percentages-toggle-button")), "toggled",
G_CALLBACK(ShowPercentagesToggleButtonCB), this);
g_signal_connect(G_OBJECT(gtk_builder_get_object(m_toolbarWidgetInterface, "threshold-spinbutton")), "value-changed", G_CALLBACK(ThresholdSpinbuttonCB),
this);
g_signal_connect(G_OBJECT(gtk_builder_get_object(m_toolbarWidgetInterface, "level-measure-toolbar")), "delete_event", G_CALLBACK(gtk_widget_hide), nullptr);
m_mainWindow = GTK_WIDGET(gtk_builder_get_object(m_mainWidgetInterface, "level-measure-table"));
m_toolbarWidget = GTK_WIDGET(gtk_builder_get_object(m_toolbarWidgetInterface, "level-measure-toolbar"));
m_ShowPercentages = (gtk_toggle_tool_button_get_active(
GTK_TOGGLE_TOOL_BUTTON(gtk_builder_get_object(m_toolbarWidgetInterface, "show-percentages-toggle-button"))) != 0);
m_Threshold = .01 * gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(m_toolbarWidgetInterface, "threshold-spinbutton")));
return true;
}
bool CAlgorithmLevelMeasure::uninitialize()
{
g_object_unref(m_toolbarWidgetInterface);
m_toolbarWidgetInterface = nullptr;
g_object_unref(m_mainWidgetInterface);
m_mainWidgetInterface = nullptr;
m_opToolbarWidget.uninitialize();
m_opMainWidget.uninitialize();
m_ipMatrix.uninitialize();
return true;
}
bool CAlgorithmLevelMeasure::process()
{
if (this->isInputTriggerActive(OVP_Algorithm_LevelMeasure_InputTriggerId_Reset))
{
if (m_ipMatrix->getDimensionCount() != 1 && m_ipMatrix->getDimensionCount() != 2)
{
getLogManager() << Kernel::LogLevel_ImportantWarning << "Input matrix does not have 1 or 2 dimensions (" << m_ipMatrix->getDimensionCount() << ")\n";
return false;
}
const guint nRow = guint(m_ipMatrix->getDimensionCount() == 2 ? m_ipMatrix->getDimensionSize(0) : 1);
const guint nCol = guint(m_ipMatrix->getDimensionCount() == 2 ? m_ipMatrix->getDimensionSize(1) : m_ipMatrix->getDimensionSize(0));
GtkTable* table = GTK_TABLE(gtk_builder_get_object(m_mainWidgetInterface, "level-measure-table"));
gtk_table_resize(table, nRow, nCol);
for (guint i = 0; i < nRow; ++i)
{
for (guint j = 0; j < nCol; ++j)
{
GtkBuilder* gtkBuilder = gtk_builder_new();
gtk_builder_add_from_file(gtkBuilder, Directories::getDataDir() + "/plugins/simple-visualization/openvibe-simple-visualization-LevelMeasure.ui",
nullptr);
GtkWidget* widget = GTK_WIDGET(gtk_builder_get_object(gtkBuilder, "progress-bar-level"));
gtk_container_remove(GTK_CONTAINER(gtk_widget_get_parent(widget)), widget);
gtk_table_attach(table, widget, j, j + 1, i, i + 1, GtkAttachOptions(GTK_EXPAND | GTK_FILL), GtkAttachOptions(GTK_EXPAND | GTK_FILL), 0, 0);
g_object_unref(gtkBuilder);
progress_bar_t bar;
bar.bar = GTK_PROGRESS_BAR(widget);
bar.score = 0;
bar.lastWasOverThreshold = false;
m_ProgressBar.push_back(bar);
}
}
m_opMainWidget = m_mainWindow;
m_opToolbarWidget = m_toolbarWidget;
}
if (this->isInputTriggerActive(OVP_Algorithm_LevelMeasure_InputTriggerId_Refresh))
{
auto it = m_ProgressBar.begin();
double* iBuffer = m_ipMatrix->getBuffer();
size_t n = m_ipMatrix->getBufferElementCount();
while (n--)
{
double percent = *iBuffer;
if (percent > 1) { percent = 1; }
if (percent < 0) { percent = 0; }
if (percent > m_Threshold && !it->lastWasOverThreshold)
{
it->score++;
it->lastWasOverThreshold = true;
}
if (percent <= m_Threshold) { it->lastWasOverThreshold = false; }
std::stringstream ss;
ss << std::fixed << std::setprecision(2) << "score : " << it->score << "\n";
if (m_ShowPercentages) { ss << "level : " << percent * 100 << "%\n"; }
gtk_progress_bar_set_fraction(it->bar, percent);
gtk_progress_bar_set_text(it->bar, ss.str().c_str());
iBuffer++;
++it;
}
this->activateOutputTrigger(OVP_Algorithm_LevelMeasure_OutputTriggerId_Refreshed, true);
}
return true;
}
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,85 @@
#pragma once
#include "../ovp_defines.h"
#include <openvibe/ov_all.h>
#include <toolkit/ovtk_all.h>
#include <gtk/gtk.h>
#include <map>
#include <vector>
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
class CAlgorithmLevelMeasure final : public Toolkit::TAlgorithm<IAlgorithm>
{
public:
void release() override { delete this; }
bool initialize() override;
bool uninitialize() override;
bool process() override;
_IsDerivedFromClass_Final_(Toolkit::TAlgorithm<IAlgorithm>, OVP_ClassId_Algorithm_LevelMeasure)
protected:
Kernel::TParameterHandler<CMatrix*> m_ipMatrix;
Kernel::TParameterHandler<GtkWidget*> m_opMainWidget;
Kernel::TParameterHandler<GtkWidget*> m_opToolbarWidget;
GtkBuilder* m_mainWidgetInterface = nullptr;
GtkBuilder* m_toolbarWidgetInterface = nullptr;
GtkWidget* m_mainWindow = nullptr;
GtkWidget* m_toolbarWidget = nullptr;
public:
using progress_bar_t = struct
{
GtkProgressBar* bar;
size_t score;
bool lastWasOverThreshold;
};
std::vector<progress_bar_t> m_ProgressBar;
bool m_ShowPercentages = false;
double m_Threshold = 0.1;
};
class CAlgorithmLevelMeasureDesc final : public IAlgorithmDesc
{
public:
void release() override { }
CString getName() const override { return CString("Level measure"); }
CString getAuthorName() const override { return CString("Yann Renard"); }
CString getAuthorCompanyName() const override { return CString("INRIA/IRISA"); }
CString getShortDescription() const override { return CString("Displays sample chunk of each channel as a row of progress bars"); }
CString getDetailedDescription() const override { return CString("Another way to look at it: Each displayed row is a histogram normalized to sum to 1"); }
CString getCategory() const override { return CString("Simple visualization"); }
CString getVersion() const override { return CString("1.0"); }
virtual CString getStockItemName() const { return CString("gtk-go-up"); }
CIdentifier getCreatedClass() const override { return OVP_ClassId_Algorithm_LevelMeasure; }
IPluginObject* create() override { return new CAlgorithmLevelMeasure; }
bool getAlgorithmPrototype(Kernel::IAlgorithmProto& prototype) const override
{
prototype.addInputParameter(OVP_Algorithm_LevelMeasure_InputParameterId_Matrix, "Matrix", Kernel::ParameterType_Matrix);
prototype.addOutputParameter(OVP_Algorithm_LevelMeasure_OutputParameterId_MainWidget, "Main widget", Kernel::ParameterType_Pointer);
prototype.addOutputParameter(OVP_Algorithm_LevelMeasure_OutputParameterId_ToolbarWidget, "Toolbar widget", Kernel::ParameterType_Pointer);
prototype.addInputTrigger(OVP_Algorithm_LevelMeasure_InputTriggerId_Reset, "Reset");
prototype.addInputTrigger(OVP_Algorithm_LevelMeasure_InputTriggerId_Refresh, "Refresh");
prototype.addOutputTrigger(OVP_Algorithm_LevelMeasure_OutputTriggerId_Refreshed, "Refreshed");
return true;
}
_IsDerivedFromClass_Final_(IAlgorithmDesc, OVP_ClassId_Algorithm_LevelMeasureDesc)
};
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,643 @@
#include "ovpCBoxAlgorithmErpPlot.h"
#include <boost/lexical_cast.hpp>
#include "../utils.h"
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
static void EventHandler(GtkWidget* widget, const gint width, const gint height, gpointer data)
{
auto* graphs = reinterpret_cast<std::list<Graph*>*>(data);
for (auto it = graphs->begin(); it != graphs->end(); ++it)
{
(*it)->resizeAxis(width, height, graphs->size());
(*it)->draw(widget);
}
}
static void OnConfigureEvent(GtkWidget* widget, GdkEventConfigure* event, gpointer data)
{
//std::cout << "OnConfigureEvent"<<event->width<<" "<< event->height<<"on widget "<<widget->allocation.width<<" "<<widget->allocation.height << "\n";
gtk_widget_queue_draw_area(widget, 0, 0, event->width, event->height);
EventHandler(widget, event->width, event->height, data);
}
/*
static gboolean on_resize_event(GtkWidget *widget, GdkRectangle * event, gpointer data)
{
//std::cout << "on_resize_event" << "\n";
EventHandler(widget, event->width, event->height, data);
return TRUE;
}
//*/
static gboolean OnExposeEvent(GtkWidget* widget, GdkEventExpose* event, gpointer data)
{
//std::cout << "OnExposeEvent" <<event->area.width<<" "<< event->area.height<<"on widget "<<widget->allocation.width<<" "<<widget->allocation.height<< "\n";
//gtk_widget_queue_draw_area(widget,0, 0,event->area.width, event->area.height );
EventHandler(widget, event->area.width, event->area.height, data);
return FALSE;
}
static void RenderText(cairo_t* cr, const char* text, const double x, const double y)
{
cairo_identity_matrix(cr);
cairo_translate(cr, x, y); // set the origin of cairo instance 'cr' to (10,20) (i.e. this is where drawing will start from).
PangoLayout* layout = pango_cairo_create_layout(cr); // init pango layout ready for use
pango_layout_set_text(layout, text, -1); // sets the text to be associated with the layout (final arg is length, -1 to calculate automatically when passing a nul-terminated string)
PangoFontDescription* desc = pango_font_description_from_string("Sans Bold 10"); // specify the font that would be ideal for your particular use
pango_layout_set_font_description(layout, desc); // assign the previous font description to the layout
pango_font_description_free(desc); // free the description
pango_cairo_update_layout(cr, layout); // if the target surface or transformation properties of the cairo instance have changed, update the pango layout to reflect this
pango_cairo_show_layout(cr, layout); // draw the pango layout onto the cairo surface // mandatory
g_object_unref(layout); // free the layout
}
void Graph::resizeAxis(const gint width, const gint height, const size_t nrOfGraphs)
{
size_t nrOfRows = size_t(ceil(sqrt(double(nrOfGraphs))));
const size_t nrOfColumns = nrOfRows;
if (nrOfGraphs <= (nrOfRows - 1) * nrOfRows) { nrOfRows--; }
this->m_GraphWidth = double(width) / double(nrOfColumns);
this->m_GraphHeight = double(height) / double(nrOfRows);
this->m_GraphOriginX = this->m_GraphWidth * double(this->m_ColIdx);
this->m_GraphOriginY = this->m_GraphHeight * double(this->m_RowIdx);
//std::cout << "resizeAxis: origin x: " << m_GraphOriginX << ", origin y: " << m_GraphOriginY << ", width: " << m_GraphWidth << ", height: " << m_GraphHeight << "\n";
}
void Graph::draw(GtkWidget* widget)//cairo_t * cairoContext)
{
cairo_t* cairoContext = gdk_cairo_create(widget->window);
cairo_set_line_width(cairoContext, 1);
cairo_translate(cairoContext, m_GraphOriginX + 20, m_GraphOriginY + 20);
cairo_scale(cairoContext, m_GraphWidth - 40, m_GraphHeight - 40);
cairo_save(cairoContext);
drawAxis(cairoContext);
cairo_restore(cairoContext);
cairo_save(cairoContext);
drawLegend(cairoContext);
cairo_restore(cairoContext);
cairo_save(cairoContext);
drawVar(cairoContext);
cairo_restore(cairoContext);
cairo_save(cairoContext);
drawCurves(cairoContext);
cairo_restore(cairoContext);
cairo_save(cairoContext);
drawAxisLabels(cairoContext);
cairo_restore(cairoContext);
cairo_destroy(cairoContext);
}
void Graph::drawAxis(cairo_t* ctx)
{
//make background white by drawing white rectangle
cairo_set_source_rgb(ctx, 1.0, 1.0, 1.0);
cairo_rectangle(ctx, 0, 0, 1, 1);
cairo_fill(ctx);
double ux = 1, uy = 1;
cairo_device_to_user_distance(ctx, &ux, &uy);
if (ux < uy) { ux = uy; }
cairo_set_line_width(ctx, ux);
cairo_set_source_rgb(ctx, 0, 0, 0);
//cairo_save(cairoContext);
//draw the horizontal line at zero if its inside the plotting region
const double zeroLevel = adjustValueToScale(0);
double xo = 0, yo = zeroLevel,
xe = 1.0, ye = zeroLevel;
if (std::fabs(zeroLevel) <= 1) { drawLine(ctx, &xo, &yo, &xe, &ye); }
// Draw y axis
double dXo = 0, dYo = 0, dXe = 0, dYe = 1.0;
drawLine(ctx, &dXo, &dYo, &dXe, &dYe);
}
void Graph::drawLine(cairo_t* ctx, double* xo, double* yo, double* xe, double* ye) const
{
cairo_save(ctx);
snapCoords(ctx, xo, yo);
snapCoords(ctx, xe, ye);
cairo_identity_matrix(ctx);
cairo_set_line_width(ctx, 1.0);
cairo_move_to(ctx, *xo, *yo);
cairo_line_to(ctx, *xe, *ye);
cairo_stroke(ctx);
cairo_restore(ctx);
}
void Graph::snapCoords(cairo_t* ctx, double* x, double* y) const
{
cairo_user_to_device(ctx, x, y);
*x = ceil(*x) + 0.5;
*y = ceil(*y) + 0.5;
}
void Graph::drawAxisLabels(cairo_t* ctx)
{
cairo_set_source_rgb(ctx, 0, 0, 0);
// If we haven't received any data yet, bail out
if (!(m_Minimum < FLT_MAX && m_Maximum > -FLT_MAX))
{
cairo_move_to(ctx, 0, 1);
double cx, cy;
cairo_get_current_point(ctx, &cx, &cy);
cairo_user_to_device(ctx, &cx, &cy);
cairo_save(ctx);
RenderText(ctx, "No data", cx, cy);
cairo_restore(ctx);
return;
}
// Note the scaling here should be compatible with adjustValueToScale.
const double graphMin = m_Minimum - m_Variance[m_ArgMinimum.first][m_ArgMinimum.second],
graphMax = m_Maximum + m_Variance[m_ArgMaximum.first][m_ArgMaximum.second];
// Including headroom may not be necessary as we are using adjustValueToScale to query where the Cairo drawing locations are, it'll take it into account
// GraphMin = GraphMin-0.10f*std::fabs(GraphMin);
// GraphMax = GraphMax+0.10f*std::fabs(GraphMax);
const size_t numSteps = 10;
const double dataRange = graphMax - graphMin,
stepSize = dataRange / numSteps;
// Find a starting point in y so that stepping will pass through 0
const double startY = floor(graphMin / stepSize) * stepSize;
for (size_t i = 0; i <= numSteps; ++i)
{
const double valueAtTick = startY + i * stepSize,
y = adjustValueToScale(valueAtTick);
cairo_move_to(ctx, 0, y);
double cx, cy;
cairo_get_current_point(ctx, &cx, &cy);
//std::cout<<"current point "<<cx<<" "<<cy<<"\n";
cairo_user_to_device(ctx, &cx, &cy);
//std::cout<<"device current point "<<cx<<" "<<cy<<"\n";
std::stringstream ss;
ss.precision(2);
ss << valueAtTick;
cairo_save(ctx);
RenderText(ctx, ss.str().c_str(), cx, cy);
cairo_restore(ctx);
}
const uint64_t xBegin = this->m_StartTime, xEnd = this->m_EndTime;
for (double x = 0; x <= 1; x += 0.2)
{
cairo_move_to(ctx, x, 1);
double cx, cy;
cairo_get_current_point(ctx, &cx, &cy);
//std::cout<<"current point "<<cx<<" "<<cy<<"\n";
cairo_user_to_device(ctx, &cx, &cy);
//X value to print range from XBegin to XEnd
const double dataLengthSecs = CTime(xEnd - xBegin).toSeconds(),
dataStart = CTime(xBegin).toSeconds();
std::stringstream ss;
ss.precision(2);
ss << (dataLengthSecs * x + dataStart);
cairo_save(ctx);
RenderText(ctx, ss.str().c_str(), cx, cy);
cairo_restore(ctx);
}
}
void Graph::drawCurves(cairo_t* ctx)
{
double ux = 1, uy = 1;
cairo_device_to_user_distance(ctx, &ux, &uy);
if (ux < uy) { ux = uy; }
cairo_set_line_width(ctx, ux);
for (size_t gi = 0; gi < m_Curves.size(); ++gi)
{
cairo_set_source_rgb(ctx, double(m_LineColor[gi].red) / 65535.0, double(m_LineColor[gi].green) / 65535.0, double(m_LineColor[gi].blue) / 65535.0);
const std::vector<double>& curve = m_Curves[gi];
//center
double y = adjustValueToScale(curve[0]),
x = 0.0;
cairo_move_to(ctx, x, y);
for (int si = 1; si < m_CurveSize; ++si)
{
y = adjustValueToScale(curve[si]);
x = (double(si)) / (double(m_CurveSize));
cairo_line_to(ctx, x, y);
}
cairo_save(ctx);
cairo_identity_matrix(ctx);
cairo_set_line_width(ctx, 1.0);
cairo_stroke(ctx);
cairo_restore(ctx);
}
}
void Graph::drawVar(cairo_t* ctx)
{
double ux = 1, uy = 1;
cairo_device_to_user_distance(ctx, &ux, &uy);
if (ux < uy) { ux = uy; }
cairo_set_line_width(ctx, ux);
for (size_t gi = 0; gi < m_Curves.size(); ++gi)
{
cairo_set_source_rgba(ctx, double(m_LineColor[gi].red) / 65535.0, double(m_LineColor[gi].green) / 65535.0, double(m_LineColor[gi].blue) / 65535.0, 0.5);
const std::vector<double>& curves = m_Curves[gi];
const std::vector<double>& variances = m_Variance[gi];
// Test first if we have any variance at all, if not, don't bother drawing as cairo slows down with tiny apertures
if (std::none_of(variances.begin(), variances.end(), [](const double a) { return a > 0; })) { continue; }
double var = variances[0];
double y = adjustValueToScale(curves[0] - var);
double x = 0.0;
cairo_move_to(ctx, x, y);
// Draw variance below the data points
for (int si = 1; si < m_CurveSize; ++si)
{
var = variances[si];
y = adjustValueToScale(curves[si] - var);
x = (double(si)) / (double(m_CurveSize));
cairo_line_to(ctx, x, y);
}
// Draw the last point separately
cairo_line_to(ctx, x, y + 2 * var);
// Draw variance above the points, including the first point
for (int si = m_CurveSize - 1; si >= 0; si--)
{
var = variances[si];
y = adjustValueToScale(curves[si] + var);
x = (double(si)) / (double(m_CurveSize));
cairo_line_to(ctx, x, y);
}
// Fill the surrounded region?
cairo_fill(ctx);
}
}
void Graph::drawLegend(cairo_t* ctx) const
{
double ux = 1, uy = 1;
cairo_device_to_user_distance(ctx, &ux, &uy);
cairo_select_font_face(ctx, "Sans Bold 10", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
cairo_set_font_size(ctx, 0.04);
double yTotal = 0;
for (size_t gi = 0; gi < m_Curves.size(); ++gi)
{
cairo_set_source_rgb(ctx, double(m_LineColor[gi].red) / 65535.0, double(m_LineColor[gi].green) / 65535.0,
double(m_LineColor[gi].blue) / 65535.0);
cairo_text_extents_t extents;
cairo_text_extents(ctx, m_LineText[gi].toASCIIString(), &extents);
yTotal += extents.height + 0.02;
// size_t len = m_LineText[gi].length();
cairo_move_to(ctx, (1.0 - extents.width - 0.01), yTotal);
double cx, cy;
cairo_get_current_point(ctx, &cx, &cy);
//std::cout<<"current point "<<cx<<" "<<cy<<"\n";
cairo_user_to_device(ctx, &cx, &cy);
//std::cout<<m_LineText[gi]<<"\n";
cairo_save(ctx);
cairo_show_text(ctx, m_LineText[gi].toASCIIString());
cairo_restore(ctx);
}
}
double Graph::adjustValueToScale(const double value)
{
//std::cout<<m_pVariance[m_ArgMinimum.first][m_ArgMinimum.second] <<" "<<m_pVariance[m_ArgMaximum.first][m_ArgMaximum.second]<<"\n";
double graphMin = m_Minimum - m_Variance[m_ArgMinimum.first][m_ArgMinimum.second];
double graphMax = m_Maximum + m_Variance[m_ArgMaximum.first][m_ArgMaximum.second];
graphMin = graphMin - 0.10 * std::fabs(graphMin);
graphMax = graphMax + 0.10 * std::fabs(graphMax);
return (graphMin - value) / (graphMax - graphMin) + 1.0;
}
void Graph::updateCurves(const double* curve, const size_t howMany, const size_t curveIndex)
{
m_Curves[curveIndex].assign(curve, curve + howMany);
m_Maximum = -FLT_MAX;
m_Minimum = FLT_MAX;
for (size_t j = 0; j < m_Curves.size(); ++j)
{
for (int i = 0; i < m_CurveSize; ++i)
{
//m_Maximum = m_lCurves[j][i]>m_Maximum ? m_lCurves[j][i]:m_Maximum;
if (m_Curves[j][i] > m_Maximum)
{
m_Maximum = m_Curves[j][i];
m_ArgMaximum.first = j;
m_ArgMaximum.second = i;
}
//m_Minimum = m_lCurves[j][i]<m_Minimum ? m_lCurves[j][i]:m_Minimum;
if (m_Curves[j][i] < m_Minimum)
{
m_Minimum = m_Curves[j][i];
m_ArgMinimum.first = j;
m_ArgMinimum.second = i;
}
}
//m_Maximum = m_Maximum+0.01f*std::fabs(m_Maximum);
//m_Minimum = m_Minimum-0.01f*std::fabs(m_Minimum);
}
}
bool CBoxAlgorithmErpPlot::initialize()
{
// If you need to retrieve setting values, use the FSettingValueAutoCast function.
const Kernel::IBox& boxContext = this->getStaticBoxContext();
m_figureFileName = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 0);
m_triggerToSave = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 1);
m_xStartsAt0 = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 2);
const size_t inputParamsStartAt = 3;
m_graphList = new std::list<Graph*>;
//should be a Graph per channel/electrode not per input (should be done when first header is received)
for (size_t i = 1; i < boxContext.getInputCount(); ++i)
{
if ((i % 2) == 1)
{
const size_t c = i / 2;
m_legendColors.push_back(CGdkcolorAutoCast(boxContext, this->getConfigurationManager(), inputParamsStartAt + 2 * c + 0));
m_legend.push_back(FSettingValueAutoCast(*this->getBoxAlgorithmContext(), inputParamsStartAt + 2 * c + 1));
m_decoders.push_back(new Toolkit::TStreamedMatrixDecoder<CBoxAlgorithmErpPlot>(*this, i));
}
else { m_varianceDecoders.push_back(new Toolkit::TStreamedMatrixDecoder<CBoxAlgorithmErpPlot>(*this, i)); }
}
m_stimulationDecoder = new Toolkit::TStimulationDecoder<CBoxAlgorithmErpPlot>(*this, 0);
//*
//initialize graphic component
GtkBuilder* widget = gtk_builder_new(); // glade_xml_new(m_interfaceFilename.toASCIIString(), "p300-speller-toolbar", nullptr);
GError* error = nullptr;
this->getLogManager() << Kernel::LogLevel_Trace << "Path to erp.ui " << Directories::getDataDir() + CString("/plugins/simple-visualization/erp-plot.ui\n");
gtk_builder_add_from_file(widget, Directories::getDataDir() + "/plugins/simple-visualization/erp-plot.ui", &error);
m_drawWindow = GTK_WIDGET(gtk_builder_get_object(widget, "plot-window"));
m_visualizationCtx = dynamic_cast<VisualizationToolkit::IVisualizationContext*>(this->createPluginObject(
OVP_ClassId_Plugin_VisualizationCtx));
m_visualizationCtx->setWidget(*this, m_drawWindow);
g_signal_connect(m_drawWindow, "expose-event", G_CALLBACK (OnExposeEvent), m_graphList);
g_signal_connect(m_drawWindow, "configure-event", G_CALLBACK (OnConfigureEvent), m_graphList);
gtk_widget_show_all(m_drawWindow);
//*/
m_firstHeaderReceived = false;
return true;
}
bool CBoxAlgorithmErpPlot::uninitialize()
{
for (size_t i = 0; i < m_decoders.size(); ++i)
{
m_decoders[i]->uninitialize();
m_varianceDecoders[i]->uninitialize();
}
m_stimulationDecoder->uninitialize();
if (m_drawWindow)
{
gtk_widget_destroy(m_drawWindow);
m_drawWindow = nullptr;
}
while (!m_graphList->empty()) { delete m_graphList->front(), m_graphList->pop_front(); }
if (m_visualizationCtx)
{
this->releasePluginObject(m_visualizationCtx);
m_visualizationCtx = nullptr;
}
return true;
}
bool CBoxAlgorithmErpPlot::processInput(const size_t /*index*/)
{
getBoxAlgorithmContext()->markAlgorithmAsReadyToProcess();
return true;
}
//saving the graph in png images
bool CBoxAlgorithmErpPlot::save()
{
cairo_t* cairoContext = gdk_cairo_create(m_drawWindow->window);
//the main surface
cairo_surface_t* surface = cairo_get_target(cairoContext);
//building filename
const std::string extension = ".png";
for (auto it = m_graphList->begin(); it != m_graphList->end(); ++it)
{
//cutting this graph
cairo_surface_t* subsurface = cairo_surface_create_for_rectangle(surface, (*it)->m_GraphOriginX, (*it)->m_GraphOriginY, (*it)->m_GraphWidth,
(*it)->m_GraphHeight);
std::stringstream filename;
//creating filename
filename << m_figureFileName << ((*it)->m_RowIdx) << "_" << ((*it)->m_ColIdx) << extension;
this->getLogManager() << Kernel::LogLevel_Info << "Saving [" << filename.str() << "] \n";
cairo_surface_write_to_png(subsurface, filename.str().c_str());
}
return true;
}
bool CBoxAlgorithmErpPlot::process()
{
Kernel::IBoxIO& dynamicBoxContext = this->getDynamicBoxContext();
const Kernel::IBox& staticBoxContext = this->getStaticBoxContext();
//listen for stimulation input
for (size_t i = 0; i < dynamicBoxContext.getInputChunkCount(0); ++i)
{
m_stimulationDecoder->decode(i);
if (m_stimulationDecoder->isBufferReceived())
{
IStimulationSet* stimSet = m_stimulationDecoder->getOutputStimulationSet();
for (size_t j = 0; j < stimSet->getStimulationCount(); ++j)
{
if (stimSet->getStimulationIdentifier(j) == m_triggerToSave)
{
this->getLogManager() << Kernel::LogLevel_Trace << "Saving\n";
save();
}
}
}
}
bool dataChanged = false;
for (size_t inputi = 1; inputi < staticBoxContext.getInputCount(); ++inputi)
{
for (size_t i = 0; i < dynamicBoxContext.getInputChunkCount(inputi); ++i)
{
if ((inputi) % 2 == 1)
{
m_decoders[inputi / 2]->decode(i);
if (m_decoders[inputi / 2]->isHeaderReceived() && !m_firstHeaderReceived)
{
const size_t nElectrodes = m_decoders[inputi / 2]->getOutputMatrix()->getDimensionSize(0);
const auto nCols = size_t(ceil(sqrt(double(nElectrodes))));
//create list of graph subplots
for (size_t dimi = 0; dimi < nElectrodes; ++dimi)
{
auto* graph = new Graph(m_legendColors, m_legend, size_t(floor(float(dimi) / nCols)),
size_t(dimi % nCols), m_decoders[inputi / 2]->getOutputMatrix()->getDimensionSize(1));
m_graphList->push_back(graph);
}
//draw the empty graphs
for (auto it = m_graphList->begin(); it != m_graphList->end(); ++it)
{
(*it)->m_StartTime = 0;
(*it)->m_EndTime = 1;
//(*it)->m_pVariance = nullptr;
cairo_t* cairoContext = gdk_cairo_create(m_drawWindow->window);
(*it)->resizeAxis(400, 400, m_graphList->size());//default init size
(*it)->drawAxis(cairoContext);
cairo_destroy(cairoContext);
cairo_t* cairoContext2 = gdk_cairo_create(m_drawWindow->window);
(*it)->drawAxisLabels(cairoContext2);
cairo_destroy(cairoContext2);
}
m_firstHeaderReceived = true;
dataChanged = true;
}
if (m_decoders[inputi / 2]->isBufferReceived())
{
const uint64_t startTime = dynamicBoxContext.getInputChunkStartTime(inputi, i),
endTime = dynamicBoxContext.getInputChunkEndTime(inputi, i);
//redraw all
//gtk_widget_queue_draw(m_drawWindow);
CMatrix* matrix = m_decoders[inputi / 2]->getOutputMatrix();
const size_t nElectrodes = matrix->getDimensionSize(0),
nSamples = matrix->getDimensionSize(1);
auto it = m_graphList->begin();
for (size_t dimi = 0; dimi < nElectrodes; dimi++, ++it)
{
const double* ptr = matrix->getBuffer() + dimi * nSamples;
(*it)->updateCurves(ptr, nSamples, inputi / 2);
//std::cout << "update curve " << inputi/2 << " beginning value " << destinationMatrix[0] << ", second value " << destinationMatrix[42] << "\n";
(*it)->m_StartTime = (m_xStartsAt0 ? 0 : startTime);
(*it)->m_EndTime = (m_xStartsAt0 ? (endTime - startTime) : endTime);
//(*graphIterator)->draw(m_drawWindow);
}
dataChanged = true;
dynamicBoxContext.markInputAsDeprecated(inputi, i);
}
//if(m_decoders[inputi/2]->isEndReceived()) { }
}
else
{
//std::cout<<" variance input"<<(inputi/2-1)<<"\n";
m_varianceDecoders[inputi / 2 - 1]->decode(i);
if (m_varianceDecoders[inputi / 2 - 1]->isBufferReceived())
{
CMatrix* matrix = m_varianceDecoders[inputi / 2 - 1]->getOutputMatrix();
const size_t nSamples = matrix->getDimensionSize(1),
nElectrodes = matrix->getDimensionSize(0);
auto it = m_graphList->begin();
for (size_t dimi = 0; dimi < nElectrodes; dimi++, ++it)
{
const double* ptr = matrix->getBuffer() + dimi * nSamples;
(*it)->m_Variance[inputi / 2 - 1].assign(ptr, ptr + nSamples);
}
dataChanged = true;
dynamicBoxContext.markInputAsDeprecated(inputi, i);
}
}
}
}
//redraw all?
if (dataChanged)
{
gtk_widget_queue_draw(m_drawWindow);
for (auto* it : *m_graphList) { it->draw(m_drawWindow); }
}
return true;
}
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,212 @@
#pragma once
//You may have to change this path to match your folder organisation
#include "../ovp_defines.h"
#include <openvibe/ov_all.h>
#include <toolkit/ovtk_all.h>
#include <visualization-toolkit/ovviz_all.h>
#include <cairo.h>
#include <cfloat>
#include <gdk/gdk.h>
#include <gtk/gtk.h>
#include <iostream>
#include <list>
#include <utility>
#include <vector>
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
class Graph
{
public:
//should be a list of colors;
Graph(std::vector<GdkColor>& lineColor, std::vector<CString>& lineText, const int rowIndex, const int colIndex, const int curveSize)
: m_LineColor(lineColor), m_LineText(lineText)
{
m_Curves.resize(lineColor.size());
m_Variance.resize(lineColor.size());
for (size_t i = 0; i < m_Curves.size(); ++i)
{
m_Curves[i].resize(curveSize, 0);
m_Variance[i].resize(curveSize, 0);
}
this->m_RowIdx = rowIndex;
this->m_ColIdx = colIndex;
this->m_CurveSize = curveSize;
m_PointCounter = new int[lineColor.size()];
m_Maximum = -FLT_MAX;
m_Minimum = FLT_MAX;
for (size_t i = 0; i < lineColor.size(); ++i) { m_PointCounter[i] = 0; }
}
~Graph()
{
delete [] m_PointCounter;
m_Curves.clear();
m_Variance.clear();
}
void resizeAxis(gint width, gint height, size_t nrOfGraphs);
void draw(GtkWidget* widget);
void drawAxis(cairo_t* ctx);
void drawLine(cairo_t* ctx, double* xo, double* yo, double* xe, double* ye) const;
void drawAxisLabels(cairo_t* ctx);
void drawCurves(cairo_t* ctx);
void drawLegend(cairo_t* ctx) const;
void drawVar(cairo_t* ctx);
void updateCurves(const double* curve, size_t howMany, size_t curveIndex);
void snapCoords(cairo_t* ctx, double* x, double* y) const;
double adjustValueToScale(double value);
std::vector<std::vector<double>> m_Curves; //private
std::vector<std::vector<double>> m_Variance;
std::vector<GdkColor>& m_LineColor; //private
std::vector<CString>& m_LineText; //private
double m_Maximum = 1;
double m_Minimum = -1;
std::pair<int, int> m_ArgMaximum;
std::pair<int, int> m_ArgMinimum;
double m_GraphWidth = 0;
double m_GraphHeight = 0;
double m_GraphOriginX = 0;
double m_GraphOriginY = 0;
int m_RowIdx = 0; //private
int m_ColIdx = 0; //private
int m_CurveSize = 0; //private
int* m_PointCounter = nullptr;
uint64_t m_StartTime = 0;
uint64_t m_EndTime = 0;
double m_FontSize = 1.0;
};
/**
* \class CBoxAlgorithmErpPlot
* \author Dieter Devlaminck (INRIA)
* \date Fri Nov 16 10:50:43 2012
* \brief The class CBoxAlgorithmErpPlot describes the box ERP plot.
*
*/
class CBoxAlgorithmErpPlot final : virtual public Toolkit::TBoxAlgorithm<IBoxAlgorithm>
{
public:
void release() override { delete this; }
bool initialize() override;
bool uninitialize() override;
bool processInput(const size_t index) override;
bool process() override;
bool save();
_IsDerivedFromClass_Final_(Toolkit::TBoxAlgorithm<IBoxAlgorithm>, OVP_ClassId_BoxAlgorithm_ErpPlot)
protected:
CString m_figureFileName;
std::vector<GdkColor> m_legendColors;
std::vector<CString> m_legend;
GtkWidget* m_drawWindow = nullptr;
std::list<Graph*>* m_graphList = nullptr;
bool m_firstHeaderReceived = false;
std::vector<Toolkit::TStreamedMatrixDecoder<CBoxAlgorithmErpPlot>*> m_decoders;
std::vector<Toolkit::TStreamedMatrixDecoder<CBoxAlgorithmErpPlot>*> m_varianceDecoders;
Toolkit::TStimulationDecoder<CBoxAlgorithmErpPlot>* m_stimulationDecoder = nullptr;
uint64_t m_triggerToSave = 0;
bool m_xStartsAt0 = false;
private:
VisualizationToolkit::IVisualizationContext* m_visualizationCtx = nullptr;
};
// The box listener can be used to call specific callbacks whenever the box structure changes : input added, name changed, etc.
// Please uncomment below the callbacks you want to use.
class CBoxAlgorithmErpPlotListener final : public Toolkit::TBoxListener<IBoxListener>
{
public:
bool onInputAdded(Kernel::IBox& box, const size_t index) override
{
box.setInputType(index, OV_TypeId_StreamedMatrix);
const size_t c = index / 2 + 1;
const std::string idx = std::to_string(c), iLabel = "ERP ",
colorLabel = "Line color ", textLabel = "Line label ", varianceLabel = "Variance ";
box.setInputName(index, (iLabel + idx).c_str());
box.addSetting((colorLabel + idx).c_str(),OV_TypeId_Color, "0,0,0");
box.addSetting((textLabel + idx).c_str(),OV_TypeId_String, "curve");
//add the corresponding variance input
box.addInput((varianceLabel + idx).c_str(), OV_TypeId_StreamedMatrix);
return true;
}
bool onInputRemoved(Kernel::IBox& box, const size_t index) override
{
box.removeSetting(index * 2 + 2);
box.removeSetting(index * 2 + 1);
return true;
}
_IsDerivedFromClass_Final_(Toolkit::TBoxListener<IBoxListener>, CIdentifier::undefined())
};
/**
* \class CBoxAlgorithmErpPlotDesc
* \author Dieter Devlaminck (INRIA)
* \date Fri Nov 16 10:50:43 2012
* \brief Descriptor of the box ERP plot.
*
*/
class CBoxAlgorithmErpPlotDesc final : virtual public IBoxAlgorithmDesc
{
public:
void release() override { }
CString getName() const override { return CString("ERP plot"); }
CString getAuthorName() const override { return CString("Dieter Devlaminck"); }
CString getAuthorCompanyName() const override { return CString("INRIA"); }
CString getShortDescription() const override { return CString("Plots event-related potentials"); }
CString getDetailedDescription() const override { return CString("plots target ERP versus non-target ERP"); }
CString getCategory() const override { return CString("Visualization/Presentation"); }
CString getVersion() const override { return CString("1.1"); }
CString getStockItemName() const override { return CString(""); }
CIdentifier getCreatedClass() const override { return OVP_ClassId_BoxAlgorithm_ErpPlot; }
IPluginObject* create() override { return new CBoxAlgorithmErpPlot; }
bool hasFunctionality(const EPluginFunctionality functionality) const override { return functionality == EPluginFunctionality::Visualization; }
IBoxListener* createBoxListener() const override { return new CBoxAlgorithmErpPlotListener; }
void releaseBoxListener(IBoxListener* listener) const override { delete listener; }
bool getBoxPrototype(Kernel::IBoxProto& prototype) const override
{
prototype.addInput("Trigger",OV_TypeId_Stimulations);
prototype.addInput("ERP1",OV_TypeId_StreamedMatrix);
prototype.addInput("Variance1",OV_TypeId_StreamedMatrix);
prototype.addFlag(Kernel::BoxFlag_CanAddInput);
prototype.addSetting("Filename final figure",OV_TypeId_Filename, "");
prototype.addSetting("Trigger to save figure",OV_TypeId_Stimulation, "OVTK_StimulationId_ExperimentStop");
prototype.addSetting("X starts at 0",OV_TypeId_Boolean, "true");
prototype.addSetting("Line color 1",OV_TypeId_Color, "0,0,0");
prototype.addSetting("Line label 1",OV_TypeId_String, "curve 1");
prototype.addFlag(OV_AttributeId_Box_FlagIsUnstable);
return true;
}
_IsDerivedFromClass_Final_(IBoxAlgorithmDesc, OVP_ClassId_BoxAlgorithm_ErpPlotDesc)
};
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,122 @@
#include "ovpCBoxAlgorithmLevelMeasure.h"
#include "../algorithms/ovpCAlgorithmLevelMeasure.h"
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
bool CBoxAlgorithmLevelMeasure::initialize()
{
m_matrix = new CMatrix();
m_matrixDecoder = &getAlgorithmManager().getAlgorithm(getAlgorithmManager().createAlgorithm(OVP_GD_ClassId_Algorithm_StreamedMatrixDecoder));
m_levelMeasure = &getAlgorithmManager().getAlgorithm(getAlgorithmManager().createAlgorithm(OVP_ClassId_Algorithm_LevelMeasure));
m_matrixDecoder->initialize();
m_levelMeasure->initialize();
m_matrixBuffer.initialize(m_matrixDecoder->getInputParameter(OVP_GD_Algorithm_StreamedMatrixDecoder_InputParameterId_MemoryBufferToDecode));
m_matrixHandler.initialize(m_matrixDecoder->getOutputParameter(OVP_GD_Algorithm_StreamedMatrixDecoder_OutputParameterId_Matrix));
m_levelMeasureMatrix.initialize(m_levelMeasure->getInputParameter(OVP_Algorithm_LevelMeasure_InputParameterId_Matrix));
m_levelMeasureMainWidget.initialize(m_levelMeasure->getOutputParameter(OVP_Algorithm_LevelMeasure_OutputParameterId_MainWidget));
m_levelMeasureToolbarWidget.initialize(m_levelMeasure->getOutputParameter(OVP_Algorithm_LevelMeasure_OutputParameterId_ToolbarWidget));
m_matrixHandler.setReferenceTarget(m_matrix);
m_levelMeasureMatrix.setReferenceTarget(m_matrix);
return true;
}
bool CBoxAlgorithmLevelMeasure::uninitialize()
{
m_levelMeasureToolbarWidget.uninitialize();
m_levelMeasureMainWidget.uninitialize();
m_levelMeasureMatrix.uninitialize();
m_matrixHandler.uninitialize();
m_matrixBuffer.uninitialize();
m_levelMeasure->uninitialize();
m_matrixDecoder->uninitialize();
getAlgorithmManager().releaseAlgorithm(*m_levelMeasure);
getAlgorithmManager().releaseAlgorithm(*m_matrixDecoder);
delete m_matrix;
m_matrix = nullptr;
if (m_visualizationCtx)
{
this->releasePluginObject(m_visualizationCtx);
m_visualizationCtx = nullptr;
}
return true;
}
bool CBoxAlgorithmLevelMeasure::processInput(const size_t /*index*/)
{
getBoxAlgorithmContext()->markAlgorithmAsReadyToProcess();
return true;
}
bool CBoxAlgorithmLevelMeasure::process()
{
Kernel::IBoxIO& boxContext = this->getDynamicBoxContext();
for (size_t i = 0; i < boxContext.getInputChunkCount(0); ++i)
{
m_matrixBuffer = boxContext.getInputChunk(0, i);
m_matrixDecoder->process();
if (m_matrixDecoder->isOutputTriggerActive(OVP_GD_Algorithm_StreamedMatrixDecoder_OutputTriggerId_ReceivedHeader))
{
m_levelMeasure->process(OVP_Algorithm_LevelMeasure_InputTriggerId_Reset);
m_visualizationCtx = dynamic_cast<VisualizationToolkit::IVisualizationContext*>(this->createPluginObject(
OVP_ClassId_Plugin_VisualizationCtx));
m_visualizationCtx->setWidget(*this, m_levelMeasureMainWidget);
m_visualizationCtx->setToolbar(*this, m_levelMeasureToolbarWidget);
}
if (m_matrixDecoder->isOutputTriggerActive(OVP_GD_Algorithm_StreamedMatrixDecoder_OutputTriggerId_ReceivedBuffer))
{
// ----- >8 ------------------------------------------------------------------------------------------------------------------------------------------------------
// should be done in a processing box !
double sum = 0;
{
double* buffer = m_matrix->getBuffer();
size_t n = m_matrix->getBufferElementCount();
while (n--)
{
sum += *buffer;
buffer++;
}
}
{
const double factor = (sum != 0 ? 1. / sum : 0.5);
double* buffer = m_matrix->getBuffer();
size_t n = m_matrix->getBufferElementCount();
while (n--)
{
*buffer *= factor;
buffer++;
}
}
// ----- >8 ------------------------------------------------------------------------------------------------------------------------------------------------------
m_levelMeasure->process(OVP_Algorithm_LevelMeasure_InputTriggerId_Refresh);
}
if (m_matrixDecoder->isOutputTriggerActive(OVP_GD_Algorithm_StreamedMatrixDecoder_OutputTriggerId_ReceivedEnd)) { }
boxContext.markInputAsDeprecated(0, i);
}
return true;
}
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,72 @@
#pragma once
#include "../ovp_defines.h"
#include <openvibe/ov_all.h>
#include <toolkit/ovtk_all.h>
#include <visualization-toolkit/ovviz_all.h>
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
class CBoxAlgorithmLevelMeasure final : public Toolkit::TBoxAlgorithm<IBoxAlgorithm>
{
public:
void release() override { delete this; }
bool initialize() override;
bool uninitialize() override;
bool processInput(const size_t index) override;
bool process() override;
_IsDerivedFromClass_Final_(Toolkit::TBoxAlgorithm<IBoxAlgorithm>, OVP_ClassId_BoxAlgorithm_LevelMeasure)
protected:
Kernel::IAlgorithmProxy* m_matrixDecoder = nullptr;
Kernel::IAlgorithmProxy* m_levelMeasure = nullptr;
Kernel::TParameterHandler<const IMemoryBuffer*> m_matrixBuffer;
Kernel::TParameterHandler<CMatrix*> m_matrixHandler;
Kernel::TParameterHandler<CMatrix*> m_levelMeasureMatrix;
Kernel::TParameterHandler<GtkWidget*> m_levelMeasureMainWidget;
Kernel::TParameterHandler<GtkWidget*> m_levelMeasureToolbarWidget;
CMatrix* m_matrix = nullptr;
private:
VisualizationToolkit::IVisualizationContext* m_visualizationCtx = nullptr;
};
class CBoxAlgorithmLevelMeasureDesc final : public IBoxAlgorithmDesc
{
public:
void release() override { }
CString getName() const override { return CString("Level measure"); }
CString getAuthorName() const override { return CString("Yann Renard"); }
CString getAuthorCompanyName() const override { return CString("INRIA/IRISA"); }
CString getShortDescription() const override { return CString(""); }
CString getDetailedDescription() const override { return CString(""); }
CString getCategory() const override { return CString("Visualization/Basic"); }
CString getVersion() const override { return CString("1.0"); }
CString getStockItemName() const override { return CString("gtk-go-up"); }
CIdentifier getCreatedClass() const override { return OVP_ClassId_BoxAlgorithm_LevelMeasure; }
IPluginObject* create() override { return new CBoxAlgorithmLevelMeasure; }
bool hasFunctionality(const EPluginFunctionality functionality) const override { return functionality == EPluginFunctionality::Visualization; }
bool getBoxPrototype(Kernel::IBoxProto& prototype) const override
{
prototype.addInput("Input matrix to display", OV_TypeId_StreamedMatrix);
return true;
}
_IsDerivedFromClass_Final_(IBoxAlgorithmDesc, OVP_ClassId_BoxAlgorithm_LevelMeasureDesc)
};
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,485 @@
#include "ovpCBoxAlgorithmP300IdentifierCardVisualization.h"
#include "../utils.h"
#include <iomanip>
#include <list>
#include <string>
#include <vector>
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
bool CBoxAlgorithmP300IdentifierCardVisualization::initialize()
{
const Kernel::IBox& boxContext = this->getStaticBoxContext();
m_mainWidgetInterface = nullptr;
//get value of settings given in the configuration box
m_interfaceFilename = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 0);
m_bgColor = CGdkcolorAutoCast(getStaticBoxContext(), getConfigurationManager(), 1);
m_targetBgColor = CGdkcolorAutoCast(getStaticBoxContext(), getConfigurationManager(), 2);
m_selectedBgColor = CGdkcolorAutoCast(getStaticBoxContext(), getConfigurationManager(), 3);
m_cardStimulationBase = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 4);
// ----------------------------------------------------------------------------------------------------------------------------------------------------------
m_sequenceStimulationDecoder = &this->getAlgorithmManager().getAlgorithm(
this->getAlgorithmManager().createAlgorithm(OVP_GD_ClassId_Algorithm_StimulationDecoder));
m_sequenceStimulationDecoder->initialize();
m_targetStimulationDecoder = &this->getAlgorithmManager().getAlgorithm(
this->getAlgorithmManager().createAlgorithm(OVP_GD_ClassId_Algorithm_StimulationDecoder));
m_targetStimulationDecoder->initialize();
m_targetFlaggingStimulationEncoder = &this->getAlgorithmManager().getAlgorithm(
this->getAlgorithmManager().createAlgorithm(OVP_GD_ClassId_Algorithm_StimulationEncoder));
m_targetFlaggingStimulationEncoder->initialize();
m_cardSelectionStimulationDecoder = &this->getAlgorithmManager().getAlgorithm(
this->getAlgorithmManager().createAlgorithm(OVP_GD_ClassId_Algorithm_StimulationDecoder));
m_cardSelectionStimulationDecoder->initialize();
m_sequenceMemoryBuffer.initialize(
m_sequenceStimulationDecoder->getInputParameter(OVP_GD_Algorithm_StimulationDecoder_InputParameterId_MemoryBufferToDecode));
m_sequenceStimulationSet.initialize(
m_sequenceStimulationDecoder->getOutputParameter(OVP_GD_Algorithm_StimulationDecoder_OutputParameterId_StimulationSet));
m_targetMemoryBuffer.initialize(
m_targetStimulationDecoder->getInputParameter(OVP_GD_Algorithm_StimulationDecoder_InputParameterId_MemoryBufferToDecode));
m_targetStimulationSet.initialize(
m_targetStimulationDecoder->getOutputParameter(OVP_GD_Algorithm_StimulationDecoder_OutputParameterId_StimulationSet));
m_targetFlaggingStimulationSet.initialize(
m_targetFlaggingStimulationEncoder->getInputParameter(OVP_GD_Algorithm_StimulationEncoder_InputParameterId_StimulationSet));
m_targetFlaggingMemoryBuffer.initialize(
m_targetFlaggingStimulationEncoder->getOutputParameter(OVP_GD_Algorithm_StimulationEncoder_OutputParameterId_EncodedMemoryBuffer));
m_lastTime = 0;
m_mainWidgetInterface = gtk_builder_new(); // glade_xml_new(m_interfaceFilename.toASCIIString(), "p300-Identifier-card-main", nullptr);
if (!gtk_builder_add_from_file(m_mainWidgetInterface, m_interfaceFilename.toASCIIString(), nullptr))
{
this->getLogManager() << Kernel::LogLevel_ImportantWarning << "Could not load interface file [" << m_interfaceFilename << "]\n";
this->getLogManager() << Kernel::LogLevel_ImportantWarning <<
"The file may be missing. However, the interface files now use gtk-builder instead of glade. Did you update your files ?\n";
return false;
}
// m_toolbarWidgetInterface=glade_xml_new(m_interfaceFilename.toASCIIString(), "p300-Identifier-card-toolbar", nullptr);
m_mainWindow = GTK_WIDGET(gtk_builder_get_object(m_mainWidgetInterface, "p300-Identifier-card-main"));
// m_toolbarWidget=gtk_builder_get_object(m_toolbarWidgetInterface, "p300-Identifier-card-toolbar");
m_table = GTK_TABLE(gtk_builder_get_object(m_mainWidgetInterface, "p300-Identifier-card-table"));
gtk_widget_modify_bg(m_mainWindow, GTK_STATE_NORMAL, &m_bgColor);
std::stringstream targetColor, selectColor;
targetColor << std::hex << std::setfill('0') << std::setw(2) << m_targetBgColor.red << std::setw(2) << m_targetBgColor.green << std::setw(2) <<
m_targetBgColor.blue;
selectColor << std::hex << std::setfill('0') << std::setw(2) << m_selectedBgColor.red << std::setw(2) << m_selectedBgColor.green << std::setw(2) <<
m_selectedBgColor.blue;
m_targetLabel = GTK_LABEL(gtk_builder_get_object(m_mainWidgetInterface, "labelTarget"));
gtk_label_set_label(m_targetLabel, (R"(<span weight="bold" size="xx-large" color="#)" + targetColor.str() + "\">Target</span>").c_str());
m_selectedLabel = GTK_LABEL(gtk_builder_get_object(m_mainWidgetInterface, "labelResult"));
gtk_label_set_label(m_selectedLabel, (R"(<span weight="bold" size="xx-large" color="#)" + targetColor.str() + "\">Selected</span>").c_str());
gtk_builder_connect_signals(m_mainWidgetInterface, nullptr);
// gtk_builder_connect_signals(m_toolbarWidgetInterface, nullptr);
m_visualizationCtx = dynamic_cast<VisualizationToolkit::IVisualizationContext*>(this->createPluginObject(
OVP_ClassId_Plugin_VisualizationCtx));
m_visualizationCtx->setWidget(*this, m_mainWindow);
// getVisualizationContext().setToolbar(m_toolbarWidget);
m_nCard = 0;
m_targetCard = -1;
m_tableInitialized = false;
this->cacheBuildFromTable(m_table);
GtkRequisition size;
gtk_widget_size_request(GTK_WIDGET(m_caches[1].widget), &size);
const gint widthWork = size.width;
const gint heightWork = size.height;
for (size_t i = 6; i < boxContext.getSettingCount(); ++i)
{
CString fgImageFilename = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), i);
if (fgImageFilename != CString(""))
{
GError* error = nullptr;
GdkPixbuf* tmp = gdk_pixbuf_new_from_file(fgImageFilename.toASCIIString(), &error);
GtkWidget* fgImageTarget = gtk_image_new_from_pixbuf(gdk_pixbuf_scale_simple(tmp, 192, 192, GDK_INTERP_BILINEAR));
g_object_unref(tmp);
gtk_widget_show(fgImageTarget);
g_object_ref(fgImageTarget);
m_fgImageTargets.push_back(fgImageTarget);
GtkWidget* fgImageWork = gtk_image_new_from_file(fgImageFilename.toASCIIString());
gtk_widget_show(fgImageWork);
g_object_ref(fgImageWork);
GdkPixbuf* srcPixbuf = gtk_image_get_pixbuf(GTK_IMAGE(fgImageWork));
GdkPixbuf* destPixbuf = gdk_pixbuf_scale_simple(srcPixbuf, widthWork, heightWork, GDK_INTERP_HYPER);
gtk_image_set_from_pixbuf(GTK_IMAGE(fgImageWork), destPixbuf);
m_fgImageWorks.push_back(fgImageWork);
GtkWidget* fgImageResult = gtk_image_new_from_file(fgImageFilename.toASCIIString());
gtk_widget_show(fgImageResult);
g_object_ref(fgImageResult);
m_fgImageResults.push_back(fgImageResult);
m_nCard++;
}
}
const CString bgImageFilename = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 5);
m_bgImageTarget = gtk_image_new_from_file((bgImageFilename + CString("-offscreen")).toASCIIString());
gtk_widget_show(m_bgImageTarget);
g_object_ref(m_bgImageTarget);
m_bgImageWork = gtk_image_new_from_file((bgImageFilename + CString("-offscreen")).toASCIIString());
gtk_widget_show(m_bgImageWork);
g_object_ref(m_bgImageWork);
GdkPixbuf* srcPixbuf = gtk_image_get_pixbuf(GTK_IMAGE(m_bgImageWork));
GdkPixbuf* destPixbuf = gdk_pixbuf_scale_simple(srcPixbuf, widthWork, heightWork, GDK_INTERP_HYPER);
gtk_image_set_from_pixbuf(GTK_IMAGE(m_bgImageWork), destPixbuf);
m_bgImageResult = gtk_image_new_from_file((bgImageFilename + CString("-offscreen")).toASCIIString());
gtk_widget_show(m_bgImageResult);
g_object_ref(m_bgImageResult);
this->cacheChangeImageCB(m_caches[0], m_bgImageTarget);
this->cacheChangeImageCB(m_caches[1], m_bgImageWork);
this->cacheChangeImageCB(m_caches[2], m_bgImageResult);
this->cacheForEach(&CBoxAlgorithmP300IdentifierCardVisualization::cacheChangeBackgroundCB, &m_bgColor);
return true;
}
bool CBoxAlgorithmP300IdentifierCardVisualization::uninitialize()
{
// g_object_unref(m_toolbarWidgetInterface);
// m_toolbarWidgetInterface= nullptr;
if (m_mainWidgetInterface)
{
g_object_unref(m_mainWidgetInterface);
m_mainWidgetInterface = nullptr;
}
m_targetFlaggingStimulationSet.uninitialize();
m_targetFlaggingMemoryBuffer.uninitialize();
m_targetStimulationSet.uninitialize();
m_targetMemoryBuffer.uninitialize();
m_sequenceStimulationSet.uninitialize();
m_sequenceMemoryBuffer.uninitialize();
if (m_cardSelectionStimulationDecoder)
{
m_cardSelectionStimulationDecoder->uninitialize();
this->getAlgorithmManager().releaseAlgorithm(*m_cardSelectionStimulationDecoder);
m_cardSelectionStimulationDecoder = nullptr;
}
if (m_targetFlaggingStimulationEncoder)
{
m_targetFlaggingStimulationEncoder->uninitialize();
this->getAlgorithmManager().releaseAlgorithm(*m_targetFlaggingStimulationEncoder);
m_targetFlaggingStimulationEncoder = nullptr;
}
if (m_targetStimulationDecoder)
{
m_targetStimulationDecoder->uninitialize();
this->getAlgorithmManager().releaseAlgorithm(*m_targetStimulationDecoder);
m_targetStimulationDecoder = nullptr;
}
if (m_sequenceStimulationDecoder)
{
m_sequenceStimulationDecoder->uninitialize();
this->getAlgorithmManager().releaseAlgorithm(*m_sequenceStimulationDecoder);
m_sequenceStimulationDecoder = nullptr;
}
if (m_visualizationCtx)
{
this->releasePluginObject(m_visualizationCtx);
m_visualizationCtx = nullptr;
}
return true;
}
bool CBoxAlgorithmP300IdentifierCardVisualization::processInput(const size_t /*index*/)
{
this->getBoxAlgorithmContext()->markAlgorithmAsReadyToProcess();
if (!m_tableInitialized)
{
this->cacheChangeImageCB(m_caches[0], m_bgImageTarget);
this->cacheChangeImageCB(m_caches[1], m_bgImageWork);
this->cacheChangeImageCB(m_caches[2], m_bgImageResult);
m_tableInitialized = true;
}
return true;
}
bool CBoxAlgorithmP300IdentifierCardVisualization::process()
{
Kernel::IBoxIO& boxContext = this->getDynamicBoxContext();
// --- Sequence stimulations
for (size_t i = 0; i < boxContext.getInputChunkCount(0); ++i)
{
CStimulationSet flaggingStimulationSet;
m_sequenceMemoryBuffer = boxContext.getInputChunk(0, i);
m_targetFlaggingStimulationSet = &flaggingStimulationSet;
m_targetFlaggingMemoryBuffer = boxContext.getOutputChunk(0);
m_sequenceStimulationDecoder->process();
m_lastTime = boxContext.getInputChunkEndTime(0, i);
if (m_sequenceStimulationDecoder->isOutputTriggerActive(OVP_GD_Algorithm_StimulationDecoder_OutputTriggerId_ReceivedHeader))
{
m_targetFlaggingStimulationEncoder->process(OVP_GD_Algorithm_StimulationEncoder_InputTriggerId_EncodeHeader);
}
if (m_sequenceStimulationDecoder->isOutputTriggerActive(OVP_GD_Algorithm_StimulationDecoder_OutputTriggerId_ReceivedBuffer))
{
IStimulationSet* stimulationSet = m_sequenceStimulationSet;
for (size_t j = 0; j < stimulationSet->getStimulationCount(); ++j)
{
const uint64_t id = stimulationSet->getStimulationIdentifier(j);
if (id >= m_cardStimulationBase && id < m_cardStimulationBase + m_nCard)
{
const int card = int(id - m_cardStimulationBase);
if (card == m_targetCard) { flaggingStimulationSet.appendStimulation(OVTK_StimulationId_Target, stimulationSet->getStimulationDate(j), 0); }
else { flaggingStimulationSet.appendStimulation(OVTK_StimulationId_NonTarget, stimulationSet->getStimulationDate(j), 0); }
this->cacheChangeImageCB(m_caches[1], m_fgImageWorks[card]);
}
else if (id == OVTK_StimulationId_ExperimentStart)
{
this->getLogManager() << Kernel::LogLevel_Debug << "Received OVTK_StimulationId_ExperimentStart - resets grid\n";
this->cacheChangeImageCB(m_caches[0], m_bgImageTarget);
this->cacheChangeBackgroundCB(m_caches[0], &m_bgColor);
this->cacheChangeImageCB(m_caches[1], m_bgImageWork);
this->cacheChangeBackgroundCB(m_caches[1], &m_bgColor);
this->cacheChangeImageCB(m_caches[2], m_bgImageResult);
this->cacheChangeBackgroundCB(m_caches[2], &m_bgColor);
}
else if (id == OVTK_StimulationId_VisualStimulationStop)
{
this->getLogManager() << Kernel::LogLevel_Debug << "Received OVTK_StimulationId_VisualStimulationStop - resets grid\n";
//this->cacheChangeImageCB(m_caches[1], m_backgroundImageWork);
GtkContainer* container = GTK_CONTAINER(m_caches[1].widget);
gtk_container_remove(container, m_caches[1].image);
m_caches[1].image = nullptr;
}
else if (id == OVTK_StimulationId_SegmentStop)
{
this->getLogManager() << Kernel::LogLevel_Debug << "Received OVTK_StimulationId_SegmentStop - resets grid\n";
this->cacheChangeImageCB(m_caches[1], m_bgImageWork);
}
else if (id == OVTK_StimulationId_ExperimentStop)
{
this->getLogManager() << Kernel::LogLevel_Debug << "Received OVTK_StimulationId_ExperimentStop - resets grid\n";
this->cacheChangeImageCB(m_caches[0], m_bgImageTarget);
this->cacheChangeImageCB(m_caches[1], m_bgImageWork);
this->cacheChangeImageCB(m_caches[2], m_bgImageResult);
this->cacheChangeBackgroundCB(m_caches[0], &m_bgColor);
this->cacheChangeBackgroundCB(m_caches[1], &m_bgColor);
this->cacheChangeBackgroundCB(m_caches[2], &m_bgColor);
}
else if (id == OVTK_StimulationId_RestStop)
{
this->getLogManager() << Kernel::LogLevel_Debug << "Received OVTK_StimulationId_RestStop - resets grid\n";
this->cacheChangeImageCB(m_caches[2], m_bgImageResult);
this->cacheChangeBackgroundCB(m_caches[2], &m_bgColor);
}
}
m_targetFlaggingStimulationEncoder->process(OVP_GD_Algorithm_StimulationEncoder_InputTriggerId_EncodeBuffer);
}
if (m_sequenceStimulationDecoder->isOutputTriggerActive(OVP_GD_Algorithm_StimulationDecoder_OutputTriggerId_ReceivedEnd))
{
m_targetFlaggingStimulationEncoder->process(OVP_GD_Algorithm_StimulationEncoder_InputTriggerId_EncodeEnd);
}
boxContext.markInputAsDeprecated(0, i);
boxContext.markOutputAsReadyToSend(0, boxContext.getInputChunkStartTime(0, i), boxContext.getInputChunkEndTime(0, i));
}
// --- Target stimulations
for (size_t i = 0; i < boxContext.getInputChunkCount(1); ++i)
{
if (m_lastTime >= boxContext.getInputChunkStartTime(1, i))
{
m_targetMemoryBuffer = boxContext.getInputChunk(1, i);
m_targetStimulationDecoder->process();
if (m_targetStimulationDecoder->isOutputTriggerActive(OVP_GD_Algorithm_StimulationDecoder_OutputTriggerId_ReceivedHeader)) { }
if (m_targetStimulationDecoder->isOutputTriggerActive(OVP_GD_Algorithm_StimulationDecoder_OutputTriggerId_ReceivedBuffer))
{
IStimulationSet* stimulationSet = m_targetStimulationSet;
for (size_t j = 0; j < stimulationSet->getStimulationCount(); ++j)
{
uint64_t id = stimulationSet->getStimulationIdentifier(j);
if (id >= m_cardStimulationBase && id < m_cardStimulationBase + m_nCard)
{
this->getLogManager() << Kernel::LogLevel_Debug << "Received Target Card " << id << "\n";
m_targetCard = int(id - m_cardStimulationBase);
this->getLogManager() << Kernel::LogLevel_Debug << "Displays Target Cell\n";
this->cacheChangeImageCB(m_caches[0], m_fgImageTargets[m_targetCard]);
this->cacheChangeBackgroundCB(m_caches[0], &m_targetBgColor);
}
}
}
if (m_targetStimulationDecoder->isOutputTriggerActive(OVP_GD_Algorithm_StimulationDecoder_OutputTriggerId_ReceivedEnd)) { }
boxContext.markInputAsDeprecated(1, i);
}
}
// --- Selection stimulations
Kernel::TParameterHandler<const IMemoryBuffer*> selectionMemoryBuffer(
m_cardSelectionStimulationDecoder->getInputParameter(OVP_GD_Algorithm_StimulationDecoder_InputParameterId_MemoryBufferToDecode));
const Kernel::TParameterHandler<IStimulationSet*> selectionStimulationSet(
m_cardSelectionStimulationDecoder->getOutputParameter(OVP_GD_Algorithm_StimulationDecoder_OutputParameterId_StimulationSet));
for (size_t i = 0; i < boxContext.getInputChunkCount(2); ++i)
{
if (m_lastTime >= boxContext.getInputChunkStartTime(2, i))
{
selectionMemoryBuffer = boxContext.getInputChunk(2, i);
m_cardSelectionStimulationDecoder->process();
if (m_cardSelectionStimulationDecoder->isOutputTriggerActive(OVP_GD_Algorithm_StimulationDecoder_OutputTriggerId_ReceivedHeader)) { }
if (m_cardSelectionStimulationDecoder->isOutputTriggerActive(OVP_GD_Algorithm_StimulationDecoder_OutputTriggerId_ReceivedBuffer))
{
IStimulationSet* stimulationSet = selectionStimulationSet;
for (size_t j = 0; j < stimulationSet->getStimulationCount(); ++j)
{
uint64_t id = stimulationSet->getStimulationIdentifier(j);
if (id >= m_cardStimulationBase && id < m_cardStimulationBase + m_nCard)
{
this->getLogManager() << Kernel::LogLevel_Debug << "Received Selected Card " << id << "\n";
const int selectedCard = int(id - m_cardStimulationBase);
this->getLogManager() << Kernel::LogLevel_Debug << "Displays Selected Cell\n";
this->cacheChangeImageCB(m_caches[1], m_bgImageWork);
this->cacheChangeImageCB(m_caches[2], m_fgImageResults[selectedCard]);
this->cacheChangeBackgroundCB(m_caches[2], &m_selectedBgColor);
}
if (id == OVTK_StimulationId_Label_00)
{
this->getLogManager() << Kernel::LogLevel_Trace << "Selection Rejected !\n";
std::string label;
label = gtk_label_get_text(m_result);
label += "*";
gtk_label_set_text(m_result, label.c_str());
}
}
}
if (m_cardSelectionStimulationDecoder->isOutputTriggerActive(OVP_GD_Algorithm_StimulationDecoder_OutputTriggerId_ReceivedEnd)) { }
boxContext.markInputAsDeprecated(2, i);
}
}
return true;
}
// _________________________________________________________________________________________________________________________________________________________
//
void CBoxAlgorithmP300IdentifierCardVisualization::cacheBuildFromTable(GtkTable* table)
{
if (table)
{
for (GList* list = table->children; list; list = list->next)
{
GtkTableChild* child = static_cast<GtkTableChild*>(list->data);
if (child->top_attach != 0)
{
int idx = 0;
for (size_t i = child->top_attach; i < child->bottom_attach; ++i)
{
for (size_t j = child->left_attach; j < child->right_attach; ++j)
{
idx++;
widget_style_t style;
style.index = idx;
style.parent = child->widget;
style.widget = gtk_bin_get_child(GTK_BIN(style.parent));
style.image = gtk_bin_get_child(GTK_BIN(style.widget));
m_caches.push_back(style);
}
}
}
}
}
}
void CBoxAlgorithmP300IdentifierCardVisualization::cacheForEach(cache_callback callback, void* data)
{
for (auto& cache : m_caches) { (this->*callback)(cache, data); }
}
void CBoxAlgorithmP300IdentifierCardVisualization::cacheForEachIf(const int card, cache_callback ifCB, cache_callback elseCB,
void* ifUserData, void* elseUserData)
{
for (auto& cache : m_caches)
{
if (card == cache.index) { (this->*ifCB)(cache, ifUserData); }
else { (this->*elseCB)(cache, elseUserData); }
}
}
void CBoxAlgorithmP300IdentifierCardVisualization::cacheChangeNullCB(widget_style_t& /*style*/, void* /*data*/) {}
void CBoxAlgorithmP300IdentifierCardVisualization::cacheChangeImageCB(widget_style_t& style, void* data)
{
GtkContainer* container = GTK_CONTAINER(style.widget);
auto* image = static_cast<GtkWidget*>(data);
if (style.image != image)
{
if (style.image) { gtk_container_remove(container, style.image); }
gtk_container_add(container, image);
style.image = image;
}
}
void CBoxAlgorithmP300IdentifierCardVisualization::cacheChangeBackgroundCB(widget_style_t& style, void* data)
{
GdkColor color = *static_cast<GdkColor*>(data);
if (memcmp(&style.bgColor, &color, sizeof(GdkColor)) != 0)
{
gtk_widget_modify_bg(style.parent, GTK_STATE_NORMAL, &color);
gtk_widget_modify_bg(style.widget, GTK_STATE_NORMAL, &color);
gtk_widget_modify_bg(style.image, GTK_STATE_NORMAL, &color);
style.bgColor = color;
}
}
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,160 @@
#pragma once
#include "../ovp_defines.h"
#include <openvibe/ov_all.h>
#include <toolkit/ovtk_all.h>
#include <visualization-toolkit/ovviz_all.h>
#include <gtk/gtk.h>
#include <map>
#include "../utils.h"
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
class CBoxAlgorithmP300IdentifierCardVisualization final : public Toolkit::TBoxAlgorithm<IBoxAlgorithm>
{
public:
void release() override { delete this; }
bool initialize() override;
bool uninitialize() override;
bool processInput(const size_t index) override;
bool process() override;
_IsDerivedFromClass_Final_(Toolkit::TBoxAlgorithm<IBoxAlgorithm>, OVP_ClassId_BoxAlgorithm_P300IdentifierCardVisualization)
private:
using widget_style_t = struct
{
int index;
GdkColor bgColor;
GtkWidget* parent;
GtkWidget* widget;
GtkWidget* image;
};
typedef void (CBoxAlgorithmP300IdentifierCardVisualization::*cache_callback)(widget_style_t& style, void* data);
void cacheBuildFromTable(GtkTable* table);
void cacheForEach(cache_callback callback, void* data);
void cacheForEachIf(int card, cache_callback ifCB, cache_callback elseCB, void* ifUserData, void* elseUserData);
void cacheChangeNullCB(widget_style_t& style, void* data);
void cacheChangeImageCB(widget_style_t& style, void* data);
void cacheChangeBackgroundCB(widget_style_t& style, void* data);
protected:
CString m_interfaceFilename;
uint64_t m_cardStimulationBase = 0;
private:
Kernel::IAlgorithmProxy* m_sequenceStimulationDecoder = nullptr;
Kernel::IAlgorithmProxy* m_targetStimulationDecoder = nullptr;
Kernel::IAlgorithmProxy* m_targetFlaggingStimulationEncoder = nullptr;
Kernel::IAlgorithmProxy* m_cardSelectionStimulationDecoder = nullptr;
Kernel::TParameterHandler<const IMemoryBuffer*> m_sequenceMemoryBuffer;
Kernel::TParameterHandler<const IMemoryBuffer*> m_targetMemoryBuffer;
Kernel::TParameterHandler<const IStimulationSet*> m_targetFlaggingStimulationSet;
Kernel::TParameterHandler<IStimulationSet*> m_sequenceStimulationSet;
Kernel::TParameterHandler<IStimulationSet*> m_targetStimulationSet;
Kernel::TParameterHandler<IMemoryBuffer*> m_targetFlaggingMemoryBuffer;
uint64_t m_lastTime = 0;
GtkBuilder* m_mainWidgetInterface = nullptr;
//GtkBuilder* m_toolbarWidgetInterface = nullptr;
GtkWidget* m_mainWindow = nullptr;
//GtkWidget* m_toolbarWidget = nullptr;
GtkTable* m_table = nullptr;
GtkLabel* m_result = nullptr;
//GtkLabel* m_target = nullptr;
GdkColor m_bgColor = InitGDKColor(0, 0, 0, 0);
GdkColor m_targetBgColor = InitGDKColor(0, 6554, 26214, 6554);
GdkColor m_selectedBgColor = InitGDKColor(0, 45875, 13107, 13107);
GtkLabel* m_targetLabel = nullptr;
GtkLabel* m_selectedLabel = nullptr;
uint64_t m_nCard = 0;
int m_targetCard = 0;
std::vector<GtkWidget*> m_fgImageTargets;
std::vector<GtkWidget*> m_fgImageWorks;
std::vector<GtkWidget*> m_fgImageResults;
GtkWidget* m_bgImageTarget = nullptr;
GtkWidget* m_bgImageWork = nullptr;
GtkWidget* m_bgImageResult = nullptr;
bool m_tableInitialized = false;
std::vector<widget_style_t> m_caches;
VisualizationToolkit::IVisualizationContext* m_visualizationCtx = nullptr;
};
class CBoxAlgorithmP300IdentifierCardVisualizationDesc final : public IBoxAlgorithmDesc
{
public:
void release() override { }
CString getName() const override { return CString("P300 Identifier Card Visualization"); }
CString getAuthorName() const override { return CString("Baptiste Payan"); }
CString getAuthorCompanyName() const override { return CString("INRIA"); }
CString getShortDescription() const override
{
return CString("Displays images to the user based on received stimulations with some additional P300 related functionality");
}
CString getDetailedDescription() const override { return CString(""); }
CString getCategory() const override { return CString("Visualization/Presentation"); }
CString getVersion() const override { return CString("1.0"); }
CString getStockItemName() const override { return CString("gtk-select-font"); }
CIdentifier getCreatedClass() const override { return OVP_ClassId_BoxAlgorithm_P300IdentifierCardVisualization; }
IPluginObject* create() override { return new CBoxAlgorithmP300IdentifierCardVisualization; }
bool hasFunctionality(const EPluginFunctionality functionality) const override { return functionality == EPluginFunctionality::Visualization; }
bool getBoxPrototype(Kernel::IBoxProto& prototype) const override
{
prototype.addInput("Sequence stimulations", OV_TypeId_Stimulations);
prototype.addInput("Target stimulations", OV_TypeId_Stimulations);
prototype.addInput("Card selection stimulations", OV_TypeId_Stimulations);
prototype.addOutput("Target / Non target flagging", OV_TypeId_Stimulations);
prototype.addSetting("Interface filename", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/p300-identifier-card.ui");
prototype.addSetting("Background color", OV_TypeId_Color, "0,0,0");
prototype.addSetting("Target background color", OV_TypeId_Color, "10,40,10");
prototype.addSetting("Selected background color", OV_TypeId_Color, "70,20,20");
prototype.addSetting("Card stimulation base", OV_TypeId_Stimulation, "OVTK_StimulationId_Label_01");
prototype.addSetting("Background Card filename", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/p300-magic-card/openvibe-logo.png");
prototype.addSetting("Card filename", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/p300-magic-card/01.png");
prototype.addSetting("Card filename", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/p300-magic-card/02.png");
prototype.addSetting("Card filename", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/p300-magic-card/03.png");
prototype.addSetting("Card filename", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/p300-magic-card/04.png");
prototype.addSetting("Card filename", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/p300-magic-card/05.png");
prototype.addSetting("Card filename", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/p300-magic-card/06.png");
prototype.addSetting("Card filename", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/p300-magic-card/07.png");
prototype.addSetting("Card filename", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/p300-magic-card/08.png");
prototype.addSetting("Card filename", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/p300-magic-card/09.png");
prototype.addSetting("Card filename", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/p300-magic-card/10.png");
prototype.addSetting("Card filename", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/p300-magic-card/11.png");
prototype.addSetting("Card filename", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/p300-magic-card/12.png");
prototype.addSetting("Card filename", OV_TypeId_Filename, "");
prototype.addSetting("Card filename", OV_TypeId_Filename, "");
prototype.addSetting("Card filename", OV_TypeId_Filename, "");
prototype.addFlag(OV_AttributeId_Box_FlagIsUnstable);
return true;
}
_IsDerivedFromClass_Final_(IBoxAlgorithmDesc, OVP_ClassId_BoxAlgorithm_P300IdentifierCardVisualizationDesc)
};
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,477 @@
#include "ovpCBoxAlgorithmP300MagicCardVisualization.h"
#include "../utils.h"
#include <tcptagging/IStimulusSender.h>
#include <list>
#include <string>
#include <vector>
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
// This callback flushes all accumulated stimulations to the TCP Tagging
// after the rendering has completed.
static gboolean FlushCB(gpointer data)
{
static_cast<CBoxAlgorithmP300MagicCardVisualization*>(data)->flushQueue();
return false; // Only run once
}
bool CBoxAlgorithmP300MagicCardVisualization::initialize()
{
const Kernel::IBox& boxContext = this->getStaticBoxContext();
m_mainWidgetInterface = nullptr;
m_toolbarWidgetInterface = nullptr;
m_interfaceFilename = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 0);
m_bgColor = CGdkcolorAutoCast(getStaticBoxContext(), getConfigurationManager(), 1);
m_targetBgColor = CGdkcolorAutoCast(getStaticBoxContext(), getConfigurationManager(), 2);
m_selectedBgColor = CGdkcolorAutoCast(getStaticBoxContext(), getConfigurationManager(), 3);
m_cardStimulationBase = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 4);
CString tcpTaggingHostAddress = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 5);
CString tcpTaggingHostPort = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 6);
const CString bgImageFilename = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 7);
for (size_t i = 6; i < boxContext.getSettingCount(); ++i)
{
GError* error = nullptr;
CString fgImageFilename = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), i);
GdkPixbuf* tmp = gdk_pixbuf_new_from_file(fgImageFilename.toASCIIString(), &error);
GtkWidget* fgImage = gtk_image_new_from_pixbuf(gdk_pixbuf_scale_simple(tmp, 192, 192, GDK_INTERP_BILINEAR));
g_object_unref(tmp);
gtk_widget_show(fgImage);
g_object_ref(fgImage);
m_fgImage.push_back(fgImage);
GtkWidget* bgImage;
if (bgImageFilename == CString("")) { bgImage = gtk_image_new_from_file((fgImageFilename + CString("-offscreen")).toASCIIString()); }
else { bgImage = gtk_image_new_from_file(bgImageFilename.toASCIIString()); }
gtk_widget_show(bgImage);
g_object_ref(bgImage);
m_bgImage.push_back(bgImage);
}
// ----------------------------------------------------------------------------------------------------------------------------------------------------------
m_sequenceStimulationDecoder = &this->getAlgorithmManager().getAlgorithm(
this->getAlgorithmManager().createAlgorithm(OVP_GD_ClassId_Algorithm_StimulationDecoder));
m_sequenceStimulationDecoder->initialize();
m_targetStimulationDecoder = &this->getAlgorithmManager().getAlgorithm(
this->getAlgorithmManager().createAlgorithm(OVP_GD_ClassId_Algorithm_StimulationDecoder));
m_targetStimulationDecoder->initialize();
m_targetFlaggingStimulationEncoder = &this->getAlgorithmManager().getAlgorithm(
this->getAlgorithmManager().createAlgorithm(OVP_GD_ClassId_Algorithm_StimulationEncoder));
m_targetFlaggingStimulationEncoder->initialize();
m_cardSelectionStimulationDecoder = &this->getAlgorithmManager().getAlgorithm(
this->getAlgorithmManager().createAlgorithm(OVP_GD_ClassId_Algorithm_StimulationDecoder));
m_cardSelectionStimulationDecoder->initialize();
m_sequenceMemoryBuffer.initialize(
m_sequenceStimulationDecoder->getInputParameter(OVP_GD_Algorithm_StimulationDecoder_InputParameterId_MemoryBufferToDecode));
m_sequenceStimulationSet.initialize(m_sequenceStimulationDecoder->getOutputParameter(OVP_GD_Algorithm_StimulationDecoder_OutputParameterId_StimulationSet));
m_targetMemoryBuffer.initialize(m_targetStimulationDecoder->getInputParameter(OVP_GD_Algorithm_StimulationDecoder_InputParameterId_MemoryBufferToDecode));
m_targetStimulationSet.initialize(m_targetStimulationDecoder->getOutputParameter(OVP_GD_Algorithm_StimulationDecoder_OutputParameterId_StimulationSet));
m_targetFlaggingStimulationSet.initialize(
m_targetFlaggingStimulationEncoder->getInputParameter(OVP_GD_Algorithm_StimulationEncoder_InputParameterId_StimulationSet));
m_targetFlaggingMemoryBuffer.initialize(
m_targetFlaggingStimulationEncoder->getOutputParameter(OVP_GD_Algorithm_StimulationEncoder_OutputParameterId_EncodedMemoryBuffer));
m_lastTime = 0;
m_mainWidgetInterface = gtk_builder_new(); // glade_xml_new(m_interfaceFilename.toASCIIString(), "p300-magic-card-main", nullptr);
if (!gtk_builder_add_from_file(m_mainWidgetInterface, m_interfaceFilename.toASCIIString(), nullptr))
{
this->getLogManager() << Kernel::LogLevel_ImportantWarning << "Could not load interface file [" << m_interfaceFilename << "]\n";
this->getLogManager() << Kernel::LogLevel_ImportantWarning <<
"The file may be missing. However, the interface files now use gtk-builder instead of glade. Did you update your files ?\n";
return false;
}
m_toolbarWidgetInterface = gtk_builder_new(); // glade_xml_new(m_interfaceFilename.toASCIIString(), "p300-magic-card-toolbar", nullptr);
gtk_builder_add_from_file(m_toolbarWidgetInterface, m_interfaceFilename.toASCIIString(), nullptr);
m_mainWindow = GTK_WIDGET(gtk_builder_get_object(m_mainWidgetInterface, "p300-magic-card-main"));
m_toolbarWidget = GTK_WIDGET(gtk_builder_get_object(m_toolbarWidgetInterface, "p300-magic-card-toolbar"));
m_table = GTK_TABLE(gtk_builder_get_object(m_mainWidgetInterface, "p300-magic-card-table"));
gtk_widget_modify_bg(m_mainWindow, GTK_STATE_NORMAL, &m_bgColor);
gtk_builder_connect_signals(m_mainWidgetInterface, nullptr);
gtk_builder_connect_signals(m_toolbarWidgetInterface, nullptr);
m_visualizationCtx = dynamic_cast<VisualizationToolkit::IVisualizationContext*>(this->createPluginObject(OVP_ClassId_Plugin_VisualizationCtx));
m_visualizationCtx->setWidget(*this, m_mainWindow);
m_visualizationCtx->setToolbar(*this, m_toolbarWidget);
guint nRow = 0;
guint nCol = 0;
g_object_get(m_table, "n-rows", &nRow, nullptr);
g_object_get(m_table, "n-columns", &nCol, nullptr);
m_nTableRow = nRow;
m_nTableCol = nCol;
m_nCard = m_nTableRow * m_nTableCol;
m_targetCard = -1;
m_tableInitialized = false;
this->cacheBuildFromTable(m_table);
this->cacheForEach(&CBoxAlgorithmP300MagicCardVisualization::cacheChangeImageCB, &m_fgImage);
this->cacheForEach(&CBoxAlgorithmP300MagicCardVisualization::cacheChangeBackgroundCB, &m_bgColor);
//TCP TAGGING
m_idleFuncTag = 0;
m_stimuliQueue.clear();
m_stimulusSender = TCPTagging::CreateStimulusSender();
if (!m_stimulusSender->connect("localhost", "15361"))
{
this->getLogManager() << Kernel::LogLevel_Warning << "Unable to connect to AS's TCP Tagging plugin, stimuli wont be forwarded.\n";
}
return true;
}
bool CBoxAlgorithmP300MagicCardVisualization::uninitialize()
{
if (m_toolbarWidgetInterface)
{
g_object_unref(m_toolbarWidgetInterface);
m_toolbarWidgetInterface = nullptr;
}
if (m_mainWidgetInterface)
{
g_object_unref(m_mainWidgetInterface);
m_mainWidgetInterface = nullptr;
}
m_targetFlaggingStimulationSet.uninitialize();
m_targetFlaggingMemoryBuffer.uninitialize();
m_targetStimulationSet.uninitialize();
m_targetMemoryBuffer.uninitialize();
m_sequenceStimulationSet.uninitialize();
m_sequenceMemoryBuffer.uninitialize();
if (m_cardSelectionStimulationDecoder)
{
m_cardSelectionStimulationDecoder->uninitialize();
this->getAlgorithmManager().releaseAlgorithm(*m_cardSelectionStimulationDecoder);
m_cardSelectionStimulationDecoder = nullptr;
}
if (m_targetFlaggingStimulationEncoder)
{
m_targetFlaggingStimulationEncoder->uninitialize();
this->getAlgorithmManager().releaseAlgorithm(*m_targetFlaggingStimulationEncoder);
m_targetFlaggingStimulationEncoder = nullptr;
}
if (m_targetStimulationDecoder)
{
m_targetStimulationDecoder->uninitialize();
this->getAlgorithmManager().releaseAlgorithm(*m_targetStimulationDecoder);
m_targetStimulationDecoder = nullptr;
}
if (m_sequenceStimulationDecoder)
{
m_sequenceStimulationDecoder->uninitialize();
this->getAlgorithmManager().releaseAlgorithm(*m_sequenceStimulationDecoder);
m_sequenceStimulationDecoder = nullptr;
}
if (m_visualizationCtx)
{
this->releasePluginObject(m_visualizationCtx);
m_visualizationCtx = nullptr;
}
//TCP TAGGING
m_stimuliQueue.clear();
if (m_stimulusSender)
{
delete m_stimulusSender;
m_stimulusSender = nullptr;
}
return true;
}
bool CBoxAlgorithmP300MagicCardVisualization::processInput(const size_t /*index*/)
{
this->getBoxAlgorithmContext()->markAlgorithmAsReadyToProcess();
if (!m_tableInitialized)
{
this->cacheForEach(&CBoxAlgorithmP300MagicCardVisualization::cacheChangeImageCB, &m_fgImage);
m_tableInitialized = true;
}
return true;
}
bool CBoxAlgorithmP300MagicCardVisualization::process()
{
Kernel::IBoxIO& boxContext = this->getDynamicBoxContext();
// --- Sequence stimulations
for (size_t i = 0; i < boxContext.getInputChunkCount(0); ++i)
{
CStimulationSet flaggingStimulationSet;
m_sequenceMemoryBuffer = boxContext.getInputChunk(0, i);
m_targetFlaggingStimulationSet = &flaggingStimulationSet;
m_targetFlaggingMemoryBuffer = boxContext.getOutputChunk(0);
m_sequenceStimulationDecoder->process();
m_lastTime = boxContext.getInputChunkEndTime(0, i);
if (m_sequenceStimulationDecoder->isOutputTriggerActive(OVP_GD_Algorithm_StimulationDecoder_OutputTriggerId_ReceivedHeader))
{
m_targetFlaggingStimulationEncoder->process(OVP_GD_Algorithm_StimulationEncoder_InputTriggerId_EncodeHeader);
}
if (m_sequenceStimulationDecoder->isOutputTriggerActive(OVP_GD_Algorithm_StimulationDecoder_OutputTriggerId_ReceivedBuffer))
{
IStimulationSet* stimulationSet = m_sequenceStimulationSet;
for (size_t j = 0; j < stimulationSet->getStimulationCount(); ++j)
{
uint64_t id = stimulationSet->getStimulationIdentifier(j);
if (id >= m_cardStimulationBase && id < m_cardStimulationBase + m_nCard)
{
const int card = int(id - m_cardStimulationBase);
if (card == m_targetCard)
{
m_stimuliQueue.push_back(OVTK_StimulationId_Target);
flaggingStimulationSet.appendStimulation(OVTK_StimulationId_Target, stimulationSet->getStimulationDate(j), 0);
}
else
{
m_stimuliQueue.push_back(OVTK_StimulationId_NonTarget);
flaggingStimulationSet.appendStimulation(OVTK_StimulationId_NonTarget, stimulationSet->getStimulationDate(j), 0);
}
this->cacheForEachIf(card, &CBoxAlgorithmP300MagicCardVisualization::cacheChangeImageCB,
&CBoxAlgorithmP300MagicCardVisualization::cacheChangeImageCB, &m_fgImage, &m_bgImage);
this->cacheForEach(&CBoxAlgorithmP300MagicCardVisualization::cacheChangeBackgroundCB, &m_bgColor);
}
if (id == OVTK_StimulationId_ExperimentStart)
{
this->getLogManager() << Kernel::LogLevel_Debug << "Received OVTK_StimulationId_ExperimentStart - resets grid\n";
this->cacheForEach(&CBoxAlgorithmP300MagicCardVisualization::cacheChangeImageCB, &m_bgImage);
}
if (id == OVTK_StimulationId_VisualStimulationStop)
{
this->getLogManager() << Kernel::LogLevel_Debug << "Received OVTK_StimulationId_VisualStimulationStop - resets grid\n";
this->cacheForEach(&CBoxAlgorithmP300MagicCardVisualization::cacheChangeImageCB, &m_bgImage);
}
// Pass the stimulation to the server also as-is. If its a flash, it can be differentiated from a 'target' spec because
// its NOT between OVTK_StimulationId_RestStart and OVTK_StimulationId_RestStop stimuli in the generated P300 timeline.
m_stimuliQueue.push_back(id);
}
m_targetFlaggingStimulationEncoder->process(OVP_GD_Algorithm_StimulationEncoder_InputTriggerId_EncodeBuffer);
}
if (m_sequenceStimulationDecoder->isOutputTriggerActive(OVP_GD_Algorithm_StimulationDecoder_OutputTriggerId_ReceivedEnd))
{
m_targetFlaggingStimulationEncoder->process(OVP_GD_Algorithm_StimulationEncoder_InputTriggerId_EncodeEnd);
}
boxContext.markInputAsDeprecated(0, i);
boxContext.markOutputAsReadyToSend(0, boxContext.getInputChunkStartTime(0, i), boxContext.getInputChunkEndTime(0, i));
}
// --- Target stimulations
for (size_t i = 0; i < boxContext.getInputChunkCount(1); ++i)
{
if (m_lastTime >= boxContext.getInputChunkStartTime(1, i))
{
m_targetMemoryBuffer = boxContext.getInputChunk(1, i);
m_targetStimulationDecoder->process();
if (m_targetStimulationDecoder->isOutputTriggerActive(OVP_GD_Algorithm_StimulationDecoder_OutputTriggerId_ReceivedHeader)) { }
if (m_targetStimulationDecoder->isOutputTriggerActive(OVP_GD_Algorithm_StimulationDecoder_OutputTriggerId_ReceivedBuffer))
{
IStimulationSet* stimulationSet = m_targetStimulationSet;
for (size_t j = 0; j < stimulationSet->getStimulationCount(); ++j)
{
uint64_t id = stimulationSet->getStimulationIdentifier(j);
if (id >= m_cardStimulationBase && id < m_cardStimulationBase + m_nCard)
{
this->getLogManager() << Kernel::LogLevel_Debug << "Received Target Card " << id << "\n";
m_targetCard = int(id - m_cardStimulationBase);
this->getLogManager() << Kernel::LogLevel_Debug << "Displays Target Cell\n";
this->cacheForEachIf(m_targetCard, &CBoxAlgorithmP300MagicCardVisualization::cacheChangeImageCB,
&CBoxAlgorithmP300MagicCardVisualization::cacheChangeNullCB, &m_fgImage, nullptr);
this->cacheForEachIf(m_targetCard, &CBoxAlgorithmP300MagicCardVisualization::cacheChangeBackgroundCB,
&CBoxAlgorithmP300MagicCardVisualization::cacheChangeNullCB, &m_targetBgColor, nullptr);
// Merge the current target into the stimulation stream. It can be differentiated
// from a 'flash' spec because it IS between OVTK_StimulationId_RestStart and
// OVTK_StimulationId_RestStop stimulations in the P300 timeline.
{
//or just stimulationIdentifier
m_stimuliQueue.push_back(m_targetCard + m_cardStimulationBase);
}
}
}
}
if (m_targetStimulationDecoder->isOutputTriggerActive(OVP_GD_Algorithm_StimulationDecoder_OutputTriggerId_ReceivedEnd)) { }
boxContext.markInputAsDeprecated(1, i);
}
}
// --- Selection stimulations
Kernel::TParameterHandler<const IMemoryBuffer*> selectionMemoryBuffer(
m_cardSelectionStimulationDecoder->getInputParameter(OVP_GD_Algorithm_StimulationDecoder_InputParameterId_MemoryBufferToDecode));
const Kernel::TParameterHandler<IStimulationSet*> selectionStimulationSet(
m_cardSelectionStimulationDecoder->getOutputParameter(OVP_GD_Algorithm_StimulationDecoder_OutputParameterId_StimulationSet));
for (size_t i = 0; i < boxContext.getInputChunkCount(2); ++i)
{
if (m_lastTime >= boxContext.getInputChunkStartTime(2, i))
{
selectionMemoryBuffer = boxContext.getInputChunk(2, i);
m_cardSelectionStimulationDecoder->process();
if (m_cardSelectionStimulationDecoder->isOutputTriggerActive(OVP_GD_Algorithm_StimulationDecoder_OutputTriggerId_ReceivedHeader)) { }
if (m_cardSelectionStimulationDecoder->isOutputTriggerActive(OVP_GD_Algorithm_StimulationDecoder_OutputTriggerId_ReceivedBuffer))
{
IStimulationSet* stimulationSet = selectionStimulationSet;
for (size_t j = 0; j < stimulationSet->getStimulationCount(); ++j)
{
uint64_t id = stimulationSet->getStimulationIdentifier(j);
if (id >= m_cardStimulationBase && id < m_cardStimulationBase + m_nCard)
{
this->getLogManager() << Kernel::LogLevel_Debug << "Received Selected Card " << id << "\n";
const int selectedCard = int(id - m_cardStimulationBase);
this->getLogManager() << Kernel::LogLevel_Debug << "Displays Selected Cell\n";
this->cacheForEachIf(selectedCard, &CBoxAlgorithmP300MagicCardVisualization::cacheChangeImageCB,
&CBoxAlgorithmP300MagicCardVisualization::cacheChangeNullCB, &m_fgImage, nullptr);
this->cacheForEachIf(selectedCard, &CBoxAlgorithmP300MagicCardVisualization::cacheChangeBackgroundCB,
&CBoxAlgorithmP300MagicCardVisualization::cacheChangeNullCB, &m_selectedBgColor, nullptr);
}
if (id == OVTK_StimulationId_Label_00)
{
this->getLogManager() << Kernel::LogLevel_Trace << "Selection Rejected !\n";
std::string label;
label = gtk_label_get_text(m_result);
label += "*";
gtk_label_set_text(m_result, label.c_str());
}
}
}
if (m_cardSelectionStimulationDecoder->isOutputTriggerActive(OVP_GD_Algorithm_StimulationDecoder_OutputTriggerId_ReceivedEnd)) { }
boxContext.markInputAsDeprecated(2, i);
}
}
// After any possible rendering, we flush the accumulated stimuli. The default idle func is low priority, so it should be run after rendering by gtk.
if (m_idleFuncTag == 0) { m_idleFuncTag = g_idle_add(FlushCB, this); }
return true;
}
// _________________________________________________________________________________________________________________________________________________________
//
void CBoxAlgorithmP300MagicCardVisualization::cacheBuildFromTable(GtkTable* table)
{
if (table)
{
for (GList* list = table->children; list; list = list->next)
{
GtkTableChild* child = static_cast<GtkTableChild*>(list->data);
for (size_t i = child->top_attach; i < child->bottom_attach; ++i)
{
for (size_t j = child->left_attach; j < child->right_attach; ++j)
{
const int idx = int(i * m_nTableCol + j);
widget_style_t& style = m_caches[idx];
style.index = idx;
style.parent = child->widget;
style.widget = gtk_bin_get_child(GTK_BIN(style.parent));
style.image = gtk_bin_get_child(GTK_BIN(style.widget));
}
}
}
}
}
void CBoxAlgorithmP300MagicCardVisualization::cacheForEach(cache_callback callback, void* data)
{
for (auto& cache : m_caches) { (this->*callback)(cache.second, data); }
}
void CBoxAlgorithmP300MagicCardVisualization::cacheForEachIf(const int card, cache_callback ifCB, cache_callback elseCB, void* ifUserData,
void* elseUserData)
{
for (auto& cache : m_caches)
{
if (card == cache.second.index) { (this->*ifCB)(cache.second, ifUserData); }
else { (this->*elseCB)(cache.second, elseUserData); }
}
}
void CBoxAlgorithmP300MagicCardVisualization::cacheChangeNullCB(widget_style_t& /*widgetStyle*/, void* /*data*/) {}
void CBoxAlgorithmP300MagicCardVisualization::cacheChangeImageCB(widget_style_t& style, void* data)
{
GtkContainer* container = GTK_CONTAINER(style.widget);
auto* pvImage = static_cast<std::vector<GtkWidget*>*>(data);
GtkWidget* image = (*pvImage)[style.index];
if (style.image != image)
{
if (style.image) { gtk_container_remove(container, style.image); }
gtk_container_add(container, image);
style.image = image;
}
}
void CBoxAlgorithmP300MagicCardVisualization::cacheChangeBackgroundCB(widget_style_t& style, void* data)
{
GdkColor color = *static_cast<GdkColor*>(data);
if (memcmp(&style.bgColor, &color, sizeof(GdkColor)) != 0)
{
gtk_widget_modify_bg(style.parent, GTK_STATE_NORMAL, &color);
gtk_widget_modify_bg(style.widget, GTK_STATE_NORMAL, &color);
gtk_widget_modify_bg(style.image, GTK_STATE_NORMAL, &color);
style.bgColor = color;
}
}
// Note that we don't need concurrency control here as gtk callbacks run in the main thread
void CBoxAlgorithmP300MagicCardVisualization::flushQueue()
{
for (const auto& stimulation : m_stimuliQueue) { m_stimulusSender->sendStimulation(stimulation); }
m_stimuliQueue.clear();
// This function will be automatically removed after completion, so set to 0
m_idleFuncTag = 0;
}
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,160 @@
#pragma once
#include "../ovp_defines.h"
#include <openvibe/ov_all.h>
#include <toolkit/ovtk_all.h>
#include <visualization-toolkit/ovviz_all.h>
#include <gtk/gtk.h>
#include <map>
#include "../utils.h"
namespace TCPTagging {
class IStimulusSender; // fwd declare
} // namespace TCPTagging
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
class CBoxAlgorithmP300MagicCardVisualization final : public Toolkit::TBoxAlgorithm<IBoxAlgorithm>
{
public:
void release() override { delete this; }
bool initialize() override;
bool uninitialize() override;
bool processInput(const size_t index) override;
bool process() override;
void flushQueue(); // Sends all accumulated stimuli to the TCP Tagging
_IsDerivedFromClass_Final_(Toolkit::TBoxAlgorithm<IBoxAlgorithm>, OVP_ClassId_BoxAlgorithm_P300MagicCardVisualization)
private:
using widget_style_t = struct
{
int index;
GdkColor bgColor;
GtkWidget* parent;
GtkWidget* widget;
GtkWidget* image;
};
typedef void (CBoxAlgorithmP300MagicCardVisualization::*cache_callback)(widget_style_t& style, void* data);
void cacheBuildFromTable(GtkTable* table);
void cacheForEach(cache_callback callback, void* data);
void cacheForEachIf(int card, cache_callback ifCB, cache_callback elseCB, void* ifUserData, void* elseUserData);
void cacheChangeNullCB(widget_style_t& style, void* data);
void cacheChangeImageCB(widget_style_t& style, void* data);
void cacheChangeBackgroundCB(widget_style_t& style, void* data);
protected:
CString m_interfaceFilename;
uint64_t m_cardStimulationBase = 0;
private:
Kernel::IAlgorithmProxy* m_sequenceStimulationDecoder = nullptr;
Kernel::IAlgorithmProxy* m_targetStimulationDecoder = nullptr;
Kernel::IAlgorithmProxy* m_targetFlaggingStimulationEncoder = nullptr;
Kernel::IAlgorithmProxy* m_cardSelectionStimulationDecoder = nullptr;
Kernel::TParameterHandler<const IMemoryBuffer*> m_sequenceMemoryBuffer;
Kernel::TParameterHandler<const IMemoryBuffer*> m_targetMemoryBuffer;
Kernel::TParameterHandler<const IStimulationSet*> m_targetFlaggingStimulationSet;
Kernel::TParameterHandler<IStimulationSet*> m_sequenceStimulationSet;
Kernel::TParameterHandler<IStimulationSet*> m_targetStimulationSet;
Kernel::TParameterHandler<IMemoryBuffer*> m_targetFlaggingMemoryBuffer;
uint64_t m_lastTime = 0;
GtkBuilder* m_mainWidgetInterface = nullptr;
GtkBuilder* m_toolbarWidgetInterface = nullptr;
GtkWidget* m_mainWindow = nullptr;
GtkWidget* m_toolbarWidget = nullptr;
GtkTable* m_table = nullptr;
GtkLabel* m_result = nullptr;
//GtkLabel* m_target = nullptr;
GdkColor m_bgColor = InitGDKColor(0, 58982, 58982, 58982);
GdkColor m_targetBgColor = InitGDKColor(0, 6554, 26214, 6554);
GdkColor m_selectedBgColor = InitGDKColor(0, 45875, 13107, 13107);
uint64_t m_nTableRow = 0;
uint64_t m_nTableCol = 0;
uint64_t m_nCard = 0;
int m_targetCard = 0;
std::vector<GtkWidget*> m_fgImage;
std::vector<GtkWidget*> m_bgImage;
bool m_tableInitialized = false;
std::map<size_t, widget_style_t> m_caches;
VisualizationToolkit::IVisualizationContext* m_visualizationCtx = nullptr;
// TCP Tagging
std::vector<uint64_t> m_stimuliQueue;
guint m_idleFuncTag = 0;
TCPTagging::IStimulusSender* m_stimulusSender = nullptr;
};
class CBoxAlgorithmP300MagicCardVisualizationDesc final : public IBoxAlgorithmDesc
{
public:
void release() override { }
CString getName() const override { return CString("P300 Magic Card Visualization"); }
CString getAuthorName() const override { return CString("Yann Renard"); }
CString getAuthorCompanyName() const override { return CString("INRIA"); }
CString getShortDescription() const override { return CString("Presents a matrix of images to the user in various ways"); }
CString getDetailedDescription() const override { return CString(""); }
CString getCategory() const override { return CString("Visualization/Presentation"); }
CString getVersion() const override { return CString("1.0"); }
CString getStockItemName() const override { return CString("gtk-select-font"); }
CIdentifier getCreatedClass() const override { return OVP_ClassId_BoxAlgorithm_P300MagicCardVisualization; }
IPluginObject* create() override { return new CBoxAlgorithmP300MagicCardVisualization; }
bool hasFunctionality(const EPluginFunctionality functionality) const override { return functionality == EPluginFunctionality::Visualization; }
bool getBoxPrototype(Kernel::IBoxProto& prototype) const override
{
prototype.addInput("Sequence stimulations", OV_TypeId_Stimulations);
prototype.addInput("Target stimulations", OV_TypeId_Stimulations);
prototype.addInput("Card selection stimulations", OV_TypeId_Stimulations);
prototype.addOutput("Target / Non target flagging (deprecated)", OV_TypeId_Stimulations);
prototype.addSetting("Interface filename", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/p300-magic-card.ui");
prototype.addSetting("Background color", OV_TypeId_Color, "90,90,90");
prototype.addSetting("Target background color", OV_TypeId_Color, "10,40,10");
prototype.addSetting("Selected background color", OV_TypeId_Color, "70,20,20");
prototype.addSetting("Card stimulation base", OV_TypeId_Stimulation, "OVTK_StimulationId_Label_01");
prototype.addSetting("TCP Tagging Host address", OV_TypeId_String, "localhost");
prototype.addSetting("TCP Tagging Host port", OV_TypeId_Integer, "15361");
prototype.addSetting("Default background filename", OV_TypeId_Filename,
"${Path_Data}/plugins/simple-visualization/p300-magic-card/openvibe-logo.png-offscreen");
prototype.addSetting("Card filename", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/p300-magic-card/01.png");
prototype.addSetting("Card filename", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/p300-magic-card/02.png");
prototype.addSetting("Card filename", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/p300-magic-card/03.png");
prototype.addSetting("Card filename", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/p300-magic-card/04.png");
prototype.addSetting("Card filename", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/p300-magic-card/05.png");
prototype.addSetting("Card filename", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/p300-magic-card/06.png");
prototype.addSetting("Card filename", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/p300-magic-card/07.png");
prototype.addSetting("Card filename", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/p300-magic-card/08.png");
prototype.addSetting("Card filename", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/p300-magic-card/09.png");
prototype.addSetting("Card filename", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/p300-magic-card/10.png");
prototype.addSetting("Card filename", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/p300-magic-card/11.png");
prototype.addSetting("Card filename", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/p300-magic-card/12.png");
return true;
}
_IsDerivedFromClass_Final_(IBoxAlgorithmDesc, OVP_ClassId_BoxAlgorithm_P300MagicCardVisualizationDesc)
};
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,730 @@
// @todo for clarity, the StimulusSender related code blocks should be pushed inside the class and away from here
#include "ovpCBoxAlgorithmP300SpellerVisualization.h"
#include "../utils.h"
#include <tcptagging/IStimulusSender.h>
#include <algorithm>
#include <list>
#include <string>
#include <vector>
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
static void ToggleButtonShowHideCB(GtkToggleToolButton* button, gpointer data)
{
if (gtk_toggle_tool_button_get_active(button)) { gtk_widget_show(GTK_WIDGET(data)); }
else { gtk_widget_hide(GTK_WIDGET(data)); }
}
// This callback flushes all accumulated stimulations to the TCP Tagging
// after the rendering has completed.
static gboolean FlushCB(gpointer data)
{
(static_cast<CBoxAlgorithmP300SpellerVisualization*>(data))->flushQueue();
return false; // Only run once
}
bool CBoxAlgorithmP300SpellerVisualization::initialize()
{
const Kernel::IBox& boxContext = this->getStaticBoxContext();
m_mainWidgetInterface = nullptr;
m_toolbarWidgetInterface = nullptr;
m_flashFontDesc = nullptr;
m_noFlashFontDesc = nullptr;
m_targetFontDesc = nullptr;
m_selectedFontDesc = nullptr;
// ----------------------------------------------------------------------------------------------------------------------------------------------------------
m_interfaceFilename = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 0);
m_rowStimulationBase = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 1);
m_columnStimulationBase = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 2);
m_flashBgColor = CGdkcolorAutoCast(boxContext, this->getConfigurationManager(), 3);
m_flashFgColor = CGdkcolorAutoCast(boxContext, this->getConfigurationManager(), 4);
m_flashFontSize = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 5);
m_noFlashBgColor = CGdkcolorAutoCast(boxContext, this->getConfigurationManager(), 6);
m_noFlashFgColor = CGdkcolorAutoCast(boxContext, this->getConfigurationManager(), 7);
m_noFlashFontSize = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 8);
m_targetBgColor = CGdkcolorAutoCast(boxContext, this->getConfigurationManager(), 9);
m_targetFgColor = CGdkcolorAutoCast(boxContext, this->getConfigurationManager(), 10);
m_targetFontSize = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 11);
m_selectedBgColor = CGdkcolorAutoCast(boxContext, this->getConfigurationManager(), 12);
m_selectedFgColor = CGdkcolorAutoCast(boxContext, this->getConfigurationManager(), 13);
m_selectedFontSize = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 14);
// ----------------------------------------------------------------------------------------------------------------------------------------------------------
m_sequenceStimulationDecoder = &this->getAlgorithmManager().getAlgorithm(
this->getAlgorithmManager().createAlgorithm(OVP_GD_ClassId_Algorithm_StimulationDecoder));
m_sequenceStimulationDecoder->initialize();
m_targetStimulationDecoder = &this->getAlgorithmManager().getAlgorithm(
this->getAlgorithmManager().createAlgorithm(OVP_GD_ClassId_Algorithm_StimulationDecoder));
m_targetStimulationDecoder->initialize();
m_targetFlaggingStimulationEncoder = &this->getAlgorithmManager().getAlgorithm(
this->getAlgorithmManager().createAlgorithm(OVP_GD_ClassId_Algorithm_StimulationEncoder));
m_targetFlaggingStimulationEncoder->initialize();
m_rowSelectionStimulationDecoder = &this->getAlgorithmManager().getAlgorithm(
this->getAlgorithmManager().createAlgorithm(OVP_GD_ClassId_Algorithm_StimulationDecoder));
m_rowSelectionStimulationDecoder->initialize();
m_columnSelectionStimulationDecoder = &this->getAlgorithmManager().getAlgorithm(
this->getAlgorithmManager().createAlgorithm(OVP_GD_ClassId_Algorithm_StimulationDecoder));
m_columnSelectionStimulationDecoder->initialize();
m_sequenceMemoryBuffer.initialize(
m_sequenceStimulationDecoder->getInputParameter(OVP_GD_Algorithm_StimulationDecoder_InputParameterId_MemoryBufferToDecode));
m_sequenceStimulationSet.initialize(
m_sequenceStimulationDecoder->getOutputParameter(OVP_GD_Algorithm_StimulationDecoder_OutputParameterId_StimulationSet));
m_targetMemoryBuffer.initialize(
m_targetStimulationDecoder->getInputParameter(OVP_GD_Algorithm_StimulationDecoder_InputParameterId_MemoryBufferToDecode));
m_targetStimulationSet.initialize(
m_targetStimulationDecoder->getOutputParameter(OVP_GD_Algorithm_StimulationDecoder_OutputParameterId_StimulationSet));
m_targetFlaggingStimulationSet.initialize(
m_targetFlaggingStimulationEncoder->getInputParameter(OVP_GD_Algorithm_StimulationEncoder_InputParameterId_StimulationSet));
m_targetFlaggingMemoryBuffer.initialize(
m_targetFlaggingStimulationEncoder->getOutputParameter(OVP_GD_Algorithm_StimulationEncoder_OutputParameterId_EncodedMemoryBuffer));
m_lastTime = 0;
m_stimulusSender = nullptr;
m_idleFuncTag = 0;
m_stimuliQueue.clear();
m_mainWidgetInterface = gtk_builder_new(); // glade_xml_new(m_interfaceFilename.toASCIIString(), "p300-speller-main", nullptr);
if (!gtk_builder_add_from_file(m_mainWidgetInterface, m_interfaceFilename.toASCIIString(), nullptr))
{
this->getLogManager() << Kernel::LogLevel_ImportantWarning << "Could not load interface file [" << m_interfaceFilename << "]\n";
this->getLogManager() << Kernel::LogLevel_ImportantWarning <<
"The file may be missing. However, the interface files now use gtk-builder instead of glade. Did you update your files ?\n";
return false;
}
m_toolbarWidgetInterface = gtk_builder_new(); // glade_xml_new(m_interfaceFilename.toASCIIString(), "p300-speller-toolbar", nullptr);
gtk_builder_add_from_file(m_toolbarWidgetInterface, m_interfaceFilename.toASCIIString(), nullptr);
m_mainWindow = GTK_WIDGET(gtk_builder_get_object(m_mainWidgetInterface, "p300-speller-main"));
m_toolbarWidget = GTK_WIDGET(gtk_builder_get_object(m_toolbarWidgetInterface, "p300-speller-toolbar"));
m_table = GTK_TABLE(gtk_builder_get_object(m_mainWidgetInterface, "p300-speller-table"));
m_result = GTK_LABEL(gtk_builder_get_object(m_mainWidgetInterface, "label-result"));
m_target = GTK_LABEL(gtk_builder_get_object(m_mainWidgetInterface, "label-target"));
gtk_builder_connect_signals(m_mainWidgetInterface, nullptr);
gtk_builder_connect_signals(m_toolbarWidgetInterface, nullptr);
g_signal_connect(gtk_builder_get_object(m_toolbarWidgetInterface, "toolbutton-show_target_text"), "toggled", G_CALLBACK(ToggleButtonShowHideCB),
gtk_builder_get_object(m_mainWidgetInterface, "label-target"));
g_signal_connect(gtk_builder_get_object(m_toolbarWidgetInterface, "toolbutton-show_target_text"), "toggled", G_CALLBACK(ToggleButtonShowHideCB),
gtk_builder_get_object(m_mainWidgetInterface, "label-target-title"));
g_signal_connect(gtk_builder_get_object(m_toolbarWidgetInterface, "toolbutton-show_result_text"), "toggled", G_CALLBACK(ToggleButtonShowHideCB),
gtk_builder_get_object(m_mainWidgetInterface, "label-result"));
g_signal_connect(gtk_builder_get_object(m_toolbarWidgetInterface, "toolbutton-show_result_text"), "toggled", G_CALLBACK(ToggleButtonShowHideCB),
gtk_builder_get_object(m_mainWidgetInterface, "label-result-title"));
m_visualizationCtx = dynamic_cast<VisualizationToolkit::IVisualizationContext*>(this->createPluginObject(
OVP_ClassId_Plugin_VisualizationCtx));
m_visualizationCtx->setWidget(*this, m_mainWindow);
m_visualizationCtx->setToolbar(*this, m_toolbarWidget);
guint nRow = 0, nCol = 0;
g_object_get(m_table, "n-rows", &nRow, nullptr);
g_object_get(m_table, "n-columns", &nCol, nullptr);
m_nRow = nRow;
m_nCol = nCol;
PangoFontDescription* maxFontDesc = pango_font_description_copy(pango_context_get_font_description(gtk_widget_get_pango_context(m_mainWindow)));
m_flashFontDesc = pango_font_description_copy(pango_context_get_font_description(gtk_widget_get_pango_context(m_mainWindow)));
m_noFlashFontDesc = pango_font_description_copy(pango_context_get_font_description(gtk_widget_get_pango_context(m_mainWindow)));
m_targetFontDesc = pango_font_description_copy(pango_context_get_font_description(gtk_widget_get_pango_context(m_mainWindow)));
m_selectedFontDesc = pango_font_description_copy(pango_context_get_font_description(gtk_widget_get_pango_context(m_mainWindow)));
uint64_t maxSize = 0;
maxSize = std::max(maxSize, m_flashFontSize);
maxSize = std::max(maxSize, m_noFlashFontSize);
maxSize = std::max(maxSize, m_targetFontSize);
maxSize = std::max(maxSize, m_selectedFontSize);
pango_font_description_set_size(maxFontDesc, gint(maxSize * PANGO_SCALE));
pango_font_description_set_size(m_flashFontDesc, gint(m_flashFontSize * PANGO_SCALE));
pango_font_description_set_size(m_noFlashFontDesc, gint(m_noFlashFontSize * PANGO_SCALE));
pango_font_description_set_size(m_targetFontDesc, gint(m_targetFontSize * PANGO_SCALE));
pango_font_description_set_size(m_selectedFontDesc, gint(m_selectedFontSize * PANGO_SCALE));
this->cacheBuildFromTable(m_table);
this->cacheForEach(&CBoxAlgorithmP300SpellerVisualization::cacheChangeBackgroundCB, &m_noFlashBgColor);
this->cacheForEach(&CBoxAlgorithmP300SpellerVisualization::cacheChangeForegroundCB, &m_noFlashFgColor);
this->cacheForEach(&CBoxAlgorithmP300SpellerVisualization::cacheChangeFontCB, maxFontDesc);
pango_font_description_free(maxFontDesc);
m_lastTargetRow = -1;
m_lastTargetCol = -1;
m_targetRow = -1;
m_targetCol = -1;
m_selectedRow = -1;
m_selectedCol = -1;
m_stimulusSender = TCPTagging::CreateStimulusSender();
if (!m_stimulusSender->connect("localhost", "15361"))
{
this->getLogManager() << Kernel::LogLevel_Warning << "Unable to connect to AS TCP Tagging, stimuli wont be forwarded.\n";
}
m_tableInitialized = false;
return true;
}
bool CBoxAlgorithmP300SpellerVisualization::uninitialize()
{
if (m_idleFuncTag)
{
m_stimuliQueue.clear();
g_source_remove(m_idleFuncTag);
m_idleFuncTag = 0;
}
if (m_stimulusSender)
{
delete m_stimulusSender;
m_stimulusSender = nullptr;
}
if (m_selectedFontDesc)
{
pango_font_description_free(m_selectedFontDesc);
m_selectedFontDesc = nullptr;
}
if (m_targetFontDesc)
{
pango_font_description_free(m_targetFontDesc);
m_targetFontDesc = nullptr;
}
if (m_noFlashFontDesc)
{
pango_font_description_free(m_noFlashFontDesc);
m_noFlashFontDesc = nullptr;
}
if (m_flashFontDesc)
{
pango_font_description_free(m_flashFontDesc);
m_flashFontDesc = nullptr;
}
if (m_toolbarWidgetInterface)
{
g_object_unref(m_toolbarWidgetInterface);
m_toolbarWidgetInterface = nullptr;
}
if (m_mainWidgetInterface)
{
g_object_unref(m_mainWidgetInterface);
m_mainWidgetInterface = nullptr;
}
m_targetFlaggingStimulationSet.uninitialize();
m_targetFlaggingMemoryBuffer.uninitialize();
m_targetStimulationSet.uninitialize();
m_targetMemoryBuffer.uninitialize();
m_sequenceStimulationSet.uninitialize();
m_sequenceMemoryBuffer.uninitialize();
if (m_columnSelectionStimulationDecoder)
{
m_columnSelectionStimulationDecoder->uninitialize();
this->getAlgorithmManager().releaseAlgorithm(*m_columnSelectionStimulationDecoder);
m_columnSelectionStimulationDecoder = nullptr;
}
if (m_rowSelectionStimulationDecoder)
{
m_rowSelectionStimulationDecoder->uninitialize();
this->getAlgorithmManager().releaseAlgorithm(*m_rowSelectionStimulationDecoder);
m_rowSelectionStimulationDecoder = nullptr;
}
if (m_targetFlaggingStimulationEncoder)
{
m_targetFlaggingStimulationEncoder->uninitialize();
this->getAlgorithmManager().releaseAlgorithm(*m_targetFlaggingStimulationEncoder);
m_targetFlaggingStimulationEncoder = nullptr;
}
if (m_targetStimulationDecoder)
{
m_targetStimulationDecoder->uninitialize();
this->getAlgorithmManager().releaseAlgorithm(*m_targetStimulationDecoder);
m_targetStimulationDecoder = nullptr;
}
if (m_sequenceStimulationDecoder)
{
m_sequenceStimulationDecoder->uninitialize();
this->getAlgorithmManager().releaseAlgorithm(*m_sequenceStimulationDecoder);
m_sequenceStimulationDecoder = nullptr;
}
if (m_visualizationCtx)
{
this->releasePluginObject(m_visualizationCtx);
m_sequenceStimulationDecoder = nullptr;
}
return true;
}
bool CBoxAlgorithmP300SpellerVisualization::processInput(const size_t /*index*/)
{
this->getBoxAlgorithmContext()->markAlgorithmAsReadyToProcess();
if (!m_tableInitialized)
{
this->cacheForEach(&CBoxAlgorithmP300SpellerVisualization::cacheChangeBackgroundCB, &m_noFlashBgColor);
this->cacheForEach(&CBoxAlgorithmP300SpellerVisualization::cacheChangeForegroundCB, &m_noFlashFgColor);
this->cacheForEach(&CBoxAlgorithmP300SpellerVisualization::cacheChangeFontCB, m_noFlashFontDesc);
m_tableInitialized = true;
}
return true;
}
bool CBoxAlgorithmP300SpellerVisualization::process()
{
Kernel::IBoxIO& boxContext = this->getDynamicBoxContext();
// --- Sequence stimulations
for (size_t i = 0; i < boxContext.getInputChunkCount(0); ++i)
{
CStimulationSet flaggingStimulationSet;
m_sequenceMemoryBuffer = boxContext.getInputChunk(0, i);
m_targetFlaggingStimulationSet = &flaggingStimulationSet;
m_targetFlaggingMemoryBuffer = boxContext.getOutputChunk(0);
m_sequenceStimulationDecoder->process();
m_lastTime = boxContext.getInputChunkEndTime(0, i);
if (m_sequenceStimulationDecoder->isOutputTriggerActive(OVP_GD_Algorithm_StimulationDecoder_OutputTriggerId_ReceivedHeader))
{
m_targetFlaggingStimulationEncoder->process(OVP_GD_Algorithm_StimulationEncoder_InputTriggerId_EncodeHeader);
}
if (m_sequenceStimulationDecoder->isOutputTriggerActive(OVP_GD_Algorithm_StimulationDecoder_OutputTriggerId_ReceivedBuffer))
{
IStimulationSet* stimulationSet = m_sequenceStimulationSet;
for (size_t j = 0; j < stimulationSet->getStimulationCount(); ++j)
{
uint64_t id = stimulationSet->getStimulationIdentifier(j);
bool flash = false;
int row = -1;
int col = -1;
bool isTarget = false;
if (id >= m_rowStimulationBase && id < m_rowStimulationBase + m_nRow)
{
row = int(id - m_rowStimulationBase);
flash = true;
isTarget = (row == m_lastTargetRow);
}
if (id >= m_columnStimulationBase && id < m_columnStimulationBase + m_nCol)
{
col = int(id - m_columnStimulationBase);
flash = true;
isTarget = (col == m_lastTargetCol);
}
if (id == OVTK_StimulationId_VisualStimulationStop)
{
this->getLogManager() << Kernel::LogLevel_Debug << "Received OVTK_StimulationId_VisualStimulationStop - resets grid\n";
this->cacheForEach(&CBoxAlgorithmP300SpellerVisualization::cacheChangeBackgroundCB, &m_noFlashBgColor);
this->cacheForEach(&CBoxAlgorithmP300SpellerVisualization::cacheChangeForegroundCB, &m_noFlashFgColor);
this->cacheForEach(&CBoxAlgorithmP300SpellerVisualization::cacheChangeFontCB, m_noFlashFontDesc);
}
if (id == OVTK_StimulationId_Reset)
{
gtk_label_set_text(m_target, "");
gtk_label_set_text(m_result, "");
}
if (flash)
{
this->cacheForEachIf(row, col, &CBoxAlgorithmP300SpellerVisualization::cacheChangeBackgroundCB,
&CBoxAlgorithmP300SpellerVisualization::cacheChangeBackgroundCB, &m_flashBgColor, &m_noFlashBgColor);
this->cacheForEachIf(row, col, &CBoxAlgorithmP300SpellerVisualization::cacheChangeForegroundCB,
&CBoxAlgorithmP300SpellerVisualization::cacheChangeForegroundCB, &m_flashFgColor, &m_noFlashFgColor);
this->cacheForEachIf(row, col, &CBoxAlgorithmP300SpellerVisualization::cacheChangeFontCB,
&CBoxAlgorithmP300SpellerVisualization::cacheChangeFontCB, m_flashFontDesc, m_noFlashFontDesc);
// We now know if this flash corresponds to the current target or not, merge this to the outgoing stimulation stream
if (isTarget)
{
m_stimuliQueue.push_back(OVTK_StimulationId_Target);
flaggingStimulationSet.appendStimulation(OVTK_StimulationId_Target, stimulationSet->getStimulationDate(j), 0);
}
else
{
m_stimuliQueue.push_back(OVTK_StimulationId_NonTarget);
flaggingStimulationSet.appendStimulation(OVTK_StimulationId_NonTarget, stimulationSet->getStimulationDate(j), 0);
}
}
// Pass the stimulation to the server also as-is. If its a flash, it can be differentiated from a 'target' spec because
// its NOT between OVTK_StimulationId_RestStart and OVTK_StimulationId_RestStop stimuli in the generated P300 timeline.
m_stimuliQueue.push_back(id);
}
m_targetFlaggingStimulationEncoder->process(OVP_GD_Algorithm_StimulationEncoder_InputTriggerId_EncodeBuffer);
}
if (m_sequenceStimulationDecoder->isOutputTriggerActive(OVP_GD_Algorithm_StimulationDecoder_OutputTriggerId_ReceivedEnd))
{
m_targetFlaggingStimulationEncoder->process(OVP_GD_Algorithm_StimulationEncoder_InputTriggerId_EncodeEnd);
}
boxContext.markInputAsDeprecated(0, i);
boxContext.markOutputAsReadyToSend(0, boxContext.getInputChunkStartTime(0, i), boxContext.getInputChunkEndTime(0, i));
}
// --- Target stimulations
for (size_t i = 0; i < boxContext.getInputChunkCount(1); ++i)
{
if (m_lastTime >= boxContext.getInputChunkStartTime(1, i))
{
m_targetMemoryBuffer = boxContext.getInputChunk(1, i);
m_targetStimulationDecoder->process();
if (m_targetStimulationDecoder->isOutputTriggerActive(OVP_GD_Algorithm_StimulationDecoder_OutputTriggerId_ReceivedHeader)) { }
if (m_targetStimulationDecoder->isOutputTriggerActive(OVP_GD_Algorithm_StimulationDecoder_OutputTriggerId_ReceivedBuffer))
{
IStimulationSet* stimulationSet = m_targetStimulationSet;
for (size_t j = 0; j < stimulationSet->getStimulationCount(); ++j)
{
uint64_t id = stimulationSet->getStimulationIdentifier(j);
bool target = false;
if (id >= m_rowStimulationBase && id < m_rowStimulationBase + m_nRow)
{
this->getLogManager() << Kernel::LogLevel_Debug << "Received Target Row " << id << "\n";
m_targetRow = int(id - m_rowStimulationBase);
target = true;
}
if (id >= m_columnStimulationBase && id < m_columnStimulationBase + m_nCol)
{
this->getLogManager() << Kernel::LogLevel_Debug << "Received Target Column " << id << "\n";
m_targetCol = int(id - m_columnStimulationBase);
target = true;
}
if (target && m_targetRow != -1 && m_targetCol != -1)
{
this->getLogManager() << Kernel::LogLevel_Debug << "Displays Target Cell\n";
this->cacheForEachIf(m_targetRow, m_targetCol, &CBoxAlgorithmP300SpellerVisualization::cacheChangeBackgroundCB,
&CBoxAlgorithmP300SpellerVisualization::cacheChangeNullCB, &m_targetBgColor, nullptr);
this->cacheForEachIf(m_targetRow, m_targetCol, &CBoxAlgorithmP300SpellerVisualization::cacheChangeForegroundCB,
&CBoxAlgorithmP300SpellerVisualization::cacheChangeNullCB, &m_targetFgColor, nullptr);
this->cacheForEachIf(m_targetRow, m_targetCol, &CBoxAlgorithmP300SpellerVisualization::cacheChangeFontCB,
&CBoxAlgorithmP300SpellerVisualization::cacheChangeNullCB, m_targetFontDesc, nullptr);
std::vector<GtkWidget*> widgets;
this->cacheForEachIf(m_targetRow, m_targetCol, &CBoxAlgorithmP300SpellerVisualization::cacheCollectChildWidgetCB,
&CBoxAlgorithmP300SpellerVisualization::cacheCollectChildWidgetCB, &widgets, nullptr);
// Merge the current target into the stimulation stream. It can be differentiated
// from a 'flash' spec because it IS between OVTK_StimulationId_RestStart and
// OVTK_StimulationId_RestStop stimulations in the P300 timeline.
{
m_stimuliQueue.push_back(m_targetRow + m_rowStimulationBase);
m_stimuliQueue.push_back(m_targetCol + m_columnStimulationBase);
}
if (widgets.size() == 1)
{
if (GTK_IS_LABEL(widgets[0]))
{
std::string label;
label = gtk_label_get_text(m_target);
label += gtk_label_get_text(GTK_LABEL(widgets[0]));
gtk_label_set_text(m_target, label.c_str());
}
else
{
this->getLogManager() << Kernel::LogLevel_Warning << "Expected label class widget... could not find a valid text to append\n";
}
}
else
{
this->getLogManager() << Kernel::LogLevel_Warning << "Did not find a unique widget at row:" << size_t(m_targetRow) << " column:" <<
size_t(m_targetCol) << "\n";
}
m_targetHistory.emplace_back(m_targetRow, m_targetCol);
m_lastTargetRow = m_targetRow;
m_lastTargetCol = m_targetCol;
m_targetRow = -1;
m_targetCol = -1;
}
}
}
if (m_targetStimulationDecoder->isOutputTriggerActive(OVP_GD_Algorithm_StimulationDecoder_OutputTriggerId_ReceivedEnd)) { }
boxContext.markInputAsDeprecated(1, i);
}
}
// --- Selection stimulations
for (size_t k = 2; k < 4; ++k)
{
Kernel::IAlgorithmProxy* decoder = (k == 2 ? m_rowSelectionStimulationDecoder : m_columnSelectionStimulationDecoder);
Kernel::TParameterHandler<const IMemoryBuffer*> selectionMemoryBuffer(
decoder->getInputParameter(OVP_GD_Algorithm_StimulationDecoder_InputParameterId_MemoryBufferToDecode));
Kernel::TParameterHandler<IStimulationSet*> selectionStimulationSet(
decoder->getOutputParameter(OVP_GD_Algorithm_StimulationDecoder_OutputParameterId_StimulationSet));
for (size_t i = 0; i < boxContext.getInputChunkCount(k); ++i)
{
if (m_lastTime >= boxContext.getInputChunkStartTime(k, i))
{
selectionMemoryBuffer = boxContext.getInputChunk(k, i);
decoder->process();
if (decoder->isOutputTriggerActive(OVP_GD_Algorithm_StimulationDecoder_OutputTriggerId_ReceivedHeader)) { }
if (decoder->isOutputTriggerActive(OVP_GD_Algorithm_StimulationDecoder_OutputTriggerId_ReceivedBuffer))
{
IStimulationSet* stimulationSet = selectionStimulationSet;
for (size_t j = 0; j < stimulationSet->getStimulationCount(); ++j)
{
uint64_t id = stimulationSet->getStimulationIdentifier(j);
bool selected = false;
if (id >= m_rowStimulationBase && id < m_rowStimulationBase + m_nRow)
{
this->getLogManager() << Kernel::LogLevel_Debug << "Received Selected Row " << id << "\n";
m_selectedRow = int(id - m_rowStimulationBase);
selected = true;
}
if (id >= m_columnStimulationBase && id < m_columnStimulationBase + m_nRow)
{
this->getLogManager() << Kernel::LogLevel_Debug << "Received Selected Column " << id << "\n";
m_selectedCol = int(id - m_columnStimulationBase);
selected = true;
}
if (id == OVTK_StimulationId_Label_00)
{
if (k == 2) { m_selectedRow = -2; }
if (k == 3) { m_selectedCol = -2; }
selected = true;
}
if (selected && m_selectedRow != -1 && m_selectedCol != -1)
{
if (m_selectedRow >= 0 && m_selectedCol >= 0)
{
this->getLogManager() << Kernel::LogLevel_Debug << "Displays Selected Cell\n";
this->cacheForEachIf(m_selectedRow, m_selectedCol, &CBoxAlgorithmP300SpellerVisualization::cacheChangeBackgroundCB,
&CBoxAlgorithmP300SpellerVisualization::cacheChangeNullCB, &m_selectedBgColor, nullptr);
this->cacheForEachIf(m_selectedRow, m_selectedCol, &CBoxAlgorithmP300SpellerVisualization::cacheChangeForegroundCB,
&CBoxAlgorithmP300SpellerVisualization::cacheChangeNullCB, &m_selectedFgColor, nullptr);
this->cacheForEachIf(m_selectedRow, m_selectedCol, &CBoxAlgorithmP300SpellerVisualization::cacheChangeFontCB,
&CBoxAlgorithmP300SpellerVisualization::cacheChangeNullCB, m_selectedFontDesc, nullptr);
std::vector<GtkWidget*> widgets;
this->cacheForEachIf(m_selectedRow, m_selectedCol, &CBoxAlgorithmP300SpellerVisualization::cacheCollectChildWidgetCB,
&CBoxAlgorithmP300SpellerVisualization::cacheCollectChildWidgetCB, &widgets, nullptr);
if (widgets.size() == 1)
{
if (GTK_IS_LABEL(widgets[0]))
{
std::string label;
label = gtk_label_get_text(GTK_LABEL(widgets[0]));
if (!m_targetHistory.empty())
{
auto it = m_targetHistory.begin();
bool correct = (it->first == m_selectedRow && it->second == m_selectedCol);
bool halfCorrect = (it->first == m_selectedRow || it->second == m_selectedCol);
m_targetHistory.pop_front();
std::string tmp;
if (correct) { tmp = "<span color=\"darkgreen\">"; }
else if (halfCorrect) { tmp = "<span color=\"darkorange\">"; }
else { tmp = "<span color=\"darkred\">"; }
label = tmp.append(label).append("</span>");
}
label = std::string(gtk_label_get_label(m_result)).append(label);
gtk_label_set_markup(m_result, label.c_str());
}
else
{
this->getLogManager() << Kernel::LogLevel_Warning <<
"Expected label class widget... could not find a valid text to append\n";
}
}
else
{
this->getLogManager() << Kernel::LogLevel_Warning << "Did not find a unique widget at row : " << size_t(m_selectedRow) <<
" column : " << size_t(m_selectedCol) << "\n";
}
}
else
{
this->getLogManager() << Kernel::LogLevel_Trace << "Selection Rejected !\n";
std::string label;
label = gtk_label_get_text(m_result);
label += "*";
gtk_label_set_text(m_result, label.c_str());
}
m_selectedRow = -1;
m_selectedCol = -1;
}
}
}
if (decoder->isOutputTriggerActive(OVP_GD_Algorithm_StimulationDecoder_OutputTriggerId_ReceivedEnd)) { }
boxContext.markInputAsDeprecated(k, i);
}
}
}
// After any possible rendering, we flush the accumulated stimuli. The default idle func is low priority, so it should be run after rendering by gtk.
if (m_idleFuncTag == 0) { m_idleFuncTag = g_idle_add(FlushCB, this); }
return true;
}
// _________________________________________________________________________________________________________________________________________________________
//
void CBoxAlgorithmP300SpellerVisualization::cacheBuildFromTable(GtkTable* table)
{
if (table)
{
const GdkColor white = InitGDKColor(65535, 65535, 65535, 65535);
for (GList* list = table->children; list; list = list->next)
{
GtkTableChild* child = static_cast<GtkTableChild*>(list->data);
for (size_t i = child->top_attach; i < child->bottom_attach; ++i)
{
for (size_t j = child->left_attach; j < child->right_attach; ++j)
{
widget_style_t& style = m_cache[i][j];
style.widget = child->widget;
style.childWidget = gtk_bin_get_child(GTK_BIN(child->widget));
style.bgColor = white;
style.fgColor = white;
style.fontDesc = nullptr;
}
}
}
}
}
void CBoxAlgorithmP300SpellerVisualization::cacheForEach(const cache_callback callback, void* data)
{
for (auto i = m_cache.begin(); i != m_cache.end(); ++i)
{
for (auto j = i->second.begin(); j != i->second.end(); ++j) { (this->*callback)(j->second, data); }
}
}
void CBoxAlgorithmP300SpellerVisualization::cacheForEachIf(const int iLine, const int iColumn, const cache_callback ifCB, const cache_callback elseCB,
void* ifUserData, void* elseUserData)
{
for (auto i = m_cache.begin(); i != m_cache.end(); ++i)
{
for (auto j = i->second.begin(); j != i->second.end(); ++j)
{
const bool line = (iLine != -1);
const bool column = (iColumn != -1);
bool inLine = false;
bool inCol = false;
bool first;
if (line && size_t(iLine) == i->first) { inLine = true; }
if (column && size_t(iColumn) == j->first) { inCol = true; }
if (line && column) { first = inLine && inCol; }
else { first = inLine || inCol; }
if (first) { (this->*ifCB)(j->second, ifUserData); }
else { (this->*elseCB)(j->second, elseUserData); }
}
}
}
void CBoxAlgorithmP300SpellerVisualization::cacheChangeNullCB(widget_style_t& /*rWidgetStyle*/, void* /*data*/) { }
void CBoxAlgorithmP300SpellerVisualization::cacheChangeBackgroundCB(widget_style_t& style, void* data)
{
GdkColor oColor = *static_cast<GdkColor*>(data);
if (memcmp(&style.bgColor, &oColor, sizeof(GdkColor)) != 0)
{
gtk_widget_modify_bg(style.widget, GTK_STATE_NORMAL, &oColor);
style.bgColor = oColor;
}
}
void CBoxAlgorithmP300SpellerVisualization::cacheChangeForegroundCB(widget_style_t& style, void* data)
{
GdkColor oColor = *static_cast<GdkColor*>(data);
if (memcmp(&style.fgColor, &oColor, sizeof(GdkColor)) != 0)
{
gtk_widget_modify_fg(style.childWidget, GTK_STATE_NORMAL, &oColor);
style.fgColor = oColor;
}
}
void CBoxAlgorithmP300SpellerVisualization::cacheChangeFontCB(widget_style_t& style, void* data)
{
auto* pFontDescription = static_cast<PangoFontDescription*>(data);
if (style.fontDesc != pFontDescription)
{
gtk_widget_modify_font(style.childWidget, pFontDescription);
style.fontDesc = pFontDescription;
}
}
void CBoxAlgorithmP300SpellerVisualization::cacheCollectWidgetCB(widget_style_t& style, void* data)
{
if (data) { (static_cast<std::vector<GtkWidget*>*>(data))->push_back(style.widget); }
}
void CBoxAlgorithmP300SpellerVisualization::cacheCollectChildWidgetCB(widget_style_t& style, void* data)
{
if (data) { (static_cast<std::vector<GtkWidget*>*>(data))->push_back(style.childWidget); }
}
// Note that we don't need concurrency control here as gtk callbacks run in the main thread
void CBoxAlgorithmP300SpellerVisualization::flushQueue()
{
for (const auto& stimulation : m_stimuliQueue) { m_stimulusSender->sendStimulation(stimulation); }
m_stimuliQueue.clear();
// This function will be automatically removed after completion, so set to 0
m_idleFuncTag = 0;
}
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,184 @@
#pragma once
#include "../ovp_defines.h"
#include <openvibe/ov_all.h>
#include <toolkit/ovtk_all.h>
#include <visualization-toolkit/ovviz_all.h>
#include <gtk/gtk.h>
#include <list>
#include <map>
#include "../utils.h"
namespace TCPTagging {
class IStimulusSender; // fwd declare
} // namespace TCPTagging
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
class CBoxAlgorithmP300SpellerVisualization final : public Toolkit::TBoxAlgorithm<IBoxAlgorithm>
{
public:
void release() override { delete this; }
bool initialize() override;
bool uninitialize() override;
bool processInput(const size_t index) override;
bool process() override;
_IsDerivedFromClass_Final_(Toolkit::TBoxAlgorithm<IBoxAlgorithm>, OVP_ClassId_BoxAlgorithm_P300SpellerVisualization)
private:
using widget_style_t = struct
{
GtkWidget* widget;
GtkWidget* childWidget;
GdkColor bgColor;
GdkColor fgColor;
PangoFontDescription* fontDesc;
};
typedef void (CBoxAlgorithmP300SpellerVisualization::*cache_callback)(widget_style_t& style, void* data);
void cacheBuildFromTable(GtkTable* table);
void cacheForEach(cache_callback callback, void* data);
void cacheForEachIf(int iLine, int iColumn, cache_callback ifCB, cache_callback elseCB, void* ifUserData, void* elseUserData);
void cacheChangeNullCB(widget_style_t& style, void* data);
void cacheChangeBackgroundCB(widget_style_t& style, void* data);
void cacheChangeForegroundCB(widget_style_t& style, void* data);
void cacheChangeFontCB(widget_style_t& style, void* data);
void cacheCollectWidgetCB(widget_style_t& style, void* data);
void cacheCollectChildWidgetCB(widget_style_t& style, void* data);
public:
void flushQueue(); // Sends all accumulated stimuli to the TCP Tagging
protected:
CString m_interfaceFilename;
uint64_t m_rowStimulationBase = 0;
uint64_t m_columnStimulationBase = 0;
GdkColor m_flashBgColor = InitGDKColor(0, 6554, 6554, 6554);
GdkColor m_flashFgColor = InitGDKColor(0, 65535, 65535, 65535);
uint64_t m_flashFontSize = 100;
PangoFontDescription* m_flashFontDesc = nullptr;
GdkColor m_noFlashBgColor = InitGDKColor(0, 0, 0, 0);
GdkColor m_noFlashFgColor = InitGDKColor(0, 32768, 32768, 32768);
uint64_t m_noFlashFontSize = 75;
PangoFontDescription* m_noFlashFontDesc = nullptr;
GdkColor m_targetBgColor = InitGDKColor(0, 6554, 26214, 6554);
GdkColor m_targetFgColor = InitGDKColor(0, 39321, 65535, 39321);
uint64_t m_targetFontSize = 100;
PangoFontDescription* m_targetFontDesc = nullptr;
GdkColor m_selectedBgColor = InitGDKColor(0, 45875, 13107, 13107);
GdkColor m_selectedFgColor = InitGDKColor(0, 19661, 6554, 6554);
uint64_t m_selectedFontSize = 100;
PangoFontDescription* m_selectedFontDesc = nullptr;
private:
Kernel::IAlgorithmProxy* m_sequenceStimulationDecoder = nullptr;
Kernel::IAlgorithmProxy* m_targetStimulationDecoder = nullptr;
Kernel::IAlgorithmProxy* m_targetFlaggingStimulationEncoder = nullptr;
Kernel::IAlgorithmProxy* m_rowSelectionStimulationDecoder = nullptr;
Kernel::IAlgorithmProxy* m_columnSelectionStimulationDecoder = nullptr;
Kernel::TParameterHandler<const IMemoryBuffer*> m_sequenceMemoryBuffer;
Kernel::TParameterHandler<const IMemoryBuffer*> m_targetMemoryBuffer;
Kernel::TParameterHandler<const IStimulationSet*> m_targetFlaggingStimulationSet;
Kernel::TParameterHandler<IStimulationSet*> m_sequenceStimulationSet;
Kernel::TParameterHandler<IStimulationSet*> m_targetStimulationSet;
Kernel::TParameterHandler<IMemoryBuffer*> m_targetFlaggingMemoryBuffer;
uint64_t m_lastTime = 0;
GtkBuilder* m_mainWidgetInterface = nullptr;
GtkBuilder* m_toolbarWidgetInterface = nullptr;
GtkWidget* m_mainWindow = nullptr;
GtkWidget* m_toolbarWidget = nullptr;
GtkTable* m_table = nullptr;
GtkLabel* m_result = nullptr;
GtkLabel* m_target = nullptr;
uint64_t m_nRow = 0;
uint64_t m_nCol = 0;
int m_lastTargetRow = 0;
int m_lastTargetCol = 0;
int m_targetRow = 0;
int m_targetCol = 0;
int m_selectedRow = 0;
int m_selectedCol = 0;
bool m_tableInitialized = false;
// @todo refactor to std::pair<long,long> ?
std::map<size_t, std::map<size_t, widget_style_t>> m_cache;
std::list<std::pair<int, int>> m_targetHistory;
// TCP Tagging
std::vector<uint64_t> m_stimuliQueue;
guint m_idleFuncTag = 0;
TCPTagging::IStimulusSender* m_stimulusSender = nullptr;
VisualizationToolkit::IVisualizationContext* m_visualizationCtx = nullptr;
};
class CBoxAlgorithmP300SpellerVisualizationDesc final : public IBoxAlgorithmDesc
{
public:
void release() override { }
CString getName() const override { return CString("P300 Speller Visualization"); }
CString getAuthorName() const override { return CString("Yann Renard"); }
CString getAuthorCompanyName() const override { return CString("INRIA"); }
CString getShortDescription() const override { return CString("Visualizes the alphabet for P300 spellers"); }
CString getDetailedDescription() const override { return CString(""); }
CString getCategory() const override { return CString("Visualization/Presentation"); }
CString getVersion() const override { return CString("1.0"); }
CString getStockItemName() const override { return CString("gtk-select-font"); }
CIdentifier getCreatedClass() const override { return OVP_ClassId_BoxAlgorithm_P300SpellerVisualization; }
IPluginObject* create() override { return new CBoxAlgorithmP300SpellerVisualization; }
bool hasFunctionality(const EPluginFunctionality functionality) const override { return functionality == EPluginFunctionality::Visualization; }
bool getBoxPrototype(Kernel::IBoxProto& prototype) const override
{
prototype.addInput("Sequence stimulations", OV_TypeId_Stimulations);
prototype.addInput("Target stimulations", OV_TypeId_Stimulations);
prototype.addInput("Row selection stimulations", OV_TypeId_Stimulations);
prototype.addInput("Column selection stimulations", OV_TypeId_Stimulations);
prototype.addOutput("Target / Non target flagging (deprecated)", OV_TypeId_Stimulations);
prototype.addSetting("Interface filename", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/p300-speller.ui");
prototype.addSetting("Row stimulation base", OV_TypeId_Stimulation, "OVTK_StimulationId_Label_01");
prototype.addSetting("Column stimulation base", OV_TypeId_Stimulation, "OVTK_StimulationId_Label_07");
prototype.addSetting("Flash background color", OV_TypeId_Color, "10,10,10");
prototype.addSetting("Flash foreground color", OV_TypeId_Color, "100,100,100");
prototype.addSetting("Flash font size", OV_TypeId_Integer, "100");
prototype.addSetting("No flash background color", OV_TypeId_Color, "0,0,0");
prototype.addSetting("No flash foreground color", OV_TypeId_Color, "50,50,50");
prototype.addSetting("No flash font size", OV_TypeId_Integer, "75");
prototype.addSetting("Target background color", OV_TypeId_Color, "10,40,10");
prototype.addSetting("Target foreground color", OV_TypeId_Color, "60,100,60");
prototype.addSetting("Target font size", OV_TypeId_Integer, "100");
prototype.addSetting("Selected background color", OV_TypeId_Color, "70,20,20");
prototype.addSetting("Selected foreground color", OV_TypeId_Color, "30,10,10");
prototype.addSetting("Selected font size", OV_TypeId_Integer, "100");
return true;
}
_IsDerivedFromClass_Final_(IBoxAlgorithmDesc, OVP_ClassId_BoxAlgorithm_P300SpellerVisualizationDesc)
};
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,492 @@
#include "ovpCDisplayCueImage.h"
#include <algorithm> // std::min
#include <tcptagging/IStimulusSender.h>
#if defined TARGET_OS_Linux
#include <unistd.h>
#endif
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
// This callback flushes all accumulated stimulations to the TCP Tagging
// after the rendering has completed.
static gboolean DisplayCueImageFlushCB(gpointer data)
{
reinterpret_cast<CDisplayCueImage*>(data)->flushQueue();
return false; // Only run once
}
static gboolean DisplayCueImageResizeCB(GtkWidget* /*widget*/, GtkAllocation* allocation, gpointer data)
{
reinterpret_cast<CDisplayCueImage*>(data)->resize(size_t(allocation->width), size_t(allocation->height));
return FALSE;
}
static gboolean DisplayCueImageRedrawCB(GtkWidget* /*widget*/, GdkEventExpose* /*event*/, gpointer data)
{
reinterpret_cast<CDisplayCueImage*>(data)->redraw();
return TRUE;
}
bool CDisplayCueImage::initialize()
{
m_idleFuncTag = 0;
m_stimulusSender = nullptr;
//>>>> Reading Settings:
//Number of Cues:
m_numberOfCues = (getStaticBoxContext().getSettingCount() - NON_CUE_SETTINGS_COUNT) / 2;
//Do we display the images in full screen?
m_fullScreen = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 0);
m_scaleImages = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 1);
//Clear screen stimulation:
m_clearScreenStimulation = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 2);
//Stimulation ID and images file names for each cue
m_imageNames.resize(m_numberOfCues);
m_stimulationsIds.resize(m_numberOfCues);
for (size_t i = 0; i < m_numberOfCues; ++i)
{
m_imageNames[i] = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), NON_CUE_SETTINGS_COUNT + 2 * i);
m_stimulationsIds[i] = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), NON_CUE_SETTINGS_COUNT + 2 * i + 1);
}
//>>>> Initialisation
m_stimulationDecoder.initialize(*this, 0);
m_stimulationEncoder.initialize(*this, 0);
//load the gtk builder interface
m_builderInterface = gtk_builder_new();
if (!m_builderInterface)
{
getBoxAlgorithmContext()->getPlayerContext()->getLogManager() << Kernel::LogLevel_Error << "Couldn't load the interface !";
return false;
}
const CString uiFile = Directories::getDataDir() + "/plugins/simple-visualization/openvibe-simple-visualization-DisplayCueImage.ui";
if (!gtk_builder_add_from_file(m_builderInterface, uiFile, nullptr))
{
getBoxAlgorithmContext()->getPlayerContext()->getLogManager() << Kernel::LogLevel_Error << "Could not load the .ui file " << uiFile << "\n";
return false;
}
gtk_builder_connect_signals(m_builderInterface, nullptr);
m_drawingArea = GTK_WIDGET(gtk_builder_get_object(m_builderInterface, "DisplayCueImageDrawingArea"));
g_signal_connect(G_OBJECT(m_drawingArea), "expose_event", G_CALLBACK(DisplayCueImageRedrawCB), this);
g_signal_connect(G_OBJECT(m_drawingArea), "size-allocate", G_CALLBACK(DisplayCueImageResizeCB), this);
//set widget bg color
gtk_widget_modify_bg(m_drawingArea, GTK_STATE_NORMAL, &m_backgroundColor);
gtk_widget_modify_bg(m_drawingArea, GTK_STATE_PRELIGHT, &m_backgroundColor);
gtk_widget_modify_bg(m_drawingArea, GTK_STATE_ACTIVE, &m_backgroundColor);
gtk_widget_modify_fg(m_drawingArea, GTK_STATE_NORMAL, &m_foregroundColor);
gtk_widget_modify_fg(m_drawingArea, GTK_STATE_PRELIGHT, &m_foregroundColor);
gtk_widget_modify_fg(m_drawingArea, GTK_STATE_ACTIVE, &m_foregroundColor);
//Load the pictures:
m_originalPictures.resize(m_numberOfCues);
m_scaledPictures.resize(m_numberOfCues);
for (size_t i = 0; i < m_numberOfCues; ++i)
{
m_originalPictures[i] = gdk_pixbuf_new_from_file_at_size(m_imageNames[i], -1, -1, nullptr);
m_scaledPictures[i] = nullptr;
if (!m_originalPictures[i])
{
getBoxAlgorithmContext()->getPlayerContext()->getLogManager() << Kernel::LogLevel_Error << "Error couldn't load resource file : " << m_imageNames[i]
<<
"!\n";
return false;
}
}
m_stimuliQueue.clear();
m_stimulusSender = TCPTagging::CreateStimulusSender();
if (!m_stimulusSender->connect("localhost", "15361"))
{
this->getLogManager() << Kernel::LogLevel_Warning << "Unable to connect to AS's TCP Tagging plugin, stimuli wont be forwarded.\n";
}
if (m_fullScreen)
{
GtkWidget* window = gtk_widget_get_toplevel(m_drawingArea);
gtk_window_fullscreen(GTK_WINDOW(window));
gtk_widget_show(window);
// @fixme small mem leak?
GdkCursor* cursor = gdk_cursor_new(GDK_BLANK_CURSOR);
gdk_window_set_cursor(gtk_widget_get_window(window), cursor);
}
else
{
m_visualizationCtx = dynamic_cast<VisualizationToolkit::IVisualizationContext*>(this->createPluginObject(OVP_ClassId_Plugin_VisualizationCtx));
m_visualizationCtx->setWidget(*this, m_drawingArea);
}
// Invalidate the drawing area in order to get the image resize already called at this point. The actual run will be smoother.
if (GTK_WIDGET(m_drawingArea)->window) { gdk_window_invalidate_rect(GTK_WIDGET(m_drawingArea)->window, nullptr, true); }
return true;
}
bool CDisplayCueImage::uninitialize()
{
// Remove the possibly dangling idle loop.
if (m_idleFuncTag)
{
g_source_remove(m_idleFuncTag);
m_idleFuncTag = 0;
}
m_stimulationDecoder.uninitialize();
m_stimulationEncoder.uninitialize();
if (m_stimulusSender)
{
delete m_stimulusSender;
m_stimulusSender = nullptr;
}
// Close the full screen
if (m_fullScreen)
{
GtkWidget* window = gtk_widget_get_toplevel(m_drawingArea);
gtk_window_unfullscreen(GTK_WINDOW(window));
gtk_widget_destroy(window);
}
//destroy drawing area
if (m_drawingArea)
{
gtk_widget_destroy(m_drawingArea);
m_drawingArea = nullptr;
}
// unref the xml file as it's not needed anymore
if (m_builderInterface)
{
g_object_unref(G_OBJECT(m_builderInterface));
m_builderInterface = nullptr;
}
m_stimulationsIds.clear();
m_imageNames.clear();
if (!m_originalPictures.empty())
{
for (size_t i = 0; i < m_numberOfCues; ++i) { if (m_originalPictures[i]) { g_object_unref(G_OBJECT(m_originalPictures[i])); } }
m_originalPictures.clear();
}
if (!m_scaledPictures.empty())
{
for (size_t i = 0; i < m_numberOfCues; ++i) { if (m_scaledPictures[i]) { g_object_unref(G_OBJECT(m_scaledPictures[i])); } }
m_scaledPictures.clear();
}
if (m_visualizationCtx)
{
this->releasePluginObject(m_visualizationCtx);
m_visualizationCtx = nullptr;
}
return true;
}
bool CDisplayCueImage::processClock(Kernel::CMessageClock& /*msg*/)
{
Kernel::IBoxIO* boxIO = getBoxAlgorithmContext()->getDynamicBoxContext();
m_stimulationEncoder.getInputStimulationSet()->clear();
if (this->getPlayerContext().getCurrentTime() == 0)
{
// Always send header first
m_stimulationEncoder.encodeHeader();
boxIO->markOutputAsReadyToSend(0, 0, 0);
}
if (m_imageDrawn)
{
// this is first redraw() for that image or clear screen
// we send a stimulation to signal it.
// @note this practice is deprecated, the TCP Tagging should be used. We pass the stimulus here for compatibility.
if (m_drawnImageId >= 0)
{
// it was a image
m_stimulationEncoder.getInputStimulationSet()->appendStimulation(m_stimulationsIds[m_drawnImageId], this->getPlayerContext().getCurrentTime(), 0);
}
else
{
// it was a clear_screen
m_stimulationEncoder.getInputStimulationSet()->appendStimulation(m_clearScreenStimulation, this->getPlayerContext().getCurrentTime(), 0);
}
m_imageDrawn = false;
if (m_drawnImageId != m_requestedImageId)
{
// We must be late...
getBoxAlgorithmContext()->getPlayerContext()->getLogManager() << Kernel::LogLevel_Warning <<
"One image may have been skipped => we must be late...\n";
}
}
m_stimulationEncoder.encodeBuffer();
boxIO->markOutputAsReadyToSend(0, m_lastOutputChunkDate, this->getPlayerContext().getCurrentTime());
m_lastOutputChunkDate = this->getPlayerContext().getCurrentTime();
bool stimulusMatchedBefore = false;
// We check if some images must be displayed
for (size_t stim = 0; stim < m_pendingStimulationSet.getStimulationCount();)
{
const uint64_t date = m_pendingStimulationSet.getStimulationDate(stim);
const uint64_t time = this->getPlayerContext().getCurrentTime();
if (date < time)
{
const uint64_t id = m_pendingStimulationSet.getStimulationIdentifier(stim);
bool stimulusMatchedNow = false;
if (id == m_clearScreenStimulation)
{
if (m_imageRequested)
{
getBoxAlgorithmContext()->getPlayerContext()->getLogManager() << Kernel::LogLevel_ImportantWarning <<
"Clear screen was received before previous cue image in slot " << m_requestedImageId + 1 << " was displayed!!\n";
}
m_imageRequested = true;
stimulusMatchedBefore = true;
stimulusMatchedNow = true;
m_requestedImageId = -1;
}
else
{
for (size_t i = 0; i < m_numberOfCues; ++i)
{
if (id == m_stimulationsIds[i])
{
if (m_imageRequested)
{
getBoxAlgorithmContext()->getPlayerContext()->getLogManager() << Kernel::LogLevel_ImportantWarning << "Previous request of slot " <<
m_requestedImageId + 1 << " image was replaced by request for slot " << i + 1 <<
" => Not enough time between two images!!\n";
}
m_imageRequested = true;
stimulusMatchedBefore = true;
stimulusMatchedNow = true;
m_requestedImageId = i;
break;
}
}
}
if (stimulusMatchedNow)
{
// Queue the recognized stimulation to TCP Tagging to be sent after rendering
const uint64_t sentStimulation = (m_requestedImageId >= 0 ? m_stimulationsIds[m_requestedImageId] : m_clearScreenStimulation);
m_stimuliQueue.push_back(sentStimulation);
}
else
{
// Pass unrecognized stimulations to TCP Tagging. Be careful when modifying the code that the
// stimuli received by AS keep their original time order despite the delays introduced to rendered stimuli.
if (stimulusMatchedBefore)
{
// We have queued a cue to be drawn, so we should delay this stimulation to TCP Tagging to be processed after the cue rendering to keep the time order
m_stimuliQueue.push_back(id);
}
else
{
// We have not yet queued anything to be drawn, so we can forward immediately.
m_stimulusSender->sendStimulation(id);
}
}
const double delay = CTime(time - date).toSeconds() * 1000; // delay in ms
if (delay > 50)
{
getBoxAlgorithmContext()->getPlayerContext()->getLogManager() << Kernel::LogLevel_Warning << "Stimulation " << id <<
" was late in processClock() : " << delay << " ms \n";
}
m_pendingStimulationSet.removeStimulation(stim);
}
else
{
// Stim is still in the future, skip it for now
stim++;
}
}
// We should show the cue image now. How this works:
// - The gtk drawing area is invalidated
// - Gtk will request a redraw
// - The redraw puts in instructions to render the new image
// - The corresponding stimulation is buffered to TCP Tagging
// - Callback to flush the TCP Tagging buffer is registered to be run by gtk once after the rendering
// - Gtk renders
// - Callback to flush the TCP Tagging buffer is called by gtk
if (m_imageRequested && GTK_WIDGET(m_drawingArea)->window)
{
// this will trigger the callback redraw(). Since the draw will happen after the exit from this function, no point calling this in the loop above
gdk_window_invalidate_rect(GTK_WIDGET(m_drawingArea)->window, nullptr, true);
}
return true;
}
bool CDisplayCueImage::processInput(const size_t /*index*/)
{
getBoxAlgorithmContext()->markAlgorithmAsReadyToProcess();
return true;
}
bool CDisplayCueImage::process()
{
Kernel::IBoxIO* boxIO = getBoxAlgorithmContext()->getDynamicBoxContext();
// We decode and save the received stimulations.
for (size_t input = 0; input < getBoxAlgorithmContext()->getStaticBoxContext()->getInputCount(); ++input)
{
for (size_t chunk = 0; chunk < boxIO->getInputChunkCount(input); ++chunk)
{
m_stimulationDecoder.decode(chunk, true);
if (m_stimulationDecoder.isHeaderReceived())
{
// nop
}
if (m_stimulationDecoder.isBufferReceived())
{
for (size_t stim = 0; stim < m_stimulationDecoder.getOutputStimulationSet()->getStimulationCount(); ++stim)
{
// We always add the stimulations to the set to allow passing them to TCP Tagging in order in processClock()
const uint64_t id = m_stimulationDecoder.getOutputStimulationSet()->getStimulationIdentifier(stim);
const uint64_t date = m_stimulationDecoder.getOutputStimulationSet()->getStimulationDate(stim);
const uint64_t duration = m_stimulationDecoder.getOutputStimulationSet()->getStimulationDuration(stim);
const uint64_t time = this->getPlayerContext().getCurrentTime();
if (date < time)
{
const double delay = CTime(time - date).toSeconds() * 1000; //delay in ms
if (delay > 50)
{
getBoxAlgorithmContext()->getPlayerContext()->getLogManager() << Kernel::LogLevel_Warning << "Stimulation " << id <<
" was received late: " << delay << " ms \n";
}
}
if (date < boxIO->getInputChunkStartTime(input, chunk))
{
this->getLogManager() << Kernel::LogLevel_ImportantWarning << "Input Stimulation Date before beginning of the buffer\n";
}
m_pendingStimulationSet.appendStimulation(id, date, duration);
}
}
}
}
return true;
}
//Callback called by GTK
void CDisplayCueImage::redraw()
{
if (m_requestedImageId >= 0) { drawCuePicture(m_requestedImageId); }
if (m_imageRequested)
{
m_imageRequested = false;
m_imageDrawn = true;
m_drawnImageId = m_requestedImageId;
// Set the handler to push out the queued stims after the actual rendering
if (m_idleFuncTag == 0) { m_idleFuncTag = g_idle_add(DisplayCueImageFlushCB, this); }
}
}
void CDisplayCueImage::drawCuePicture(const size_t cueID)
{
const gint width = m_drawingArea->allocation.width;
const gint height = m_drawingArea->allocation.height;
// Center image
const gint x = (width / 2) - gdk_pixbuf_get_width(m_scaledPictures[cueID]) / 2;
const gint y = (height / 2) - gdk_pixbuf_get_height(m_scaledPictures[cueID]) / 2;
gdk_draw_pixbuf(m_drawingArea->window, nullptr, m_scaledPictures[cueID], 0, 0, x, y, -1, -1, GDK_RGB_DITHER_NONE, 0, 0);
}
void CDisplayCueImage::resize(const size_t width, const size_t height)
{
for (auto& i : m_scaledPictures) { if (i) { g_object_unref(G_OBJECT(i)); } }
if (!m_scaleImages)
{
for (size_t i = 0; i < m_scaledPictures.size(); ++i) { m_scaledPictures[i] = gdk_pixbuf_copy(m_originalPictures[i]); }
return;
}
// Scale
if (m_fullScreen)
{
for (size_t i = 0; i < m_scaledPictures.size(); ++i)
{
// Keep aspect ratio when scaling
const int picWidth = gdk_pixbuf_get_width(m_originalPictures[i]),
picHeight = gdk_pixbuf_get_height(m_originalPictures[i]);
const double scaleWidth = width / double(picWidth),
scaleHeight = height / double(picHeight),
scaleMin = std::min<double>(scaleWidth, scaleHeight);
const int newWidth = int(scaleMin * picWidth),
newHeight = int(scaleMin * picHeight);
m_scaledPictures[i] = gdk_pixbuf_scale_simple(m_originalPictures[i], newWidth, newHeight, GDK_INTERP_BILINEAR);
}
}
else
{
const auto x = float(width < 64 ? 64 : width),
y = float(height < 64 ? 64 : height);
for (size_t i = 0; i < m_scaledPictures.size(); ++i)
{
auto picWidth = float(gdk_pixbuf_get_width(m_originalPictures[i])),
picHeight = float(gdk_pixbuf_get_height(m_originalPictures[i]));
if ((x / picWidth) < (y / picHeight))
{
picHeight = x * picHeight / (3 * picWidth);
picWidth = x / 3;
}
else
{
picWidth = y * picWidth / (3 * picHeight);
picHeight = y / 3;
}
m_scaledPictures[i] = gdk_pixbuf_scale_simple(m_originalPictures[i], int(picWidth), int(picHeight), GDK_INTERP_BILINEAR);
}
}
}
// Note that we don't need concurrency control here as gtk callbacks run in the main thread
void CDisplayCueImage::flushQueue()
{
for (const auto& stimulation : m_stimuliQueue) { m_stimulusSender->sendStimulation(stimulation); }
m_stimuliQueue.clear();
// This function will be automatically removed after completion, so set to 0
m_idleFuncTag = 0;
}
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,209 @@
#pragma once
#include "../ovp_defines.h"
#include <openvibe/ov_all.h>
#include <toolkit/ovtk_all.h>
#include <visualization-toolkit/ovviz_all.h>
#include <gtk/gtk.h>
#include <deque>
#include <iomanip>
#include <map>
#include <sstream>
#include <string>
#include <vector>
#include "../utils.h"
namespace TCPTagging {
class IStimulusSender; // fwd declare
} // namespace TCPTagging
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
class CDisplayCueImage final : public Toolkit::TBoxAlgorithm<IBoxAlgorithm>
{
public:
void release() override { delete this; }
bool initialize() override;
bool uninitialize() override;
bool processInput(const size_t index) override;
uint64_t getClockFrequency() override { return (128LL << 32); } // 128hz
bool processClock(Kernel::CMessageClock& msg) override;
bool process() override;
void redraw();
void resize(size_t width, size_t height);
_IsDerivedFromClass_Final_(IBoxAlgorithm, OVP_ClassId_DisplayCueImage)
void flushQueue(); // Sends all accumulated stimuli to the TCP Tagging
static const size_t NON_CUE_SETTINGS_COUNT = 3; // fullscreen + scale + clear
protected:
void drawCuePicture(size_t cueID);
//The Builder handler used to create the interface
GtkBuilder* m_builderInterface = nullptr;
GtkWidget* m_mainWindow = nullptr;
GtkWidget* m_drawingArea = nullptr;
Toolkit::TStimulationDecoder<CDisplayCueImage> m_stimulationDecoder;
Toolkit::TStimulationEncoder<CDisplayCueImage> m_stimulationEncoder;
// For the display of the images:
bool m_imageRequested = false; //when true: a new image must be drawn
int m_requestedImageId = -1; //ID of the requested image. -1 => clear the screen
bool m_imageDrawn = false; //when true: the new image has been drawn
int m_drawnImageId = -1; //ID of the drawn image. -1 => clear the screen
// Data corresponding to each cue image. Could be refactored to a vector of structs.
std::vector<GdkPixbuf*> m_originalPictures;
std::vector<GdkPixbuf*> m_scaledPictures;
std::vector<uint64_t> m_stimulationsIds;
std::vector<CString> m_imageNames;
GdkColor m_backgroundColor = InitGDKColor(0, 0, 0, 0);
GdkColor m_foregroundColor = InitGDKColor(0, 65535, 65535, 65535);
//Settings
size_t m_numberOfCues = 0;
uint64_t m_clearScreenStimulation = 0;
bool m_fullScreen = false;
bool m_scaleImages = false;
//Start and end time of the last buffer
uint64_t m_startTime = 0;
uint64_t m_endTime = 0;
uint64_t m_lastOutputChunkDate = 0;
//We save the received stimulations
CStimulationSet m_pendingStimulationSet;
// For queuing stimulations to the TCP Tagging
std::vector<uint64_t> m_stimuliQueue;
guint m_idleFuncTag = 0;
TCPTagging::IStimulusSender* m_stimulusSender = nullptr;
private:
VisualizationToolkit::IVisualizationContext* m_visualizationCtx = nullptr;
};
class CDisplayCueImageListener final : public Toolkit::TBoxListener<IBoxListener>
{
public:
bool onSettingAdded(Kernel::IBox& box, const size_t index) override
{
const size_t previousCues = ((index - 1) - CDisplayCueImage::NON_CUE_SETTINGS_COUNT) / 2 + 1;
const size_t cueNumber = previousCues + 1;
std::stringstream ss;
ss << std::setfill('0') << std::setw(2) << cueNumber;
std::string value = "${Path_Data}/plugins/simple-visualization/p300-magic-card/" + ss.str() + ".png";
box.setSettingDefaultValue(index, value.c_str());
box.setSettingValue(index, value.c_str());
value = "OVTK_StimulationId_Label_" + ss.str();
box.addSetting("", OV_TypeId_Stimulation, value.c_str());
box.setSettingDefaultValue(index + 1, value.c_str());
box.setSettingValue(index + 1, value.c_str());
checkSettingNames(box);
return true;
}
bool onSettingRemoved(Kernel::IBox& box, const size_t index) override
{
// Remove also the associated setting in the other slot
const size_t indexNumber = (index - CDisplayCueImage::NON_CUE_SETTINGS_COUNT);
if (indexNumber % 2 == 0)
{
// This was the 'cue image' setting, remove 'stimulation setting'
// when onSettingRemoved is called, index has already been removed, so using it will effectively mean 'remove next setting'.
box.removeSetting(index);
}
else
{
// This was the 'stimulation setting'. Remove the 'cue image' setting.
box.removeSetting(index - 1);
}
checkSettingNames(box);
return true;
}
_IsDerivedFromClass_Final_(Toolkit::TBoxListener<IBoxListener>, CIdentifier::undefined())
private:
// This function is used to make sure the setting names and types are correct
bool checkSettingNames(Kernel::IBox& box) const
{
for (size_t i = CDisplayCueImage::NON_CUE_SETTINGS_COUNT; i < box.getSettingCount() - 1; i += 2)
{
const std::string idx = std::to_string(i / 2);
box.setSettingName(i, ("Cue Image " + idx).c_str());
box.setSettingType(i, OV_TypeId_Filename);
box.setSettingName(i + 1, ("Stimulation " + idx).c_str());
box.setSettingType(i + 1, OV_TypeId_Stimulation);
}
return true;
}
};
/**
* Plugin's description
*/
class CDisplayCueImageDesc final : public IBoxAlgorithmDesc
{
public:
CString getName() const override { return CString("Display cue image"); }
CString getAuthorName() const override { return CString("Joan Fruitet, Jussi T. Lindgren"); }
CString getAuthorCompanyName() const override { return CString("Inria Sophia, Inria Rennes"); }
CString getShortDescription() const override { return CString("Display cue images when receiving stimulations"); }
CString getDetailedDescription() const override
{
return CString("Display cue images when receiving specified stimulations. Forwards the stimulations to the AS using TCP Tagging.");
}
CString getCategory() const override { return CString("Visualization/Presentation"); }
CString getVersion() const override { return CString("1.2"); }
void release() override { }
CIdentifier getCreatedClass() const override { return OVP_ClassId_DisplayCueImage; }
CString getStockItemName() const override { return CString("gtk-fullscreen"); }
IPluginObject* create() override { return new CDisplayCueImage(); }
IBoxListener* createBoxListener() const override { return new CDisplayCueImageListener; }
void releaseBoxListener(IBoxListener* listener) const override { delete listener; }
bool hasFunctionality(const EPluginFunctionality functionality) const override { return functionality == EPluginFunctionality::Visualization; }
bool getBoxPrototype(Kernel::IBoxProto& prototype) const override
{
prototype.addInput("Stimulations", OV_TypeId_Stimulations);
prototype.addOutput("Stimulations (deprecated)", OV_TypeId_Stimulations);
prototype.addSetting("Display images in full screen", OV_TypeId_Boolean, "false");
prototype.addSetting("Scale images to fit", OV_TypeId_Boolean, "false");
prototype.addSetting("Clear screen Stimulation", OV_TypeId_Stimulation, "OVTK_StimulationId_VisualStimulationStop");
prototype.addSetting("Cue Image 1", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/p300-magic-card/01.png");
prototype.addSetting("Stimulation 1", OV_TypeId_Stimulation, "OVTK_StimulationId_Label_01");
prototype.addFlag(Kernel::BoxFlag_CanAddSetting);
return true;
}
_IsDerivedFromClass_Final_(IBoxAlgorithmDesc, OVP_ClassId_DisplayCueImageDesc)
};
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,710 @@
#include "ovpCGrazMultiVisualization.h"
#include "../utils.h"
#include <algorithm> // std::min, max
#include <array>
#include <cmath>
#include <cstdlib>
#include <functional> // greater
#include <iomanip>
#include <sys/timeb.h>
#include <tcptagging/IStimulusSender.h>
#include <vector>
#if defined TARGET_OS_Linux
#include <unistd.h>
#endif
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
//******************
//***** STATIC *****
//******************
//---------------------------------------------------------------------------------------------------
static gboolean FlushCB(gpointer data)
{
reinterpret_cast<CGrazMultiVisualization*>(data)->flushQueue();
return FALSE; // Only run once
}
//---------------------------------------------------------------------------------------------------
// \remarks spend a lot of time with one problem DON'T USE LOG MANAGER IN FONCTION CALLED BY CALLBACK. Why ? Because OpenViBE
//---------------------------------------------------------------------------------------------------
static gboolean ResizeCB(GtkWidget* /*widget*/, GtkAllocation* allocation, gpointer data)
{
reinterpret_cast<CGrazMultiVisualization*>(data)->resize(size_t(allocation->width), size_t(allocation->height));
return FALSE;
}
//---------------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------
static gboolean RedrawCB(GtkWidget* /*widget*/, GdkEventExpose* /*event*/, gpointer data)
{
reinterpret_cast<CGrazMultiVisualization*>(data)->redraw();
return TRUE;
}
//---------------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------
/// <summary> Run scale command with the good size if you want to keep the proportion of the original pixbuf.</summary>
/// <param name="in"> The pixbuf.</param>
/// <param name="newW"> The new width.</param>
/// <param name="newH"> The new height.</param>
/// <param name="keepRatio"> True if you wan't to keep ratio of pixbuf.</param>
/// <param name="min"> True If you select the size min when you keep ratio.</param>
/// <returns> The gdk_pixbuf_scale_simple command.</returns>
static GdkPixbuf* RescalePixbuf(const GdkPixbuf* in, const int newW, const int newH, const bool keepRatio = true, const bool min = true)
{
if (!in || newW <= 0 || newH <= 0) { return nullptr; }
if (keepRatio)
{
const double scaleW = double(newW) / double(gdk_pixbuf_get_width(in)),
scaleH = double(newH) / double(gdk_pixbuf_get_height(in)),
scale = min ? MIN(scaleW, scaleH) : MAX(scaleW, scaleH);
return gdk_pixbuf_scale_simple(in, int(scale * gdk_pixbuf_get_width(in)), int(scale * gdk_pixbuf_get_height(in)), GDK_INTERP_BILINEAR);
}
return gdk_pixbuf_scale_simple(in, newW, newH, GDK_INTERP_BILINEAR);
}
//---------------------------------------------------------------------------------------------------
//******************************
//***** OPENVIBE FUNCTIONS *****
//******************************
//---------------------------------------------------------------------------------------------------
bool CGrazMultiVisualization::initialize()
{
//***** Codecs *****
m_stimDecoder.initialize(*this, 0);
m_classifDecoder.initialize(*this, 1);
m_barSizeEncoder.initialize(*this, 0);
m_confusionEncoder.initialize(*this, 1);
m_iStim = m_stimDecoder.getOutputStimulationSet();
m_iMatrix = m_classifDecoder.getOutputMatrix();
m_oBarSize = m_barSizeEncoder.getInputMatrix();
m_oConfusion = m_confusionEncoder.getInputMatrix();
//***** Settings *****
m_showInstruction = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 0);
m_feedbackMode = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 1);
m_delayFeedback = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 2);
m_showAccuracy = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 3);
m_nbPredictionsMin = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 4);
m_nbModality = (getStaticBoxContext().getSettingCount() - m_NonModalitySettingsCount) / 2;
m_barScales.resize(m_nbModality);
for (auto& s : m_barScales) { s = 0; }
std::vector<std::string> paths;
paths.reserve(m_nbModality + 1);
m_stimlist.reserve(m_nbModality);
size_t idx = m_NonModalitySettingsCount - 1; //the last non modality setting is for the none instruction feedback image
paths.emplace_back(CString(FSettingValueAutoCast(*this->getBoxAlgorithmContext(), idx++)).toASCIIString());
//Stimulation ID and images file names for each modality
for (size_t i = 0; i < m_nbModality; ++i)
{
m_stimlist.emplace_back(uint64_t(FSettingValueAutoCast(*this->getBoxAlgorithmContext(), idx++)));
paths.emplace_back(CString(FSettingValueAutoCast(*this->getBoxAlgorithmContext(), idx++)).toASCIIString());
}
m_oConfusion->resize(m_nbModality, m_nbModality); // Confusion Matrix
m_oBarSize->resize(m_nbModality); // Bar Size Matrix
//this->getLogManager() << Kernel::LogLevel_Warning << infos();
//this->getLogManager() << Kernel::LogLevel_Warning << "Paths : \n";
//for (const auto& p : paths) { this->getLogManager() << Kernel::LogLevel_Warning << p << "\n"; }
OV_ERROR_UNLESS_KRF(m_nbPredictionsMin > 0, "Number of predictions to integrate : " << m_nbPredictionsMin << " (expected value > 1)\n",
Kernel::ErrorType::BadSetting);
OV_ERROR_UNLESS_KRF(initImages(paths), "Error: couldn't load resource files!\n", Kernel::ErrorType::BadProcessing);
paths.clear();
OV_ERROR_UNLESS_KRF(initWindow(), "Error: couldn't load the interface!\n", Kernel::ErrorType::BadProcessing);
//***** TCP Tagging *****
m_stimulusSender = nullptr;
m_idleFuncTag = 0;
m_stimuliQueue.clear();
m_stimulusSender = TCPTagging::CreateStimulusSender();
if (!m_stimulusSender->connect("localhost", "15361"))
{
this->getLogManager() << Kernel::LogLevel_Warning << "Unable to connect to AS's TCP Tagging plugin, stimuli wont be forwarded.\n";
}
//this->getLogManager() << Kernel::LogLevel_Warning << infos() << "\n";
return true;
}
//---------------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------
bool CGrazMultiVisualization::uninitialize()
{
//***** Print Confusion Matrix *****
this->getLogManager() << Kernel::LogLevel_Info << "Confusion Matrix : \n";
const double* buffer = m_oConfusion->getBuffer();
std::stringstream ss;
ss << std::setfill('0');
size_t idx = 0;
size_t predictions = 0, good = 0;
for (size_t i = 0; i < m_nbModality; ++i)
{
for (size_t j = 0; j < m_nbModality; ++j)
{
ss.str(std::string());
const int val = int(buffer[idx++]);
ss << std::setw(3) << val;
predictions += val;
this->getLogManager() << ss.str() << " ";
if (i == j) { good += val; }
}
this->getLogManager() << "\n";
}
ss.str(std::string());
ss << "Accuracy = " << std::fixed << std::setprecision(1) << (predictions == 0 ? 0.0 : 100.0 * double(good) / double(predictions)) << "\n";
this->getLogManager() << ss.str();
//***** Codecs *****
m_iStim = nullptr;
m_iMatrix = nullptr;
m_oBarSize = nullptr;
m_oConfusion = nullptr;
m_stimDecoder.uninitialize();
m_classifDecoder.uninitialize();
m_barSizeEncoder.uninitialize();
m_confusionEncoder.uninitialize();
//***** Images *****
if (m_originalBar) { g_object_unref(G_OBJECT(m_originalBar)); }
if (m_bar) { g_object_unref(G_OBJECT(m_bar)); }
for (size_t i = 0; i < m_originalImgs.size(); ++i)
{
if (m_originalImgs[i]) { g_object_unref(G_OBJECT(m_originalImgs[i])); }
if (m_smallImgs[i]) { g_object_unref(G_OBJECT(m_smallImgs[i])); }
if (m_largeImgs[i]) { g_object_unref(G_OBJECT(m_largeImgs[i])); }
}
m_originalImgs.clear();
m_smallImgs.clear();
m_largeImgs.clear();
//***** Window *****
if (m_visualizationCtx)
{
this->releasePluginObject(m_visualizationCtx);
m_visualizationCtx = nullptr;
}
if (m_widget)
{
gtk_widget_destroy(m_widget);
m_widget = nullptr;
}
//***** TCP Tagging *****
if (m_idleFuncTag)
{
m_stimuliQueue.clear();
g_source_remove(m_idleFuncTag);
m_idleFuncTag = 0;
}
m_stimlist.clear();
delete m_stimulusSender;
m_amplitudes.clear();
return true;
}
//---------------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------
bool CGrazMultiVisualization::processInput(const size_t /*index*/)
{
getBoxAlgorithmContext()->markAlgorithmAsReadyToProcess();
return true;
}
//---------------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------
bool CGrazMultiVisualization::process()
{
Kernel::IBoxIO& boxContext = this->getDynamicBoxContext();
//**** Stimulations *****
for (size_t i = 0; i < boxContext.getInputChunkCount(0); ++i)
{
m_needRedraw = true;
m_stimDecoder.decode(i);
if (m_stimDecoder.isBufferReceived())
{
for (size_t s = 0; s < m_iStim->getStimulationCount(); ++s) { setStimulation(m_iStim->getStimulationIdentifier(s)); }
}
}
for (size_t i = 0; i < boxContext.getInputChunkCount(1); ++i)
{
const uint64_t start = boxContext.getInputChunkStartTime(1, i), // Time Code Chunk Start
end = boxContext.getInputChunkEndTime(1, i); // Time Code Chunk End
m_needRedraw = true;
m_classifDecoder.decode(i);
if (m_classifDecoder.isHeaderReceived())
{
if (m_iMatrix->getBufferElementCount() != m_nbModality)
{
this->getLogManager() << Kernel::LogLevel_Error << "Error, the vector/matrix do not contain the same number of values as modalities! ("
<< m_iMatrix->getBufferElementCount() << " VS " << m_nbModality << ")\n";
return false;
}
m_confusionEncoder.encodeHeader();
m_barSizeEncoder.encodeHeader();
}
if (m_classifDecoder.isBufferReceived())
{
setMatrixBuffer(m_iMatrix->getBuffer());
m_confusionEncoder.encodeBuffer();
m_barSizeEncoder.encodeBuffer();
}
if (m_classifDecoder.isEndReceived())
{
m_confusionEncoder.encodeEnd();
m_barSizeEncoder.encodeEnd();
}
boxContext.markOutputAsReadyToSend(0, start, end);
boxContext.markOutputAsReadyToSend(1, start, end);
}
// After any possible rendering, we flush the accumulated stimuli. The default idle func is low priority, so it should be run after rendering by gtk.
// Only register a single idle func, if the previous is there its just as good
if (m_idleFuncTag == 0) { m_idleFuncTag = g_idle_add(FlushCB, this); }
return true;
}
//---------------------------------------------------------------------------------------------------
//*******************
//***** DRAWING *****
//*******************
//---------------------------------------------------------------------------------------------------
bool CGrazMultiVisualization::redraw()
{
switch (m_state)
{
case EStates::Idle: // Start Experiment & Idle
drawReference();
break;
case EStates::Cross: // Start Trial & Cross
drawReference();
drawCross();
break;
case EStates::Instruction: // Show Instruction
drawReference();
drawModality();
break;
case EStates::Feedback: // Feedback
drawReference();
if (m_feedbackMode != 3 && m_modality != -1 && m_vote != -1 && !m_delayFeedback) { drawBar(); }
break;
case EStates::Black: // End Trial
if (m_feedbackMode != 3 && m_modality != -1 && m_vote != -1 && m_delayFeedback)
{
drawReference();
drawBar();
}
break;
}
if (m_showAccuracy) { drawAccuracy(); }
return true;
}
//---------------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------
void CGrazMultiVisualization::drawReference()
{
gint currX = m_modalityX;
// Draw for each Horizontal line, Vertical Line & modality
for (size_t i = 1; i < m_smallImgs.size(); ++i, currX += m_modalityX)
{
gdk_draw_line(m_widget->window, m_widget->style->fg_gc[gtk_widget_get_state(m_widget)], currX - m_modalityW, m_modalityY, currX + m_modalityW,
m_modalityY);
gdk_draw_line(m_widget->window, m_widget->style->fg_gc[gtk_widget_get_state(m_widget)], currX, m_margin, currX, m_modalityY);
const gint x = currX - gdk_pixbuf_get_width(m_smallImgs[i]) / 2,
y = m_modalityY + (m_windowH - m_modalityY - gdk_pixbuf_get_height(m_smallImgs[i])) / 2;
gdk_draw_pixbuf(m_widget->window, nullptr, m_smallImgs[i], 0, 0, x, y, -1, -1, GDK_RGB_DITHER_NONE, 0, 0);
}
}
//---------------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------
void CGrazMultiVisualization::drawCross() const
{
const gint xc = m_windowW / 2, yc = m_windowH / 2, // Window Center
size = std::min(xc, yc) / 4, // Cross Size
xm = xc - size, ym = yc - size, // min(x,y)
xM = xc + size, yM = yc + size; // Max(x,y)
gdk_draw_line(m_widget->window, m_widget->style->fg_gc[gtk_widget_get_state(m_widget)], xm, yc, xM, yc);
gdk_draw_line(m_widget->window, m_widget->style->fg_gc[gtk_widget_get_state(m_widget)], xm, yc + 1, xM, yc + 1);
gdk_draw_line(m_widget->window, m_widget->style->fg_gc[gtk_widget_get_state(m_widget)], xc, ym, xc, yM);
gdk_draw_line(m_widget->window, m_widget->style->fg_gc[gtk_widget_get_state(m_widget)], xc + 1, ym, xc + 1, yM);
}
//---------------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------
void CGrazMultiVisualization::drawModality()
{
const GdkPixbuf* modality = m_largeImgs[(m_showInstruction ? m_modality + 1 : 0)]; // (m_modality + 1) Because the first element is the none instruction
const gint w = m_windowW / 2,
h = m_windowH / 2,
x = w - gdk_pixbuf_get_width(modality) / 2,
y = h - gdk_pixbuf_get_height(modality) / 2;
gdk_draw_pixbuf(m_widget->window, nullptr, modality, 0, 0, x, y, -1, -1, GDK_RGB_DITHER_NONE, 0, 0);
}
//---------------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------
void CGrazMultiVisualization::drawBar()
{
if ((m_feedbackMode == 0 && m_modality == m_vote) || m_feedbackMode == 1) // Draw Positive or Best Only
{
const double scale = m_barScales[m_vote] < 0.0 ? 0.0 : m_barScales[m_vote];
const gint x = (m_vote + 1) * m_modalityX, // Get the pos X of the bar
h = gint(scale * m_barH); // Get the height of the bar
gdk_pixbuf_render_to_drawable(m_bar, m_widget->window, nullptr, 0, m_barH - h, x - m_barW / 2, m_modalityY - h, m_barW, h, GDK_RGB_DITHER_NONE, 0, 0);
m_oBarSize->getBuffer()[m_vote] = 100.0 * scale; // Update the displayed bar size
}
else if (m_feedbackMode == 2) // Draw All
{
gint x = m_modalityX;
for (size_t i = 0; i < m_nbModality; ++i) // For each Modality
{
const double scale = m_barScales[i] < 0.0 ? 0.0 : m_barScales[i];
const gint h = gint(scale * m_barH); // Get the H of the modality
gdk_pixbuf_render_to_drawable(m_bar, m_widget->window, nullptr, 0, m_barH - h, x - m_barW / 2, m_modalityY - h, m_barW, h, GDK_RGB_DITHER_NONE, 0,
0);
x += m_modalityX;
m_oBarSize->getBuffer()[i] = 100.0 * scale; // Update the displayed bar size
}
}
}
//---------------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------
void CGrazMultiVisualization::drawAccuracy()
{
const double* buffer = m_oConfusion->getBuffer();
PangoLayout* layout = pango_layout_new(gdk_pango_context_get());
const gint stepX = 40, stepY = 16;
const gint startX = 8, startY = 16;
gint x = startX, y = startY;
std::stringstream ss;
ss << std::setfill('0');
size_t idx = 0;
size_t predictions = 0, good = 0;
for (size_t i = 0; i < m_nbModality; ++i)
{
for (size_t j = 0; j < m_nbModality; ++j)
{
ss.str(std::string());
const int val = int(buffer[idx++]);
ss << std::setw(3) << val;
predictions += val;
pango_layout_set_text(layout, ss.str().c_str(), -1);
if (i == j)
{
gdk_draw_layout(m_widget->window, m_widget->style->white_gc, x, y, layout);
good += val;
}
else { gdk_draw_layout(m_widget->window, m_widget->style->fg_gc[GTK_WIDGET_STATE(m_widget)], x, y, layout); }
x += stepX;
}
x = startX;
y += stepY;
}
ss.str(std::string());
ss << "Acc = " << std::fixed << std::setprecision(1) << (predictions == 0 ? 0.0 : 100.0 * double(good) / double(predictions));
pango_layout_set_text(layout, ss.str().c_str(), -1);
gdk_draw_layout(m_widget->window, m_widget->style->white_gc, x + (gint(m_nbModality) * stepX), y - stepY, layout);
g_object_unref(layout);
}
//---------------------------------------------------------------------------------------------------
//********************
//***** COMPUTES *****
//********************
//---------------------------------------------------------------------------------------------------
void CGrazMultiVisualization::aggregatePredictions(const bool all)
{
if (m_amplitudes.size() >= m_nbPredictionsMin)
{
m_vote = 0;
// step backwards with rev iter to take the mean of the latest samples
uint64_t count = 0;
double sum = 0;
std::vector<double> amp(m_nbModality, 0);
//double maxA = -DBL_MAX;
for (auto a = m_amplitudes.rbegin(); a != m_amplitudes.rend() && (all || count < m_nbPredictionsMin); ++a, ++count)
{
for (size_t i = 0; i < a->size(); ++i) { amp[i] += a->at(i); }
}
for (const auto& a : amp) { sum += a; }
// Computes bar scale for each modality as a probability (other possibility substract the min/max value or the mean of the values)
// Then we put this probability between 0-1 to 0-1 but when 0 is equal to 1/m_nbModality
const double minV = 1.0 / double(m_nbModality),
factorV = 1.0 / (1 - minV);
for (size_t i = 0; i < amp.size(); ++i)
{
m_barScales[i] = (amp[i] / sum - minV) * factorV;
if (amp[m_vote] < amp[i]) { m_vote = int(i); }
}
}
}
//---------------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------
void CGrazMultiVisualization::updateConfusionMatrix()
{
// Col = Expected modality, Row = Computed modality
if (m_modality != -1 && m_vote != -1) { (m_oConfusion->getBuffer())[m_vote * m_nbModality + m_modality]++; }
m_oBarSize->resetBuffer();
}
//---------------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------
void CGrazMultiVisualization::setMatrixBuffer(const double* buffer)
{
if (m_state != EStates::Feedback) { return; } // No continuous feedback
// Ad-hoc forcing to probability (range [0,1], sum to 1). This will make scaling easier
// if run forever in a continuous mode. If the input is already scaled this way, no effect.
double sum = 0;
std::vector<double> values;
values.reserve(m_nbModality);
for (size_t i = 0; i < m_nbModality; ++i)
{
const double v = std::abs(buffer[i]);
values.emplace_back(v);
sum += v;
}
if (sum != 0.0) { for (auto& v : values) { v /= sum; } }
else { for (auto& v : values) { v = 1.0 / double(m_nbModality); } }
m_amplitudes.emplace_back(values); // Add this buffer to the list
if (m_feedbackMode != 3 && !m_delayFeedback)
{
aggregatePredictions(false);
gdk_window_invalidate_rect(m_widget->window, nullptr, true);
}
}
//---------------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------
void CGrazMultiVisualization::setStimulation(const uint64_t stimulation)
{
bool needRedraw = true;
switch (stimulation)
{
// Classical stimulations
case OVTK_GDF_End_Of_Session:
m_state = EStates::Black; // Idle State Black Screen
break;
case OVTK_GDF_End_Of_Trial:
m_state = EStates::Black; // Idle State Black Screen
aggregatePredictions(true);
updateConfusionMatrix();
break;
case OVTK_GDF_Start_Of_Trial:
case OVTK_GDF_Cross_On_Screen:
m_state = EStates::Cross; // Draw Reference State and cross
break;
case OVTK_GDF_Feedback_Continuous:
m_state = EStates::Feedback; // Draw Feedback State
break;
default: // Modalities stimulations
m_modality = -1; // Initialize modality number
m_vote = -1; // Initialize vote
m_amplitudes.clear(); // Clear the previous amplitudes
for (auto& s : m_barScales) { s = 0; } // Reinit Bar Scales
for (size_t i = 0; i < m_stimlist.size(); ++i)
{
if (m_stimlist[i] == stimulation)
{
m_modality = int(i);
break; // stop the loop (but not so usefull big number of modality is not so big generally)
}
}
if (m_modality != -1) { m_state = EStates::Instruction; } // If recognize stimulation Draw Modality State
else { needRedraw = false; } // If not, we don't care about stimulations and we don't want to redraw
break;
}
// Queue the stimulation to be sent to TCP Tagging
m_stimuliQueue.push_back(stimulation);
// Indicates that the window is invalidate (must be redrawn)
if (needRedraw && GTK_WIDGET(m_widget)->window) { gdk_window_invalidate_rect(GTK_WIDGET(m_widget)->window, nullptr, true); }
}
//---------------------------------------------------------------------------------------------------
//**************************
//***** CALLBACKS HACK *****
//**************************
//---------------------------------------------------------------------------------------------------
// Note that we don't need concurrency control here as gtk callbacks run in the main thread
void CGrazMultiVisualization::flushQueue()
{
for (auto i : m_stimuliQueue) { m_stimulusSender->sendStimulation(i); }
m_stimuliQueue.clear();
m_idleFuncTag = 0; // This function will be automatically removed after completion, so set to 0
}
//---------------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------
void CGrazMultiVisualization::resize(size_t width, size_t height)
{
//***** Variables update *****
m_windowW = gint(width < 8 ? 8 : width); // Windows Width minimum to avoïd 0 sizes
m_windowH = gint(height < 8 ? 8 : height); // Windows Height minimum to avoïd 0 sizes
m_margin = gint(0.01 * double(MIN(width, height))); // Margin 1% of the minimum between width and height
const gint drawAreaW = gint(width - 2 * m_margin), // Drawing area Width (without margin)
drawAreaH = gint(height - 2 * m_margin); // Drawing area Height (without margin)
m_barH = gint(0.80 * drawAreaH); // Height of the Graz Bar
m_barW = MIN(m_barH / 6, (drawAreaW / gint(m_nbModality)) - m_margin); // Keep some proportion
m_modalityX = drawAreaW / gint(m_nbModality + 1); // Center X Position of the first modality (next is in x+(x+margin))
m_modalityY = m_barH + m_margin; // Bottom Y Position of the modalities
m_modalityW = (m_barW + 2 * m_margin) / 2; // Half width dedicated for the modalities
const int wL = m_windowW / 3, hL = m_windowH / 3, wS = m_barW;
//***** Images update *****
for (size_t i = 0; i < m_originalImgs.size(); ++i)
{
if (m_smallImgs[i]) { g_object_unref(G_OBJECT(m_smallImgs[i])); } // Delete
if (m_largeImgs[i]) { g_object_unref(G_OBJECT(m_largeImgs[i])); } // Delete
m_smallImgs[i] = RescalePixbuf(m_originalImgs[i], wS, wS); // Rescale
m_largeImgs[i] = RescalePixbuf(m_originalImgs[i], wL, hL); // Rescale
}
if (m_originalBar)
{
if (m_bar) { g_object_unref(G_OBJECT(m_bar)); } // Delete
m_bar = RescalePixbuf(m_originalBar, m_barW, m_barH, false);
}
}
//---------------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------
bool CGrazMultiVisualization::initWindow()
{
m_widget = GTK_WIDGET(gtk_drawing_area_new()); // Creation
gtk_widget_set_size_request(m_widget, 400, 300); // Minimum Window Size
gtk_widget_set_double_buffered(m_widget, TRUE); // Double Buffer
//set widget basic color
GdkColor bgColor = InitGDKColor(0, 0, 0, 0), // = { 0, 0, 0, 0 }; // pixel, red, green, blue (black) vs 2013 doesn't allow this initialization
fgColor = InitGDKColor(0, 0, 32768, 0); // = { 0, 0, 32768, 0 }; // pixel, red, green, blue (dark green)
gtk_widget_modify_bg(m_widget, GTK_STATE_NORMAL, &bgColor);
gtk_widget_modify_bg(m_widget, GTK_STATE_PRELIGHT, &bgColor);
gtk_widget_modify_bg(m_widget, GTK_STATE_ACTIVE, &bgColor);
gtk_widget_modify_fg(m_widget, GTK_STATE_NORMAL, &fgColor);
gtk_widget_modify_fg(m_widget, GTK_STATE_PRELIGHT, &fgColor);
gtk_widget_modify_fg(m_widget, GTK_STATE_ACTIVE, &fgColor);
m_visualizationCtx = dynamic_cast<VisualizationToolkit::IVisualizationContext*>(this->createPluginObject(OVP_ClassId_Plugin_VisualizationCtx));
m_visualizationCtx->setWidget(*this, m_widget);
g_signal_connect(G_OBJECT(m_widget), "size-allocate", G_CALLBACK(ResizeCB), this);
g_signal_connect(G_OBJECT(m_widget), "expose_event", G_CALLBACK(RedrawCB), this);
return true;
}
//---------------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------
bool CGrazMultiVisualization::initImages(const std::vector<std::string>& paths)
{
const size_t s = paths.size();
if (s != m_nbModality + 1) { return false; }
m_originalImgs.resize(s);
m_largeImgs.resize(s);
m_smallImgs.resize(s);
// Bar
m_originalBar = gdk_pixbuf_new_from_file_at_size(Directories::getDataDir() + "/plugins/simple-visualization/graz/bar.png", -1, -1, nullptr);
if (!m_originalBar) { return false; }
// Modalities
for (size_t i = 0; i < s; ++i)
{
m_originalImgs[i] = gdk_pixbuf_new_from_file_at_size(paths[i].c_str(), -1, -1, nullptr);
if (!m_originalImgs[i]) { return false; }
}
resize(400, 300); // Initialization
return true;
}
//---------------------------------------------------------------------------------------------------
//****************
//***** MISC *****
//****************
//---------------------------------------------------------------------------------------------------
std::string CGrazMultiVisualization::infos() const
{
std::stringstream ss;
ss << "\n";
ss << "Show instruction : " << (m_showInstruction ? "yes" : "no") << ", Feedback Mode : ";
if (m_feedbackMode == 0) { ss << "Positive Only"; }
else if (m_feedbackMode == 1) { ss << "Best Only"; }
else if (m_feedbackMode == 2) { ss << "All"; }
else if (m_feedbackMode == 3) { ss << "None"; }
ss << ", Delay : " << (m_delayFeedback ? "yes" : "no") << "\n";
ss << "Show Accuracy : " << (m_showAccuracy ? "yes" : "no") << ", Nb Predictions : " << m_nbPredictionsMin << ", Nb Modality : " << m_nbModality << "\n";
ss << "Number of Images : " << m_originalImgs.size() << ", " << m_smallImgs.size() << ", " << m_largeImgs.size() << "\n";
ss << "\tWindow W : " << m_windowW << "\tWindow H : " << m_windowH << "\tMargin : " << m_margin << "\tBar W : " << m_barW << "\tBar H : " << m_barH << "\n";
if (m_originalBar) { ss << "\tO Bar W : " << gdk_pixbuf_get_width(m_originalBar) << "\tO Bar H : " << gdk_pixbuf_get_height(m_originalBar); }
else { ss << "\tNo O Bar"; }
if (m_bar) { ss << "\tR Bar W : " << gdk_pixbuf_get_width(m_bar) << "\tR Bar H : " << gdk_pixbuf_get_height(m_bar); }
else { ss << "\tNo R Bar"; }
ss << "\n";
for (size_t i = 0; i < m_originalImgs.size(); ++i)
{
//*
if (m_originalImgs[i])
{
ss << "\tO Img " << i << " W : " << gdk_pixbuf_get_width(m_originalImgs[i]) << "\tO Img " << i << " H : " << gdk_pixbuf_get_height(
m_originalImgs[i]);
}
else { ss << "\tNo O Img " << i; }
if (m_smallImgs[i])
{
ss << "\tS Img " << i << " W : " << gdk_pixbuf_get_width(m_smallImgs[i]) << "\tS Img " << i << " H : " << gdk_pixbuf_get_height(m_smallImgs[i]);
}
else { ss << "\tNo S Img " << i; }
if (m_largeImgs[i])
{
ss << "\tL Img " << i << " W : " << gdk_pixbuf_get_width(m_largeImgs[i]) << "\tL Img " << i << " H : " << gdk_pixbuf_get_height(m_largeImgs[i]);
}
else { ss << "\tNo L Img " << i; }
ss << "\n";
}
ss << "Stimulation List : ";
for (const auto& stim : m_stimlist) { ss << stim << " "; }
ss << "\n";
return ss.str();
}
//---------------------------------------------------------------------------------------------------
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,286 @@
///-------------------------------------------------------------------------------------------------
///
/// \file ovpCGrazMultiVisualization.h
/// \brief Class of the Generalized Graz Visualization box.
/// \author Thibaut Monseigne (Inria).
/// \version 1.0.
/// \date 28/05/2019.
/// \copyright <a href="https://choosealicense.com/licenses/agpl-3.0/">GNU Affero General Public License v3.0</a>.
///
///-------------------------------------------------------------------------------------------------
#pragma once
#include "../ovp_defines.h"
#include <openvibe/ov_all.h>
#include <toolkit/ovtk_all.h>
#include <visualization-toolkit/ovviz_all.h>
#include <gtk/gtk.h>
#include <string>
#include <vector>
namespace TCPTagging {
class IStimulusSender; // fwd declare
} // namespace TCPTagging
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
/// <summary> The class CGrazMultiVisualization describes the box Multimodal Graz Visuallisation. </summary>
/// <seealso cref="Toolkit::TBoxAlgorithm{IBoxAlgorithm}" />
class CGrazMultiVisualization final : virtual public Toolkit::TBoxAlgorithm<IBoxAlgorithm>
{
public:
void release() override { delete this; }
bool initialize() override;
bool uninitialize() override;
bool processInput(const size_t index) override;
bool process() override;
bool redraw();
void resize(size_t width, size_t height);
void flushQueue(); // Sends all accumulated stimuli to the TCP Tagging
static const size_t m_NonModalitySettingsCount = 6;
protected:
/// <summary> Enumeration of States. </summary>
enum class EStates { Idle, Cross, Black, Instruction, Feedback };
/// <summary> Initialize GTK Window. </summary>
/// <returns> True if it succeeds, false if it fails. </returns>
bool initWindow();
/// <summary> Initialize GTK Images. </summary>
/// <param name="paths"> The pathsof the image file.</param>
/// <returns> True if it succeeds, false if it fails. </returns>
bool initImages(const std::vector<std::string>& paths);
/// <summary> Update the box state according to the stimulation received. </summary>
/// <param name="stimulation">The stimulation received.</param>
void setStimulation(uint64_t stimulation);
/// <summary> Update the amplitudesaccording to the new amplitude received in buffer. </summary>
/// <param name="buffer"> The buffer with amplitudes.</param>
void setMatrixBuffer(const double* buffer);
/// <summary> Draw the reference.</summary>
///
/// ┌──────────────────────────────┐ \n
/// │ │ │ │ │ │ \n
/// │ │ │ │ │ │ \n
/// │ │ │ │ │ │ \n
/// │ │ │ │ │ │ \n
/// │ ─┴─ ─┴─ ─┴─ ─┴─ │ \n
/// │ Im01 Im02 Im03 Im04 │ \n
/// └──────────────────────────────┘
void drawReference();
/// <summary> Draw Cross on Screen.</summary>
void drawCross() const;
/// <summary> Draw the actual modality in center.</summary>
void drawModality();
/// <summary> Draw the Feedback bar.</summary>
///
/// ┌──────────────────────────────┐ \n
/// │ │ │ │ │ │ \n
/// │ │ │ │ │ │ \n
/// │ │ │ ┌┴┐ │ │ \n
/// │ │ │ │ │ │ │ \n
/// │ ─┴─ ─┴─ ┴─┴ ─┴─ │ \n
/// │ Im01 Im02 Im03 Im04 │ \n
/// └──────────────────────────────┘
void drawBar();
/// <summary> Draw the accuracy on top left.</summary>
void drawAccuracy();
/// <summary> Update the confusion matrix draw in the corner.</summary>
void updateConfusionMatrix();
/// <summary> Get the mean of last predictions.</summary>
/// <param name="all"> All previous predictions is used if true, only <see cref="m_nbPredictionsMin" /> if false. </param>
void aggregatePredictions(bool all);
/// <summary> Return some infos about the class.</summary>
/// <returns> A string with infos.</returns>
std::string infos() const;
_IsDerivedFromClass_Final_(IBoxAlgorithm, OVP_ClassId_GeneralizedGrazVisualization)
//********** Variables **********
//***** Codecs *****
Toolkit::TStimulationDecoder<CGrazMultiVisualization> m_stimDecoder;
Toolkit::TStreamedMatrixDecoder<CGrazMultiVisualization> m_classifDecoder;
Toolkit::TStreamedMatrixEncoder<CGrazMultiVisualization> m_barSizeEncoder;
Toolkit::TStreamedMatrixEncoder<CGrazMultiVisualization> m_confusionEncoder;
IStimulationSet* m_iStim = nullptr; // Input StimulationSet Pointer
CMatrix* m_iMatrix = nullptr; // Input Matrix pointer
CMatrix* m_oBarSize = nullptr; // Outpout bar size matrix pointer (in percent)
CMatrix* m_oConfusion = nullptr; // Outpout confusion Matrix pointer
//***** Interface *****
GtkWidget* m_widget = nullptr;
VisualizationToolkit::IVisualizationContext* m_visualizationCtx = nullptr;
std::vector<GdkPixbuf*> m_originalImgs, m_largeImgs,
m_smallImgs; // Vector of all Images in 3 form (original, small under bar and large in center) first is none instruction
GdkPixbuf *m_originalBar = nullptr, *m_bar = nullptr; // Image of the bar
gint m_windowW = 0, m_windowH = 0, m_margin = 0, // Windows Height/Width and margin between window limit and drawing area
m_barW = 0, m_barH = 0, // Height/Width of the bar
m_modalityX = 0, m_modalityY = 0, // Center Bottom Position (x, y) of the first modality (next is in x+(x+margin)
m_modalityW = 0; // Half Width of the modality : (m_barW + 2 * m_margin) / 2
std::vector<double> m_barScales; // 0 to 1 for each bar (depends of the feedback if only positive feedback or not)
double m_barScale = 0.0; // 0 to 1
bool m_needRedraw = false; // if we need redraw
EStates m_state = EStates::Idle; // Actual state
int m_modality = -1; // Actual Modality
int m_vote = -1; // Computed Modality
//***** Settings *****
bool m_showInstruction = true,
m_delayFeedback = false,
m_showAccuracy = false;
size_t m_feedbackMode = 0,
m_nbPredictionsMin = 5,
m_nbModality = 2,
m_currModality = 0;
std::vector<uint64_t>
m_stimlist; // List of stimulations (OVTK_GDF_End_Of_Trial, OVTK_GDF_End_Of_Session, OVTK_GDF_Cross_On_Screen, OVTK_GDF_Feedback_Continuous)
//***** TCP Tagging *****
std::vector<uint64_t> m_stimuliQueue;
guint m_idleFuncTag = 0;
TCPTagging::IStimulusSender* m_stimulusSender = nullptr;
//***** Other *****
std::vector<std::vector<double>> m_amplitudes; // All amplitudes the current trial
bool m_twoValueInput = false;
};
/// <summary> Listener of the box Multimodal Graz Visuallisation. </summary>
/// <seealso cref="Toolkit::TBoxListener{IBoxListener}" />
class CGrazMultiVisualizationListener final : public Toolkit::TBoxListener<IBoxListener>
{
public:
/// <summary> Action when we add a setting (we copy the previous setting value. </summary>
/// <param name="box"> The box to listen.</param>
/// <param name="idx"> The idx of the new setting.</param>
/// <returns> True if it succeeds, false if it fails. </returns>
bool onSettingAdded(Kernel::IBox& box, const size_t idx) override
{
CString value;
box.getSettingDefaultValue(idx - 2, value);
box.setSettingDefaultValue(idx, value);
box.setSettingValue(idx, value);
box.getSettingDefaultValue(idx - 1, value);
box.addSetting("", OV_TypeId_Filename, value);
box.setSettingDefaultValue(idx + 1, value);
box.setSettingValue(idx + 1, value);
checkSettingNames(box);
return true;
}
/// <summary> Action when when remove a setting (we remove the associated setting. </summary>
/// <param name="box"> The box to listen.</param>
/// <param name="idx"> The idx of the deleted setting.</param>
/// <returns> True if it succeeds, false if it fails. </returns>
bool onSettingRemoved(Kernel::IBox& box, const size_t idx) override
{
// Remove also the associated setting in the other slot
const size_t modalityNumber = (idx - CGrazMultiVisualization::m_NonModalitySettingsCount);
if (modalityNumber % 2 == 0) { box.removeSetting(idx); }
else { box.removeSetting(idx - 1); }
checkSettingNames(box);
return true;
}
protected:
/// <summary> This function is used to make sure the setting names and types are correct (if you remove one middle setting).</summary>
/// <param name="box"> The box to listen.</param>
/// <returns> True if it succeeds, false if it fails. </returns>
bool checkSettingNames(Kernel::IBox& box) const
{
size_t idx = 1;
for (size_t i = CGrazMultiVisualization::m_NonModalitySettingsCount; i < box.getSettingCount() - 1; i += 2)
{
const std::string tmp = std::to_string(idx);
box.setSettingName(i, ("Stimulation modality " + tmp).c_str());
box.setSettingType(i, OV_TypeId_Stimulation);
box.setSettingName(i + 1, ("Image modality " + tmp).c_str());
box.setSettingType(i + 1, OV_TypeId_Filename);
idx++;
}
return true;
}
_IsDerivedFromClass_Final_(Toolkit::TBoxListener<IBoxListener>, CIdentifier::undefined())
};
/// <summary> Descriptor of the box Multimodal Graz Visuallisation. </summary>
/// <seealso cref="IBoxAlgorithmDesc" />
class CGrazMultiVisualizationDesc final : public IBoxAlgorithmDesc
{
public:
CString getName() const override { return CString("Multimodal Graz visualization"); }
CString getAuthorName() const override { return CString("Thibaut Monseigne"); }
CString getAuthorCompanyName() const override { return CString("Inria"); }
CString getShortDescription() const override { return CString("Generalization of visualization plugin for the Graz experiment"); }
CString getDetailedDescription() const override { return CString("Generalization of Visualization/Feedback plugin for the Graz experiment"); }
CString getCategory() const override { return CString("Visualization/Presentation"); }
CString getVersion() const override { return CString("1.0"); }
void release() override { }
CIdentifier getCreatedClass() const override { return OVP_ClassId_GeneralizedGrazVisualization; }
CString getStockItemName() const override { return CString("gtk-fullscreen"); }
IPluginObject* create() override { return new CGrazMultiVisualization(); }
IBoxListener* createBoxListener() const override { return new CGrazMultiVisualizationListener; }
void releaseBoxListener(IBoxListener* listener) const override { delete listener; }
bool hasFunctionality(const EPluginFunctionality identifier) const override { return identifier == EPluginFunctionality::Visualization; }
bool getBoxPrototype(Kernel::IBoxProto& prototype) const override
{
prototype.addInput("Stimulations", OV_TypeId_Stimulations);
prototype.addInput("Amplitude", OV_TypeId_StreamedMatrix);
prototype.addSetting("Show instruction", OV_TypeId_Boolean, "true");
prototype.addSetting("Feedback display mode", OVP_TypeId_FeedbackMode, "Positive Only");
prototype.addSetting("Delay feedback", OV_TypeId_Boolean, "false");
prototype.addSetting("Show accuracy", OV_TypeId_Boolean, "false");
prototype.addSetting("Predictions to integrate", OV_TypeId_Integer, "5");
prototype.addSetting("Image no instruction", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/graz/none.png");
prototype.addSetting("Stimulation modality 1", OV_TypeId_Stimulation, "OVTK_GDF_Left");
prototype.addSetting("Image modality 1", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/graz/left.png");
prototype.addSetting("Stimulation modality 2", OV_TypeId_Stimulation, "OVTK_GDF_Right");
prototype.addSetting("Image modality 2", OV_TypeId_Filename, "${Path_Data}/plugins/simple-visualization/graz/right.png");
prototype.addFlag(Kernel::BoxFlag_CanAddSetting);
prototype.addOutput("Displayed Bar Size", OV_TypeId_StreamedMatrix);
prototype.addOutput("Confusion Matrix", OV_TypeId_StreamedMatrix);
return true;
}
_IsDerivedFromClass_Final_(IBoxAlgorithmDesc, OVP_ClassId_GeneralizedGrazVisualizationDesc)
};
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,677 @@
#include "ovpCGrazVisualization.h"
#include <tcptagging/IStimulusSender.h>
#include <algorithm> // std::min, max
#include <cmath>
#include <cstdlib>
#include <iomanip>
#include <sys/timeb.h>
#if defined TARGET_OS_Linux
#include <unistd.h>
#endif
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
// This callback flushes all accumulated stimulations to the TCP Tagging
// after the rendering has completed.
static gboolean FlushCB(gpointer data)
{
reinterpret_cast<CGrazVisualization*>(data)->flushQueue();
return FALSE; // Only run once
}
static gboolean ResizeCB(GtkWidget* /*widget*/, GtkAllocation* allocation, gpointer data)
{
reinterpret_cast<CGrazVisualization*>(data)->resize(size_t(allocation->width), size_t(allocation->height));
return FALSE;
}
static gboolean RedrawCB(GtkWidget* /*widget*/, GdkEventExpose* /*event*/, gpointer data)
{
reinterpret_cast<CGrazVisualization*>(data)->redraw();
return TRUE;
}
// n.b. This reacts immediately to the received stimulation and doesn't use the date. Usually stimulations come from the upstream with
// chunks having a very narrow time range, so its alright for Graz that changes state only rarely. Note if multiple stimulations are
// received in the same chunk, they'll be passed to TCP Tagging with the true delay between them lost.
void CGrazVisualization::setStimulation(const size_t /*stimulationIndex*/, const uint64_t identifier, const uint64_t /*stimulationDate*/)
{
/*
OVTK_GDF_Start_Of_Trial
OVTK_GDF_Cross_On_Screen
OVTK_GDF_Left
OVTK_GDF_Right
*/
bool stateUpdated = false;
m_LastStimulation = identifier;
switch (identifier)
{
case OVTK_GDF_End_Of_Trial:
m_CurrentState = EStates::Idle;
stateUpdated = true;
if (m_ShowAccuracy || m_DelayFeedback)
{
const double prediction = aggregatePredictions(true);
updateConfusionMatrix(prediction);
m_BarScale = prediction;
}
break;
case OVTK_GDF_End_Of_Session:
m_CurrentState = EStates::Idle;
stateUpdated = true;
if (m_ShowFeedback)
{
m_BarScale = 0;
drawBar();
}
break;
case OVTK_GDF_Cross_On_Screen:
m_CurrentState = EStates::Reference;
stateUpdated = true;
break;
case OVTK_GDF_Beep:
// gdk_beep();
getBoxAlgorithmContext()->getPlayerContext()->getLogManager() << Kernel::LogLevel_Trace <<
"Beep is no more considered in 'Graz Visu', use the 'Sound player' for this!\n";
#if 0
#if defined TARGET_OS_Linux
system("cat /local/ov_beep.wav > /dev/dsp &");
#endif
#endif
break;
case OVTK_GDF_Left:
m_CurrentState = EStates::Cue;
m_CurrentDirection = EArrowDirections::Left;
stateUpdated = true;
break;
case OVTK_GDF_Right:
m_CurrentState = EStates::Cue;
m_CurrentDirection = EArrowDirections::Right;
stateUpdated = true;
break;
case OVTK_GDF_Up:
m_CurrentState = EStates::Cue;
m_CurrentDirection = EArrowDirections::Up;
stateUpdated = true;
break;
case OVTK_GDF_Down:
m_CurrentState = EStates::Cue;
m_CurrentDirection = EArrowDirections::Down;
stateUpdated = true;
break;
case OVTK_GDF_Feedback_Continuous:
// New trial starts
m_CurrentState = EStates::ContinousFeedback;
m_Amplitudes.clear();
// as some trials may have artifacts and hence very high responses from e.g. LDA
// its better to reset the max between trials
m_MaxAmplitude = -DBL_MAX;
m_BarScale = 0;
stateUpdated = true;
break;
default: break;
}
if (stateUpdated) { processState(); }
// Queue the stimulation to be sent to TCP Tagging
m_StimuliQueue.push_back(m_LastStimulation);
}
void CGrazVisualization::processState()
{
switch (m_CurrentState)
{
case EStates::Reference:
case EStates::Cue:
case EStates::Idle:
case EStates::ContinousFeedback:
if (GTK_WIDGET(m_DrawingArea)->window) { gdk_window_invalidate_rect(GTK_WIDGET(m_DrawingArea)->window, nullptr, true); }
break;
default:
break;
}
}
bool CGrazVisualization::initialize()
{
m_StimulationDecoder.initialize(*this, 0);
m_MatrixDecoder.initialize(*this, 1);
m_confusionEncoder.initialize(*this, 0);
m_oConfusion = m_confusionEncoder.getInputMatrix();
m_ShowInstruction = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 0);
m_ShowFeedback = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 1);
m_DelayFeedback = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 2);
m_ShowAccuracy = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 3);
m_PredictionsToIntegrate = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 4);
m_PositiveFeedbackOnly = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 5);
m_StimulusSender = nullptr;
m_IdleFuncTag = 0;
m_StimuliQueue.clear();
if (m_PredictionsToIntegrate < 1)
{
this->getLogManager() << Kernel::LogLevel_Error << "Number of predictions to integrate must be at least 1!";
return false;
}
m_oConfusion->resize(2, 2);
//load the gtk builder interface
m_Builder =
gtk_builder_new(); // glade_xml_new(Directories::getDataDir() + "/plugins/simple-visualization/openvibe-simple-visualization-GrazVisualization.ui", nullptr, nullptr);
gtk_builder_add_from_file(m_Builder,
Directories::getDataDir() + "/plugins/simple-visualization/openvibe-simple-visualization-GrazVisualization.ui", nullptr);
if (!m_Builder)
{
this->getLogManager() << Kernel::LogLevel_Error << "Error: couldn't load the interface!";
return false;
}
gtk_builder_connect_signals(m_Builder, nullptr);
m_DrawingArea = GTK_WIDGET(gtk_builder_get_object(m_Builder, "GrazVisualizationDrawingArea"));
g_signal_connect(G_OBJECT(m_DrawingArea), "expose_event", G_CALLBACK(RedrawCB), this);
g_signal_connect(G_OBJECT(m_DrawingArea), "size-allocate", G_CALLBACK(ResizeCB), this);
#if 0
//does nothing on the main window if the user tries to close it
g_signal_connect(G_OBJECT(gtk_builder_get_object(m_BuilderInterface, "GrazVisualizationWindow")), "delete_event", G_CALLBACK(gtk_widget_do_nothing), nullptr);
//creates the window
m_mainWindow = GTK_WIDGET(gtk_builder_get_object(m_BuilderInterface, "GrazVisualizationWindow"));
#endif
//set widget bg color
gtk_widget_modify_bg(m_DrawingArea, GTK_STATE_NORMAL, &m_BackgroundColor);
gtk_widget_modify_bg(m_DrawingArea, GTK_STATE_PRELIGHT, &m_BackgroundColor);
gtk_widget_modify_bg(m_DrawingArea, GTK_STATE_ACTIVE, &m_BackgroundColor);
gtk_widget_modify_fg(m_DrawingArea, GTK_STATE_NORMAL, &m_ForegroundColor);
gtk_widget_modify_fg(m_DrawingArea, GTK_STATE_PRELIGHT, &m_ForegroundColor);
gtk_widget_modify_fg(m_DrawingArea, GTK_STATE_ACTIVE, &m_ForegroundColor);
//arrows
m_OriginalLeftArrow = gdk_pixbuf_new_from_file_at_size(
Directories::getDataDir() + "/plugins/simple-visualization/openvibe-simple-visualization-GrazVisualization-leftArrow.png", -1, -1, nullptr);
m_OriginalRightArrow = gdk_pixbuf_new_from_file_at_size(
Directories::getDataDir() + "/plugins/simple-visualization/openvibe-simple-visualization-GrazVisualization-rightArrow.png", -1, -1, nullptr);
m_OriginalUpArrow = gdk_pixbuf_new_from_file_at_size(
Directories::getDataDir() + "/plugins/simple-visualization/openvibe-simple-visualization-GrazVisualization-upArrow.png", -1, -1, nullptr);
m_OriginalDownArrow = gdk_pixbuf_new_from_file_at_size(
Directories::getDataDir() + "/plugins/simple-visualization/openvibe-simple-visualization-GrazVisualization-downArrow.png", -1, -1, nullptr);
if (!m_OriginalLeftArrow || !m_OriginalRightArrow || !m_OriginalUpArrow || !m_OriginalDownArrow)
{
this->getLogManager() << Kernel::LogLevel_Error << "Error couldn't load arrow resource files!\n";
return false;
}
//bar
m_OriginalBar = gdk_pixbuf_new_from_file_at_size(
Directories::getDataDir() + "/plugins/simple-visualization/openvibe-simple-visualization-GrazVisualization-bar.png", -1, -1, nullptr);
if (!m_OriginalBar)
{
this->getLogManager() << Kernel::LogLevel_Error << "Error couldn't load bar resource file!\n";
return false;
}
#if 0
gtk_widget_show_all(m_mainWindow);
#endif
m_visualizationCtx = dynamic_cast<VisualizationToolkit::IVisualizationContext*>(this->createPluginObject(
OVP_ClassId_Plugin_VisualizationCtx));
m_visualizationCtx->setWidget(*this, m_DrawingArea);
m_StimulusSender = TCPTagging::CreateStimulusSender();
if (!m_StimulusSender->connect("localhost", "15361"))
{
this->getLogManager() << Kernel::LogLevel_Warning << "Unable to connect to AS's TCP Tagging plugin, stimuli wont be forwarded.\n";
}
return true;
}
bool CGrazVisualization::uninitialize()
{
//***** Print Confusion Matrix *****
this->getLogManager() << Kernel::LogLevel_Info << "Confusion Matrix : \n";
const double* buffer = m_oConfusion->getBuffer();
std::stringstream ss;
ss << std::setfill('0');
size_t idx = 0;
size_t predictions = 0, good = 0;
for (size_t i = 0; i < 2; ++i)
{
for (size_t j = 0; j < 2; ++j)
{
ss.str(std::string());
const int val = int(buffer[idx++]);
ss << std::setw(3) << val;
predictions += val;
this->getLogManager() << ss.str() << " ";
if (i == j) { good += val; }
}
this->getLogManager() << "\n";
}
ss.str(std::string());
ss << "Accuracy = " << std::fixed << std::setprecision(1) << (predictions == 0 ? 0.0 : 100.0 * double(good) / double(predictions)) << "\n";
this->getLogManager() << ss.str();
//***** Codecs *****
m_oConfusion = nullptr;
m_confusionEncoder.uninitialize();
if (m_IdleFuncTag)
{
m_StimuliQueue.clear();
g_source_remove(m_IdleFuncTag);
m_IdleFuncTag = 0;
}
delete m_StimulusSender;
m_StimulationDecoder.uninitialize();
m_MatrixDecoder.uninitialize();
//destroy drawing area
if (m_DrawingArea)
{
gtk_widget_destroy(m_DrawingArea);
m_DrawingArea = nullptr;
}
/* unref the xml file as it's not needed anymore */
g_object_unref(G_OBJECT(m_Builder));
m_Builder = nullptr;
if (m_OriginalBar) { g_object_unref(G_OBJECT(m_OriginalBar)); }
if (m_LeftBar) { g_object_unref(G_OBJECT(m_LeftBar)); }
if (m_RightBar) { g_object_unref(G_OBJECT(m_RightBar)); }
if (m_LeftArrow) { g_object_unref(G_OBJECT(m_LeftArrow)); }
if (m_RightArrow) { g_object_unref(G_OBJECT(m_RightArrow)); }
if (m_UpArrow) { g_object_unref(G_OBJECT(m_UpArrow)); }
if (m_DownArrow) { g_object_unref(G_OBJECT(m_DownArrow)); }
if (m_OriginalLeftArrow) { g_object_unref(G_OBJECT(m_OriginalLeftArrow)); }
if (m_OriginalRightArrow) { g_object_unref(G_OBJECT(m_OriginalRightArrow)); }
if (m_OriginalUpArrow) { g_object_unref(G_OBJECT(m_OriginalUpArrow)); }
if (m_OriginalDownArrow) { g_object_unref(G_OBJECT(m_OriginalDownArrow)); }
if (m_visualizationCtx)
{
this->releasePluginObject(m_visualizationCtx);
m_visualizationCtx = nullptr;
}
return true;
}
bool CGrazVisualization::processInput(const size_t /*index*/)
{
getBoxAlgorithmContext()->markAlgorithmAsReadyToProcess();
return true;
}
bool CGrazVisualization::process()
{
Kernel::IBoxIO* boxIO = getBoxAlgorithmContext()->getDynamicBoxContext();
for (size_t chunk = 0; chunk < boxIO->getInputChunkCount(0); ++chunk)
{
m_StimulationDecoder.decode(chunk);
if (m_StimulationDecoder.isBufferReceived())
{
const IStimulationSet* stimSet = m_StimulationDecoder.getOutputStimulationSet();
for (size_t s = 0; s < stimSet->getStimulationCount(); ++s)
{
setStimulation(s, stimSet->getStimulationIdentifier(s), stimSet->getStimulationDate(s));
}
}
}
for (size_t chunk = 0; chunk < boxIO->getInputChunkCount(1); ++chunk)
{
m_MatrixDecoder.decode(chunk);
if (m_MatrixDecoder.isHeaderReceived())
{
const CMatrix* matrix = m_MatrixDecoder.getOutputMatrix();
if (matrix->getDimensionCount() == 0)
{
this->getLogManager() << Kernel::LogLevel_Error << "Error, dimension count is 0 for Amplitude input !\n";
return false;
}
if (matrix->getDimensionCount() > 1)
{
for (size_t k = 1; k < matrix->getDimensionSize(k); ++k)
{
if (matrix->getDimensionSize(k) > 1)
{
this->getLogManager() << Kernel::LogLevel_Error << "Error, only column vectors supported as Amplitude!\n";
return false;
}
}
}
if (matrix->getDimensionSize(0) == 0)
{
this->getLogManager() << Kernel::LogLevel_Error << "Error, need at least 1 dimension in Amplitude input !\n";
return false;
}
if (matrix->getDimensionSize(0) >= 2)
{
this->getLogManager() << Kernel::LogLevel_Trace <<
"Got 2 or more dimensions for feedback, feedback will be the difference between the first two.\n";
m_TwoValueInput = true;
}
m_confusionEncoder.encodeHeader();
}
if (m_MatrixDecoder.isBufferReceived())
{
setMatrixBuffer(m_MatrixDecoder.getOutputMatrix()->getBuffer());
m_confusionEncoder.encodeBuffer();
}
if (m_MatrixDecoder.isEndReceived()) { m_confusionEncoder.encodeBuffer(); }
boxIO->markOutputAsReadyToSend(0, boxIO->getInputChunkStartTime(1, chunk), boxIO->getInputChunkEndTime(1, chunk));
}
// After any possible rendering, we flush the accumulated stimuli. The default idle func is low priority, so it should be run after rendering by gtk.
// Only register a single idle func, if the previous is there its just as good
if (m_IdleFuncTag == 0) { m_IdleFuncTag = g_idle_add(FlushCB, this); }
return true;
}
void CGrazVisualization::redraw()
{
switch (m_CurrentState)
{
case EStates::Reference:
drawReferenceCross();
break;
case EStates::Cue:
drawReferenceCross();
drawArrow(m_ShowInstruction ? m_CurrentDirection : EArrowDirections::None);
break;
case EStates::ContinousFeedback:
drawReferenceCross();
if (m_ShowFeedback && !m_DelayFeedback) { drawBar(); }
break;
case EStates::Idle:
if (m_ShowFeedback && m_DelayFeedback) { drawBar(); }
break;
default: break;
}
if (m_ShowAccuracy) { drawAccuracy(); }
}
void CGrazVisualization::drawReferenceCross()
{
const gint width = m_DrawingArea->allocation.width,
height = m_DrawingArea->allocation.height;
//increase line's width
gdk_gc_set_line_attributes(m_DrawingArea->style->fg_gc[GTK_WIDGET_STATE(m_DrawingArea)], 1, GDK_LINE_SOLID, GDK_CAP_BUTT, GDK_JOIN_BEVEL);
//horizontal line
gdk_draw_line(m_DrawingArea->window, m_DrawingArea->style->fg_gc[GTK_WIDGET_STATE(m_DrawingArea)], (width / 4), (height / 2), ((3 * width) / 4),
(height / 2));
//vertical line
gdk_draw_line(m_DrawingArea->window, m_DrawingArea->style->fg_gc[GTK_WIDGET_STATE(m_DrawingArea)], (width / 2), (height / 4), (width / 2),
((3 * height) / 4));
//increase line's width
gdk_gc_set_line_attributes(m_DrawingArea->style->fg_gc[GTK_WIDGET_STATE(m_DrawingArea)], 1, GDK_LINE_SOLID, GDK_CAP_BUTT, GDK_JOIN_BEVEL);
}
void CGrazVisualization::drawArrow(const EArrowDirections direction)
{
const gint width = m_DrawingArea->allocation.width,
height = m_DrawingArea->allocation.height;
gint x = (width / 2), y = (height / 2);
switch (direction)
{
case EArrowDirections::None:
this->drawArrow(EArrowDirections::Left);
this->drawArrow(EArrowDirections::Right);
break;
case EArrowDirections::Left:
x -= gdk_pixbuf_get_width(m_LeftArrow) - 1;
y -= gdk_pixbuf_get_height(m_LeftArrow) / 2;
gdk_draw_pixbuf(m_DrawingArea->window, nullptr, m_LeftArrow, 0, 0, x, y, -1, -1, GDK_RGB_DITHER_NONE, 0, 0);
break;
case EArrowDirections::Right:
x += 2;
y -= gdk_pixbuf_get_height(m_RightArrow) / 2;
gdk_draw_pixbuf(m_DrawingArea->window, nullptr, m_RightArrow, 0, 0, x, y, -1, -1, GDK_RGB_DITHER_NONE, 0, 0);
break;
case EArrowDirections::Up:
x -= gdk_pixbuf_get_width(m_UpArrow) / 2;
y -= gdk_pixbuf_get_height(m_UpArrow) - 1;
gdk_draw_pixbuf(m_DrawingArea->window, nullptr, m_UpArrow, 0, 0, x, y, -1, -1, GDK_RGB_DITHER_NONE, 0, 0);
break;
case EArrowDirections::Down:
x -= gdk_pixbuf_get_width(m_DownArrow) / 2;
y += 2;
gdk_draw_pixbuf(m_DrawingArea->window, nullptr, m_DownArrow, 0, 0, x, y, -1, -1, GDK_RGB_DITHER_NONE, 0, 0);
break;
default: break;
}
}
void CGrazVisualization::drawBar() const
{
const gint width = m_DrawingArea->allocation.width;
const gint height = m_DrawingArea->allocation.height;
double usedScale = m_BarScale;
if (m_PositiveFeedbackOnly)
{
// @fixme for multiclass
const size_t trueDirection = size_t(m_CurrentDirection) - 1;
const size_t thisVote = (m_BarScale < 0 ? 0 : 1);
if (trueDirection != thisVote) { usedScale = 0; }
}
gint w = gint(fabs(width * fabs(usedScale) / 2));
w = (w > (width / 2)) ? (width / 2) : w;
gint x = width / 2;
const gint h = height / 6;
const gint y = (height / 2) - (h / 2);
if (m_BarScale < 0)
{
x -= w;
gdk_pixbuf_render_to_drawable(m_LeftBar, m_DrawingArea->window, nullptr, gdk_pixbuf_get_width(m_LeftBar) - w, 0, x, y, w, h, GDK_RGB_DITHER_NONE, 0, 0);
}
else { gdk_pixbuf_render_to_drawable(m_RightBar, m_DrawingArea->window, nullptr, 0, 0, x, y, w, h, GDK_RGB_DITHER_NONE, 0, 0); }
}
void CGrazVisualization::drawAccuracy()
{
std::stringstream tmp;
PangoLayout* layout = pango_layout_new(gdk_pango_context_get());
const double* buffer = m_oConfusion->getBuffer();
pango_layout_set_text(layout, "L", -1);
gdk_draw_layout(m_DrawingArea->window, m_DrawingArea->style->fg_gc[GTK_WIDGET_STATE(m_DrawingArea)], 8, 16, layout);
tmp << std::setfill('0') << std::setw(3) << int(buffer[0]);
pango_layout_set_text(layout, tmp.str().c_str(), -1);
gdk_draw_layout(m_DrawingArea->window, m_DrawingArea->style->white_gc, 8 + 16, 16, layout);
tmp.str(std::string());
tmp << std::setw(3) << int(buffer[1]);
pango_layout_set_text(layout, tmp.str().c_str(), -1);
gdk_draw_layout(m_DrawingArea->window, m_DrawingArea->style->fg_gc[GTK_WIDGET_STATE(m_DrawingArea)], 8 + 56, 16, layout);
pango_layout_set_text(layout, "R", -1);
gdk_draw_layout(m_DrawingArea->window, m_DrawingArea->style->fg_gc[GTK_WIDGET_STATE(m_DrawingArea)], 8, 32, layout);
tmp.str(std::string());
tmp << std::setw(3) << int(buffer[2]);
pango_layout_set_text(layout, tmp.str().c_str(), -1);
gdk_draw_layout(m_DrawingArea->window, m_DrawingArea->style->fg_gc[GTK_WIDGET_STATE(m_DrawingArea)], 8 + 16, 32, layout);
tmp.str(std::string());
tmp << std::setw(3) << int(buffer[3]);
pango_layout_set_text(layout, tmp.str().c_str(), -1);
gdk_draw_layout(m_DrawingArea->window, m_DrawingArea->style->white_gc, 8 + 56, 32, layout);
size_t predictions = 0;
for (size_t i = 0; i < 4; ++i) { predictions += int(buffer[i]); }
tmp.str(std::string());
tmp << "Acc = " << std::fixed << std::setprecision(1) << (predictions == 0 ? 0.0 : 100.0 * (buffer[0] + buffer[3]) / double(predictions)) << "%";
pango_layout_set_text(layout, tmp.str().c_str(), -1);
gdk_draw_layout(m_DrawingArea->window, m_DrawingArea->style->white_gc, 8 + 96, 32, layout);
g_object_unref(layout);
}
double CGrazVisualization::aggregatePredictions(const bool includeAll)
{
double voteAggregate = 0;
// Do we have enough predictions to integrate a result?
if (m_Amplitudes.size() >= m_PredictionsToIntegrate)
{
// step backwards with rev iter to take the latest samples
uint64_t count = 0;
for (auto a = m_Amplitudes.rbegin(); a != m_Amplitudes.rend() && (includeAll || count < m_PredictionsToIntegrate); ++a, ++count)
{
voteAggregate += *a;
m_MaxAmplitude = std::max<double>(m_MaxAmplitude, abs(*a));
}
voteAggregate /= m_MaxAmplitude;
voteAggregate /= count;
}
return voteAggregate;
}
// @fixme for >2 classes
void CGrazVisualization::updateConfusionMatrix(const double prediction)
{
if (m_CurrentDirection == EArrowDirections::Left || m_CurrentDirection == EArrowDirections::Right)
{
const size_t direction = size_t(m_CurrentDirection) - 1;
const size_t vote = (prediction < 0 ? 0 : 1);
(m_oConfusion->getBuffer())[direction * 2 + vote]++;
// std::cout << "Now " << trueDirection << " vote " << thisVote << "\n";
}
}
void CGrazVisualization::setMatrixBuffer(const double* buffer)
{
if (m_CurrentState != EStates::ContinousFeedback)
{
// We're not inside a trial, discard the prediction
return;
}
double predictedAmplitude;
if (m_TwoValueInput)
{
// Ad-hoc forcing to probability (range [0,1], sum to 1). This will make scaling easier
// if run forever in a continuous mode. If the input is already scaled this way, no effect.
//
double v0 = std::abs(buffer[0]), v1 = std::abs(buffer[1]);
const double sum = v0 + v1;
if (sum != 0)
{
v0 = v0 / sum;
v1 = v1 / sum;
}
else
{
v0 = 0.5;
v1 = 0.5;
}
predictedAmplitude = v1 - v0;
}
else { predictedAmplitude = buffer[0]; }
m_Amplitudes.push_back(predictedAmplitude);
if (m_ShowFeedback && !m_DelayFeedback)
{
m_BarScale = aggregatePredictions(false);
gdk_window_invalidate_rect(m_DrawingArea->window, nullptr, true);
}
}
void CGrazVisualization::resize(size_t width, size_t height)
{
width = (width < 8 ? 8 : width);
height = (height < 8 ? 8 : height);
if (m_LeftArrow) { g_object_unref(G_OBJECT(m_LeftArrow)); }
if (m_RightArrow) { g_object_unref(G_OBJECT(m_RightArrow)); }
if (m_UpArrow) { g_object_unref(G_OBJECT(m_UpArrow)); }
if (m_DownArrow) { g_object_unref(G_OBJECT(m_DownArrow)); }
if (m_RightBar) { g_object_unref(G_OBJECT(m_RightBar)); }
if (m_LeftBar) { g_object_unref(G_OBJECT(m_LeftBar)); }
m_LeftArrow = gdk_pixbuf_scale_simple(m_OriginalLeftArrow, int(2 * width) / 8, int(height) / 4, GDK_INTERP_BILINEAR);
m_RightArrow = gdk_pixbuf_scale_simple(m_OriginalRightArrow, int(2 * width) / 8, int(height) / 4, GDK_INTERP_BILINEAR);
m_UpArrow = gdk_pixbuf_scale_simple(m_OriginalUpArrow, int(width) / 4, int(2 * height) / 8, GDK_INTERP_BILINEAR);
m_DownArrow = gdk_pixbuf_scale_simple(m_OriginalDownArrow, int(width) / 4, int(2 * height) / 8, GDK_INTERP_BILINEAR);
m_RightBar = gdk_pixbuf_scale_simple(m_OriginalBar, width, int(height) / 6, GDK_INTERP_BILINEAR);
m_LeftBar = gdk_pixbuf_flip(m_RightBar, true);
}
// Note that we don't need concurrency control here as gtk callbacks run in the main thread
void CGrazVisualization::flushQueue()
{
for (const auto& stimulation : m_StimuliQueue) { m_StimulusSender->sendStimulation(stimulation); }
m_StimuliQueue.clear();
// This function will be automatically removed after completion, so set to 0
m_IdleFuncTag = 0;
}
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,162 @@
#pragma once
#include "../ovp_defines.h"
#include <openvibe/ov_all.h>
#include <toolkit/ovtk_all.h>
#include <visualization-toolkit/ovviz_all.h>
#include <gtk/gtk.h>
#include <deque>
#include <string>
#include <vector>
#include "../utils.h"
namespace TCPTagging {
class IStimulusSender; // fwd declare
} // namespace TCPTagging
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
enum class EArrowDirections { None = 0, Left, Right, Up, Down };
enum class EStates { Idle, Reference, Cue, ContinousFeedback };
/**
*/
class CGrazVisualization final : virtual public Toolkit::TBoxAlgorithm<IBoxAlgorithm>
{
public:
void release() override { delete this; }
bool initialize() override;
bool uninitialize() override;
bool processInput(const size_t index) override;
bool process() override;
void redraw();
void resize(size_t width, size_t height);
void flushQueue(); // Sends all accumulated stimuli to the TCP Tagging
protected:
void setStimulation(size_t index, uint64_t identifier, uint64_t date);
void setMatrixBuffer(const double* buffer);
void processState();
void drawReferenceCross();
void drawArrow(EArrowDirections direction);
void drawBar() const;
void drawAccuracy();
void updateConfusionMatrix(double prediction);
double aggregatePredictions(bool includeAll);
_IsDerivedFromClass_Final_(IBoxAlgorithm, OVP_ClassId_GrazVisualization)
public:
//! The Builder handler used to create the interface
GtkBuilder* m_Builder = nullptr;
GtkWidget* m_MainWindow = nullptr;
GtkWidget* m_DrawingArea = nullptr;
//ebml
Toolkit::TStimulationDecoder<CGrazVisualization> m_StimulationDecoder;
Toolkit::TStreamedMatrixDecoder<CGrazVisualization> m_MatrixDecoder;
EStates m_CurrentState = EStates::Idle;
EArrowDirections m_CurrentDirection = EArrowDirections::None;
double m_MaxAmplitude = -DBL_MAX;
double m_BarScale = 0.0;
//Start and end time of the last buffer
uint64_t m_StartTime = 0;
uint64_t m_EndTime = 0;
bool m_TwoValueInput = false;
GdkPixbuf* m_OriginalBar = nullptr;
GdkPixbuf* m_LeftBar = nullptr;
GdkPixbuf* m_RightBar = nullptr;
GdkPixbuf* m_OriginalLeftArrow = nullptr;
GdkPixbuf* m_OriginalRightArrow = nullptr;
GdkPixbuf* m_OriginalUpArrow = nullptr;
GdkPixbuf* m_OriginalDownArrow = nullptr;
GdkPixbuf* m_LeftArrow = nullptr;
GdkPixbuf* m_RightArrow = nullptr;
GdkPixbuf* m_UpArrow = nullptr;
GdkPixbuf* m_DownArrow = nullptr;
GdkColor m_BackgroundColor = InitGDKColor(0, 0, 0, 0);
GdkColor m_ForegroundColor = InitGDKColor(0, 0, 32768, 0);
std::deque<double> m_Amplitudes; // predictions for the current trial
bool m_ShowInstruction = true;
bool m_ShowFeedback = false;
bool m_DelayFeedback = false;
bool m_ShowAccuracy = false;
bool m_PositiveFeedbackOnly = false;
uint64_t m_PredictionsToIntegrate = 5;
// For queuing stimulations to the TCP Tagging
std::vector<uint64_t> m_StimuliQueue;
guint m_IdleFuncTag = 0;
TCPTagging::IStimulusSender* m_StimulusSender = nullptr;
uint64_t m_LastStimulation = 0;
private:
VisualizationToolkit::IVisualizationContext* m_visualizationCtx = nullptr;
Toolkit::TStreamedMatrixEncoder<CGrazVisualization> m_confusionEncoder;
CMatrix* m_oConfusion = nullptr; // Outpout confusion Matrix pointer
};
/**
* Plugin's description
*/
class CGrazVisualizationDesc final : public IBoxAlgorithmDesc
{
public:
CString getName() const override { return CString("Graz visualization"); }
CString getAuthorName() const override { return CString("Bruno Renier, Jussi T. Lindgren"); }
CString getAuthorCompanyName() const override { return CString("INRIA/IRISA"); }
CString getShortDescription() const override { return CString("Visualization plugin for the Graz experiment"); }
CString getDetailedDescription() const override { return CString("Visualization/Feedback plugin for the Graz experiment"); }
CString getCategory() const override { return CString("Visualization/Presentation"); }
CString getVersion() const override { return CString("0.2"); }
void release() override { }
CIdentifier getCreatedClass() const override { return OVP_ClassId_GrazVisualization; }
CString getStockItemName() const override { return CString("gtk-fullscreen"); }
IPluginObject* create() override { return new CGrazVisualization(); }
bool hasFunctionality(const EPluginFunctionality functionality) const override { return functionality == EPluginFunctionality::Visualization; }
bool getBoxPrototype(Kernel::IBoxProto& prototype) const override
{
prototype.addInput("Stimulations", OV_TypeId_Stimulations);
prototype.addInput("Amplitude", OV_TypeId_StreamedMatrix);
prototype.addSetting("Show instruction", OV_TypeId_Boolean, "true");
prototype.addSetting("Show feedback", OV_TypeId_Boolean, "false");
prototype.addSetting("Delay feedback", OV_TypeId_Boolean, "false");
prototype.addSetting("Show accuracy", OV_TypeId_Boolean, "false");
prototype.addSetting("Predictions to integrate", OV_TypeId_Integer, "5");
prototype.addSetting("Positive feedback only", OV_TypeId_Boolean, "false");
prototype.addOutput("Confusion Matrix", OV_TypeId_StreamedMatrix);
return true;
}
_IsDerivedFromClass_Final_(IBoxAlgorithmDesc, OVP_ClassId_GrazVisualizationDesc)
};
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,249 @@
/*
* Note: The signal display and its subclasses (SignalChannelDisplay, SignalDisplayView)
* were rehauled to give a better user experience for different types of signal. However,
* the code could likely use significant refactoring for clarity and maintainability.
* If this is done, care should be taken that the code does not break.
*/
#include "ovpCSignalDisplay.h"
#include <iostream>
#include <sstream>
#include <system/ovCTime.h>
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
bool CSignalDisplay::initialize()
{
this->getStaticBoxContext().getInputType(0, m_InputTypeID);
if (m_InputTypeID == OV_TypeId_StreamedMatrix) { m_StreamDecoder = new Toolkit::TStreamedMatrixDecoder<CSignalDisplay>(*this, 0); }
else if (m_InputTypeID == OV_TypeId_Signal) { m_StreamDecoder = new Toolkit::TSignalDecoder<CSignalDisplay>(*this, 0); }
else
{
this->getLogManager() << Kernel::LogLevel_Error << "Unknown input stream type at stream 0\n";
return false;
}
m_StimulationDecoder.initialize(*this, 1);
m_UnitDecoder.initialize(*this, 2);
m_BufferDatabase = new CBufferDatabase(*this);
//retrieve settings
const CIdentifier displayMode = uint64_t(FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 0));
const CIdentifier scalingMode = uint64_t(FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 1));
m_refreshInterval = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 2);
const double verticalScale = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 3);
const double verticalOffset = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 4);
const double timeScale = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 5);
const bool horizontalRuler = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 6);
const bool verticalRuler = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 7);
const bool multiview = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 8);
if (m_refreshInterval < 0)
{
this->getLogManager() << Kernel::LogLevel_Error << "Refresh interval must be >= 0\n";
return false;
}
if (verticalScale <= 0)
{
this->getLogManager() << Kernel::LogLevel_Error << "Vertical scale must be > 0\n";
return false;
}
if (timeScale <= 0)
{
this->getLogManager() << Kernel::LogLevel_Error << "Time scale must be > 0\n";
return false;
}
this->getLogManager() << Kernel::LogLevel_Debug << "l_sVerticalScale=" << verticalScale << ", offset " << verticalOffset << "\n";
this->getLogManager() << Kernel::LogLevel_Trace << "l_sScalingMode=" << scalingMode << "\n";
//create GUI
m_SignalDisplayView = new CSignalDisplayView(*m_BufferDatabase, displayMode, scalingMode, verticalScale, verticalOffset, timeScale, horizontalRuler,
verticalRuler, multiview);
m_BufferDatabase->setDrawable(m_SignalDisplayView);
//parent visualization box in visualization tree
GtkWidget* widget = nullptr;
GtkWidget* toolbar = nullptr;
dynamic_cast<CSignalDisplayView*>(m_SignalDisplayView)->getWidgets(widget, toolbar);
m_visualizationCtx = dynamic_cast<VisualizationToolkit::IVisualizationContext*>(this->createPluginObject(
OVP_ClassId_Plugin_VisualizationCtx));
m_visualizationCtx->setWidget(*this, widget);
if (toolbar != nullptr) { m_visualizationCtx->setToolbar(*this, toolbar); }
m_lastScaleRefreshTime = 0;
return true;
}
bool CSignalDisplay::uninitialize()
{
m_UnitDecoder.uninitialize();
m_StimulationDecoder.uninitialize();
if (m_StreamDecoder)
{
m_StreamDecoder->uninitialize();
delete m_StreamDecoder;
}
delete m_SignalDisplayView;
delete m_BufferDatabase;
m_SignalDisplayView = nullptr;
m_BufferDatabase = nullptr;
if (m_visualizationCtx)
{
this->releasePluginObject(m_visualizationCtx);
m_visualizationCtx = nullptr;
}
return true;
}
bool CSignalDisplay::processInput(const size_t /*index*/)
{
getBoxAlgorithmContext()->markAlgorithmAsReadyToProcess();
return true;
}
bool CSignalDisplay::process()
{
IDynamicBoxContext* boxContext = getBoxAlgorithmContext()->getDynamicBoxContext();
if (m_BufferDatabase->getErrorStatus())
{
this->getLogManager() << Kernel::LogLevel_Error <<
"Buffer database reports an error. Its possible that the inputs given to the Signal Display are not supported by it.\n";
return false;
}
// Subcomponents may generate errors while running e.g. in gtk callbacks, where its not safe/possible to call logmanager
if (!dynamic_cast<CSignalDisplayView*>(m_SignalDisplayView)->m_ErrorState.empty())
{
for (const auto& e : dynamic_cast<CSignalDisplayView*>(m_SignalDisplayView)->m_ErrorState) { this->getLogManager() << Kernel::LogLevel_Error << e; }
return false;
}
#ifdef DEBUG
uint64_t in = System::Time::zgetTime();
#endif
// Channel units on input 2
for (size_t c = 0; c < boxContext->getInputChunkCount(2); ++c)
{
m_UnitDecoder.decode(c);
if (m_UnitDecoder.isBufferReceived())
{
std::vector<std::pair<CString, CString>> channelUnits;
channelUnits.resize(m_UnitDecoder.getOutputMatrix()->getDimensionSize(0));
const double* buffer = m_UnitDecoder.getOutputMatrix()->getBuffer();
for (size_t i = 0; i < channelUnits.size(); ++i)
{
CString unit = this->getTypeManager().getEnumerationEntryNameFromValue(OV_TypeId_MeasurementUnit, uint64_t(buffer[i * 2 + 0]));
CString factor = this->getTypeManager().getEnumerationEntryNameFromValue(OV_TypeId_Factor, uint64_t(buffer[i * 2 + 1]));
channelUnits[i] = std::pair<CString, CString>(unit, factor);
}
if (!dynamic_cast<CSignalDisplayView*>(m_SignalDisplayView)->setChannelUnits(channelUnits))
{
this->getLogManager() << Kernel::LogLevel_Warning << "Unable to set channel units properly\n";
}
}
}
// Stimulations in input 1
for (size_t c = 0; c < boxContext->getInputChunkCount(1); ++c)
{
m_StimulationDecoder.decode(c);
if (m_StimulationDecoder.isBufferReceived())
{
const IStimulationSet* stimSet = m_StimulationDecoder.getOutputStimulationSet();
const size_t count = stimSet->getStimulationCount();
m_BufferDatabase->setStimulationCount(count);
for (size_t s = 0; s < count; ++s)
{
const uint64_t id = stimSet->getStimulationIdentifier(s);
const uint64_t date = stimSet->getStimulationDate(s);
CString name = getTypeManager().getEnumerationEntryNameFromValue(OV_TypeId_Stimulation, id);
if (name == CString("")) { name = CString(("Id " + std::to_string(id)).c_str()); }
dynamic_cast<CSignalDisplayView*>(m_SignalDisplayView)->onStimulationReceivedCB(id, name);
m_BufferDatabase->setStimulation(s, id, date);
}
}
}
// Streamed matrix in input 0
for (size_t c = 0; c < boxContext->getInputChunkCount(0); ++c)
{
m_StreamDecoder->decode(c);
if (m_StreamDecoder->isHeaderReceived())
{
const CMatrix* matrix = static_cast<Toolkit::TStreamedMatrixDecoder<CSignalDisplay>*>(m_StreamDecoder)->getOutputMatrix();
if (m_InputTypeID == OV_TypeId_Signal)
{
const uint64_t rate = static_cast<Toolkit::TSignalDecoder<CSignalDisplay>*>(m_StreamDecoder)->getOutputSamplingRate();
m_BufferDatabase->setSampling(size_t(rate));
}
m_BufferDatabase->setMatrixDimensionCount(matrix->getDimensionCount());
for (size_t i = 0; i < matrix->getDimensionCount(); ++i)
{
m_BufferDatabase->setMatrixDimensionSize(i, matrix->getDimensionSize(i));
for (size_t j = 0; j < matrix->getDimensionSize(i); ++j) { m_BufferDatabase->setMatrixDimensionLabel(i, j, matrix->getDimensionLabel(i, j)); }
}
}
if (m_StreamDecoder->isBufferReceived())
{
const CMatrix* matrix = static_cast<Toolkit::TStreamedMatrixDecoder<CSignalDisplay>*>(m_StreamDecoder)->getOutputMatrix();
#ifdef DEBUG
static int count = 0;
std::cout << "Push chunk " << (count++) << " at " << boxContext->getInputChunkStartTime(0, c) << "\n";
if (!matrix->isBufferValid())
{
this->getLogManager() << Kernel::LogLevel_Warning << "Chunk at ["
<< boxContext->getInputChunkStartTime(0, c) << ", "
<< boxContext->getInputChunkEndTime(0, c) << "] "
<< "contains invalid entries\n";
}
#endif
const bool returnValue = m_BufferDatabase->setMatrixBuffer(matrix->getBuffer(), boxContext->getInputChunkStartTime(0, c),
boxContext->getInputChunkEndTime(0, c));
if (!returnValue) { return false; }
}
}
const uint64_t timeNow = getPlayerContext().getCurrentTime();
if (m_lastScaleRefreshTime == 0 || timeNow - m_lastScaleRefreshTime > CTime(m_refreshInterval).time())
{
dynamic_cast<CSignalDisplayView*>(m_SignalDisplayView)->refreshScale();
m_lastScaleRefreshTime = timeNow;
}
#ifdef DEBUG
out = System::Time::zgetTime();
std::cout << "Elapsed1 " << out - in << "\n";
#endif
return true;
}
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,141 @@
#pragma once
#include "../ovp_defines.h"
#include <openvibe/ov_all.h>
#include <toolkit/ovtk_all.h>
#include <visualization-toolkit/ovviz_all.h>
#include "../ovpCBufferDatabase.h"
#include "ovpCSignalDisplay/ovpCSignalDisplayView.h"
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
/**
* This plugin opens a new GTK window and displays the incoming signals. The user may change the zoom level,
* the width of the time window displayed, ...
*/
class CSignalDisplay final : public Toolkit::TBoxAlgorithm<IBoxAlgorithm>
{
public:
CSignalDisplay() = default;
void release() override { delete this; }
bool initialize() override;
bool uninitialize() override;
bool processInput(const size_t index) override;
bool process() override;
_IsDerivedFromClass_Final_(IBoxAlgorithm, OVP_ClassId_SignalDisplay)
Toolkit::TDecoder<CSignalDisplay>* m_StreamDecoder = nullptr;
Toolkit::TStimulationDecoder<CSignalDisplay> m_StimulationDecoder;
Toolkit::TChannelUnitsDecoder<CSignalDisplay> m_UnitDecoder;
//The main object used for the display (contains all the GUI code)
CSignalDisplayDrawable* m_SignalDisplayView = nullptr;
//Contains all the data about the incoming signal
CBufferDatabase* m_BufferDatabase = nullptr;
CIdentifier m_InputTypeID = CIdentifier::undefined();
protected:
uint64_t m_lastScaleRefreshTime = 0;
double m_refreshInterval = 0;
private:
VisualizationToolkit::IVisualizationContext* m_visualizationCtx = nullptr;
};
class CSignalDisplayListener final : public Toolkit::TBoxListener<IBoxListener>
{
public:
bool onInputTypeChanged(Kernel::IBox& box, const size_t index) override
{
if (index == 1)
{
CIdentifier settingType = CIdentifier::undefined();
box.getSettingType(index, settingType);
if (settingType != OV_TypeId_Stimulations)
{
this->getLogManager() << Kernel::LogLevel_Error << "Error: Only stimulation type supported for input 2\n";
box.setInputType(index, OV_TypeId_Stimulations);
}
}
else if (index == 2)
{
CIdentifier settingType = CIdentifier::undefined();
box.getSettingType(index, settingType);
if (settingType != OV_TypeId_ChannelUnits)
{
this->getLogManager() << Kernel::LogLevel_Error << "Error: Only measurement unit type supported for input 3\n";
box.setInputType(index, OV_TypeId_ChannelUnits);
}
}
return true;
}
_IsDerivedFromClass_Final_(Toolkit::TBoxListener<IBoxListener>, CIdentifier::undefined())
};
/**
* Signal Display plugin descriptor
*/
class CSignalDisplayDesc final : public IBoxAlgorithmDesc
{
public:
CString getName() const override { return CString("Signal display"); }
CString getAuthorName() const override { return CString("Bruno Renier, Yann Renard, Alison Cellard, Jussi T. Lindgren"); }
CString getAuthorCompanyName() const override { return CString("INRIA/IRISA"); }
CString getShortDescription() const override { return CString("Displays the incoming stream"); }
CString getDetailedDescription() const override { return CString("This box can be used to visualize signal and matrix streams"); }
CString getCategory() const override { return CString("Visualization/Basic"); }
CString getVersion() const override { return CString("0.3"); }
void release() override { }
CIdentifier getCreatedClass() const override { return OVP_ClassId_SignalDisplay; }
CString getStockItemName() const override { return CString("gtk-zoom-fit"); }
IPluginObject* create() override { return new CSignalDisplay(); }
IBoxListener* createBoxListener() const override { return new CSignalDisplayListener; }
void releaseBoxListener(IBoxListener* listener) const override { delete listener; }
bool hasFunctionality(const EPluginFunctionality functionality) const override { return functionality == EPluginFunctionality::Visualization; }
bool getBoxPrototype(Kernel::IBoxProto& prototype) const override
{
prototype.addSetting("Display Mode", OVP_TypeId_SignalDisplayMode, "Scan");
prototype.addSetting("Auto vertical scale", OVP_TypeId_SignalDisplayScaling, CSignalDisplayView::SCALING_MODES[0].c_str());
prototype.addSetting("Scale refresh interval (secs)", OV_TypeId_Float, "5");
prototype.addSetting("Vertical Scale",OV_TypeId_Float, "100");
prototype.addSetting("Vertical Offset",OV_TypeId_Float, "0");
prototype.addSetting("Time Scale", OV_TypeId_Float, "10");
prototype.addSetting("Bottom ruler", OV_TypeId_Boolean, "true");
prototype.addSetting("Left ruler", OV_TypeId_Boolean, "false");
prototype.addSetting("Multiview", OV_TypeId_Boolean, "false");
prototype.addInput("Data", OV_TypeId_Signal);
prototype.addInput("Stimulations", OV_TypeId_Stimulations);
prototype.addInput("Channel Units", OV_TypeId_ChannelUnits);
prototype.addInputSupport(OV_TypeId_Signal);
prototype.addInputSupport(OV_TypeId_StreamedMatrix);
prototype.addInputSupport(OV_TypeId_ChannelUnits);
prototype.addFlag(Kernel::BoxFlag_CanModifyInput);
return true;
}
_IsDerivedFromClass_Final_(IBoxAlgorithmDesc, OVP_ClassId_SignalDisplayDesc)
};
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,980 @@
/*
* Notes:
*
* - currently this component actually never redraws by design. Calling the corresp. gtk functions for the whole
* set of buffers in memory causes the display to lag/freeze on Windows with big signals. If you 'fix' the
* redraw to actually work, make sure that the display runs smoothly in real-time on Win when Signal Display is in
* full screen (maximized), 1000hz and 256 channels, including using scrollbar, alt-tab, resize, occluding windows, etc.
*
* - explanation of y margins:
*
* outerTop ---
* innerTop ---
* [a single signal channel here]
* innerBottom ---
* outerBottom ---
*
* A channel is imagined to reside between the 'outer' top and bottom margins with a little headroom.
* The automatic rescalers should react if the signal passes outside either of the 'inner' margins.
*/
#include "ovpCSignalChannelDisplay.h"
#include "ovpCSignalDisplayView.h"
#include <system/ovCTime.h>
#include <cmath> // For unix system
#include <iostream>
#include "../../utils.h"
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
// #define DEBUG 1
// Redraw can be enabled by setting the following, however some bugs remain in some cases, esp.
// if the system is not able to render fast enough for the signal.
#define SUPPORT_REDRAW 0
static gboolean DrawingAreaExposeEventCB(GtkWidget* widget, GdkEventExpose* event, gpointer data);
static gboolean DrawingAreaConfigureCB(GtkWidget* widget, GdkEventExpose* event, gpointer data);
static gboolean DrawingAreaResizeEventCB(GtkWidget* widget, GtkAllocation* allocation, gpointer data);
static void DrawingAreaClickedEventCB(GtkWidget* widget, GdkEventButton* event, gpointer data);
static void DrawingAreaEnterEventCB(GtkWidget* widget, GdkEventCrossing* event, gpointer data);
static void DrawingAreaLeaveEventCB(GtkWidget* widget, GdkEventCrossing* event, gpointer data);
static gboolean VisibleRegionChangedCB(GtkWidget* widget, gpointer data);
CSignalChannelDisplay::CSignalChannelDisplay(CSignalDisplayView* displayView, const int channelDisplayW, const int channelDisplayH, const int leftRulerW,
const int leftRulerH)
: m_ParentDisplayView(displayView), m_Database(displayView->m_Buffer), m_LeftRulerW(leftRulerW), m_LeftRulerH(leftRulerH), m_StopY(leftRulerH)
{
//creates the drawing area
m_DrawingArea = gtk_drawing_area_new();
gtk_widget_set_size_request(m_DrawingArea, channelDisplayW, channelDisplayH);
//Set background color (White)
GdkColor backgroundColor = InitGDKColor(0, 65535, 65535, 65535);
gtk_widget_modify_bg(m_DrawingArea, GTK_STATE_NORMAL, &backgroundColor);
//connects the signals
gtk_widget_add_events(GTK_WIDGET(m_DrawingArea), GDK_BUTTON_PRESS_MASK);
gtk_widget_add_events(GTK_WIDGET(m_DrawingArea), GDK_ENTER_NOTIFY_MASK);
gtk_widget_add_events(GTK_WIDGET(m_DrawingArea), GDK_LEAVE_NOTIFY_MASK);
gtk_widget_add_events(GTK_WIDGET(m_DrawingArea), GDK_CONFIGURE); // Size change
g_signal_connect_after(G_OBJECT(m_DrawingArea), "expose_event", G_CALLBACK(DrawingAreaExposeEventCB), this);
g_signal_connect_after(G_OBJECT(m_DrawingArea), "size-allocate", G_CALLBACK(DrawingAreaResizeEventCB), this);
g_signal_connect_after(G_OBJECT(m_DrawingArea), "button-press-event", G_CALLBACK(DrawingAreaClickedEventCB), this);
g_signal_connect_after(G_OBJECT(m_DrawingArea), "enter-notify-event", G_CALLBACK(DrawingAreaEnterEventCB), this);
g_signal_connect_after(G_OBJECT(m_DrawingArea), "leave-notify-event", G_CALLBACK(DrawingAreaLeaveEventCB), this);
g_signal_connect_after(G_OBJECT(m_DrawingArea), "configure-event", G_CALLBACK(DrawingAreaConfigureCB), this); // Size change, set draw limits
// These take care of setting the redraw limits in the case of vertical scroll by user
GtkWidget* widget = GTK_WIDGET(gtk_builder_get_object(m_ParentDisplayView->m_Builder, "SignalDisplayChannelsScrolledWindow"));
GtkAdjustment* vadj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(widget));
g_signal_connect_after(G_OBJECT(vadj), "value-changed", G_CALLBACK(VisibleRegionChangedCB), this);
}
CSignalChannelDisplay::~CSignalChannelDisplay()
{
for (auto it = m_LeftRuler.begin(); it != m_LeftRuler.end(); ++it) { delete it->second; }
m_LeftRuler.clear();
/*
m_TranslateY.clear();
m_InnerTopMargin.clear();
m_OuterTopMargin.clear();
m_OuterBottomMargin.clear();
m_InnerBottomMargin.clear();
m_ScaleY.clear();*/
}
GtkWidget* CSignalChannelDisplay::getRulerWidget(const size_t index) const
{
const auto it = m_LeftRuler.find(index);
if (it != m_LeftRuler.end() && it->second) { return it->second->getWidget(); }
return nullptr;
}
void CSignalChannelDisplay::onResizeEventCB(const gint width, const gint height)
{
m_Width = width;
m_Height = height;
m_StartY = 0;
m_StopY = m_Height;
updateScale();
}
void CSignalChannelDisplay::updateScale()
{
const size_t samplesPerBuffer = m_Database->m_DimSizes[1];
size_t nBufferToDisplay = m_Database->m_NBufferToDisplay;
if (samplesPerBuffer == 1 && nBufferToDisplay != 1) { nBufferToDisplay--; }
m_WidthPerBuffer = double(m_Width) / double(nBufferToDisplay);
m_PointStep = 0;
if ((samplesPerBuffer * nBufferToDisplay) - 1 != 0) { m_PointStep = double(m_Width) / double((samplesPerBuffer * nBufferToDisplay) - 1); }
#ifdef DEBUG
std::cout << "Requesting full redraw, C (updateScale)\n";
#endif
redrawAllAtNextRefresh(true);
}
void CSignalChannelDisplay::resetChannelList()
{
m_ChannelList.clear();
m_TranslateY.clear();
m_OuterTopMargin.clear();
m_InnerTopMargin.clear();
m_InnerBottomMargin.clear();
m_OuterBottomMargin.clear();
m_ScaleY.clear();
}
void CSignalChannelDisplay::addChannel(const size_t channel)
{
m_ChannelList.push_back(channel);
m_LeftRuler[channel] = new CSignalDisplayLeftRuler(m_LeftRulerW, m_LeftRulerH);
m_TranslateY.push_back(0);
m_OuterTopMargin.push_back(0);
m_InnerTopMargin.push_back(0);
m_InnerBottomMargin.push_back(0);
m_OuterBottomMargin.push_back(0);
m_ScaleY.push_back(1);
}
// Adds a channel, but no ruler
void CSignalChannelDisplay::addChannelList(const size_t channel)
{
m_ChannelList.push_back(channel);
m_TranslateY.push_back(0);
m_OuterTopMargin.push_back(0);
m_InnerTopMargin.push_back(0);
m_InnerBottomMargin.push_back(0);
m_OuterBottomMargin.push_back(0);
m_ScaleY.push_back(1);
}
uint64_t CSignalChannelDisplay::cropCurve(const uint64_t pointCount) const
{
if (pointCount == 0) { return 0; }
//clears the vector of the points to draw
m_ParentDisplayView->m_Points.clear();
GdkPoint point;
std::vector<std::pair<double, double>>& curvePoints = m_ParentDisplayView->m_RawPoints;
GdkRegion* reg = gdk_drawable_get_visible_region(m_DrawingArea->window);
GdkRectangle box;
gdk_region_get_clipbox(reg, &box);
const double yStart = box.y,
yStop = box.y + box.height;
//for each couple of successive points
for (size_t i = 0; i < size_t(pointCount - 1); ++i)
{
//get the two points coordinates
const double x0 = curvePoints[i].first, y0 = curvePoints[i].second, x1 = curvePoints[i + 1].first, y1 = curvePoints[i + 1].second;
//if(!gdk_region_point_in(reg, x0, y0) || !gdk_region_point_in(reg, x1, y1)) { continue; }
const bool firstOutTop = (y0 < yStart), firstOutBottom = (y0 >= yStop),
secondOutTop = (y1 < yStart), secondOutBottom = (y1 >= yStop),
firstPointOut = firstOutTop || firstOutBottom,
secondPointOut = secondOutTop || secondOutBottom;
//if one of the points is out of the drawing area
if (firstPointOut || secondPointOut)
{
if ((firstOutTop && secondOutTop) || (firstOutBottom && secondOutBottom)) { continue; } // Both out and on the same side, forget about it
//computes the line's coefficients
const double a = (y1 - y0) / (x1 - x0), // slope
b = y0 - (a * x0); // intersect
//if the first point is out of the window
if (firstOutTop)
{
//computes its X-coordinate with the minimum Y
point.x = gint(-b / a);
//we take -1 and not 0, this way, the line between the two successive intersect points won't be drawn
point.y = gint(yStart - 1);
//adds it to the vector
m_ParentDisplayView->m_Points.push_back(point);
}
else if (firstOutBottom)
{
//same with the maximum Y
point.x = gint((yStop - b) / a);
point.y = gint(yStop);
m_ParentDisplayView->m_Points.push_back(point);
}
//if it is inside, keep its current coordinates
else
{
point.x = gint(x0);
point.y = gint(y0);
m_ParentDisplayView->m_Points.push_back(point);
}
//if the second point is out of the window, computes its intersect point and adds it
if (secondOutTop)
{
point.x = gint(-b / a);
point.y = gint(yStart - 1);
m_ParentDisplayView->m_Points.push_back(point);
}
else if (secondOutBottom)
{
point.x = gint((yStop - b) / a);
point.y = gint(yStop);
m_ParentDisplayView->m_Points.push_back(point);
}
}
else //both points lie within the drawing area
{
//keep the first point
point.x = gint(x0);
point.y = gint(y0);
m_ParentDisplayView->m_Points.push_back(point);
//add the last point
if (i == pointCount - 2)
{
point.x = gint(x1);
point.y = gint(y1);
m_ParentDisplayView->m_Points.push_back(point);
}
}
// if(point.x<box.x || point.x>=box.x+box.width) { std::cout << "blam\n"; } // assert
}
//return the number of points to draw
return m_ParentDisplayView->m_Points.size();
}
void CSignalChannelDisplay::getUpdateRectangle(GdkRectangle& rect) const
{
rect.y = m_StartY;
rect.height = m_StopY - m_StartY;
//if in scroll mode, or if redrawing everything, update the whole drawing area
if (m_Database->getDisplayMode() == Scroll || mustRedrawAll())
{
rect.x = 0;
rect.width = m_Width;
}
else //partial redraw only
{
//determine index and position of first buffer to display, and index of first sample to display
size_t bufferIdx = 0;
size_t sampleIdx = 0;
size_t bufferPos = 0;
getFirstBufferToDisplayInformation(bufferIdx, sampleIdx, bufferPos);
//X position of first sample that will be drawn when channel is refreshed
const double startX = getSampleXCoordinate(bufferPos, sampleIdx, 0);
//position on screen of latest buffer
const auto latestBufferPosition = size_t(bufferPos + m_Database->m_SampleBuffers.size() - 1 - bufferIdx);
//X position of last sample that will be drawn when channel is refreshed
const auto samplesPerBuffer = size_t(m_Database->m_DimSizes[1]);
const double endX = getSampleXCoordinate(latestBufferPosition, samplesPerBuffer - 1, 0);
rect.x = gint(startX);
rect.width = gint(endX) - gint(startX) + 1 /* this extra pixel accounts for vertical update line*/ + 1;
}
}
#if SUPPORT_REDRAW
void CSignalChannelDisplay::redrawAllAtNextRefresh(bool redraw) { m_redrawAll = redraw; }
#else
void CSignalChannelDisplay::redrawAllAtNextRefresh(bool /* redraw */) { m_RedrawAll = false; } // currently NOP, see comment at top
#endif
void CSignalChannelDisplay::draw(const GdkRectangle& /*area*/)
{
//ensure there is data to display
if (!m_Database || m_Database->m_SampleBuffers.empty()) { return; }
#ifdef DEBUG
uint64_t in = System::Time::zgetTime();
if (mustRedrawAll()) { std::cout << "Draw(): RedrawAll was requested of " << this << "\n"; }
#endif
const double sizePerChannel = m_Height / double(m_ChannelList.size());
//updates the left ruler
if (m_MultiView)
{
const double max = m_TranslateY[0] - ((0 - ((m_Height * m_ZoomScaleY) / 2) + (m_ZoomTranslateY * m_ZoomScaleY)) / (m_ScaleY[0] * m_ZoomScaleY * m_Height
));
const double min = m_TranslateY[0] - ((m_Height - ((m_Height * m_ZoomScaleY) / 2) + (m_ZoomTranslateY * m_ZoomScaleY)) / (
m_ScaleY[0] * m_ZoomScaleY * m_Height));
m_LeftRuler[0]->update(min, max);
}
else
{
// own ruler for each channel
for (size_t i = m_FirstChannelToDisplay; i <= m_LastChannelToDisplay; ++i)
{
const double max = m_TranslateY[i] - ((0 - ((sizePerChannel * m_ZoomScaleY) / 2) + (m_ZoomTranslateY * m_ZoomScaleY)) / (
m_ScaleY[i] * m_ZoomScaleY * sizePerChannel));
const double min = m_TranslateY[i] - ((sizePerChannel - ((sizePerChannel * m_ZoomScaleY) / 2) + (m_ZoomTranslateY * m_ZoomScaleY)) / (
m_ScaleY[i] * m_ZoomScaleY * sizePerChannel));
m_LeftRuler[m_ChannelList[i]]->update(min, max);
}
}
//determine index and position of first (in the sense of leftmost) buffer to display, and index of first sample to display
const auto samplesPerBuffer = size_t(m_Database->m_DimSizes[1]);
size_t bufferIdx = 0;
size_t sampleIdx = 0;
size_t bufferPos = 0;
getFirstBufferToDisplayInformation(bufferIdx, sampleIdx, bufferPos);
if (m_Database->getDisplayMode() == Scan && !mustRedrawAll())
{
//X position of last drawn sample (0 if restarting from left edge)
const double startX = getSampleXCoordinate(bufferPos, sampleIdx, 0);
#if SUPPORT_REDRAW
//position on screen of latest buffer
const size_t latestBufferPosition = bufferPos + m_Database->m_SampleBuffers.size()-1- bufferIdx;
//X position of last sample that will be drawn when channel is refreshed
const double endX = getSampleXCoordinate(latestBufferPosition, samplesPerBuffer-1, 0);
// is exposed area larger than the currently shown samples indicate?
if(exposedArea.x < (gint)startX ||
exposedArea.width-1/*exposed width is 1 pixel larger than asked for*/ > (gint)endX - (gint)startX + 1 + 1)
{
#ifdef DEBUG
std::cout << "Requesting full redraw, A (expose larger than sample area)\n";
#endif
//this means the window was invalidated by an external widget : redraw it all
redrawAllAtNextRefresh(true);
m_Database->getIndexOfBufferStartingAtTime(m_ParentDisplayView->m_leftmostDisplayedTime, bufferIdx);
bufferPos = 0;
}
else
#endif
{
//start drawing from at least one pixel to the left of first sample so that partial redraws connect well together
const auto oldX = size_t(startX);
size_t curX;
do
{
if (sampleIdx == 0)
{
if (bufferPos == 0) { break; }
bufferIdx--;
bufferPos--;
sampleIdx = samplesPerBuffer - 1;
}
else { sampleIdx--; }
curX = size_t(getSampleXCoordinate(bufferPos, sampleIdx, 0));
} while (curX >= oldX);
}
}
//determine start x coord of first buffer to display
double bufferStartX;
if (m_Database->getDisplayMode() == Scroll)
{
bufferStartX = m_Width - m_Database->m_SampleBuffers.size() * m_WidthPerBuffer;
if (bufferStartX < 0) { bufferStartX = 0; }
}
else { bufferStartX = getSampleXCoordinate(bufferPos, 0, 0); }
const auto lastBufferToDisplay = size_t(m_Database->m_SampleBuffers.size() - 1);
// std::cout << "plot " << firstChannelToDisplay << "," << lastChannelToDisplay << "\n";
//draw latest signals
drawSignals(bufferIdx, lastBufferToDisplay, sampleIdx, bufferStartX, m_FirstChannelToDisplay, m_LastChannelToDisplay);
//in scan mode, there is more to be drawn
if (m_Database->getDisplayMode() == Scan)
{
//draw progress line
drawProgressLine(bufferIdx, bufferPos);
#if SUPPORT_REDRAW
//if redrawing the whole window
if(m_redrawAll == true && bufferIdx > 0)
{
//get start x coord of first buffer after the most recent one
bufferStartX = getSampleXCoordinate(lastBufferToDisplay - bufferIdx + 1, 0, bufferStartX);
//draw older signals (to the right of progress line)
drawSignals(0, bufferIdx -1, 0, bufferStartX, m_FirstChannelToDisplay, m_LastChannelToDisplay);
}
#else
// We never redraw the whole window since with big signals (e.g. 256chns, 1000hz),
// this could cause rendering on Windows to freeze on resizes in a manner
// that the display doesn't recover
#endif
}
//draw Y=0 line
drawZeroLine();
//update time of latest displayed data
m_LatestDisplayedTime = m_Database->m_EndTime.back();
#ifdef DEBUG
uint64_t out = System::Time::zgetTime();
// std::cout << "Elapsed2 " << CTime(out-in).toSeconds() << ", ld=" << m_latestDisplayedTime << "\n";
#endif
//reset redraw all flag
redrawAllAtNextRefresh(false);
}
void CSignalChannelDisplay::computeZoom(const bool zoomIn, const double x, const double y)
{
if (zoomIn)
{
m_ZoomTranslateX += (x - (m_Width / (m_ZoomFactor * 2))) / m_ZoomScaleX;
m_ZoomTranslateY += (y - (m_Height / (m_ZoomFactor * 2))) / m_ZoomScaleY;
m_ZoomScaleX *= m_ZoomFactor;
m_ZoomScaleY *= m_ZoomFactor;
}
else
{
m_ZoomScaleX /= m_ZoomFactor;
m_ZoomScaleY /= m_ZoomFactor;
if (fabs(m_ZoomScaleY - 1) < 0.001) { m_ZoomScaleX = m_ZoomScaleY = 1.; }
m_ZoomTranslateX -= (x - (m_Width / (m_ZoomFactor * 2))) / m_ZoomScaleX;
m_ZoomTranslateY -= (y - (m_Height / (m_ZoomFactor * 2))) / m_ZoomScaleY;
if (fabs(m_ZoomTranslateY) < 0.001) { m_ZoomTranslateX = m_ZoomTranslateY = 1.; }
}
//check if we are out of the window
if (m_ZoomTranslateX < 0) { m_ZoomTranslateX = 0; }
if (m_ZoomTranslateY < 0) { m_ZoomTranslateY = 0; }
if (m_ZoomTranslateX > m_Width - (m_Width / m_ZoomScaleX)) { m_ZoomTranslateX = m_Width - (m_Width / m_ZoomScaleX); }
if (m_ZoomTranslateY > m_Height - (m_Height / m_ZoomScaleY)) { m_ZoomTranslateY = m_Height - (m_Height / m_ZoomScaleY); }
//Put a Y translation breaks the zoom out so let's set it to 0
m_ZoomTranslateY = 0;
}
void CSignalChannelDisplay::getDisplayedValueRange(std::vector<double>& min, std::vector<double>& max) const
{
min.resize(m_ChannelList.size());
max.resize(m_ChannelList.size());
for (size_t k = 0; k < m_ChannelList.size(); ++k)
{
//update maximum and minimum values displayed by this channel
double currentMin, currentMax;
//get local min/max
m_Database->getDisplayedChannelLocalMinMaxValue(m_ChannelList[k], currentMin, currentMax);
//set parameter to recomputed range
min[k] = currentMin;
max[k] = currentMax;
}
}
void CSignalChannelDisplay::setGlobalScaleParameters(const double min, const double max, const double margin)
{
const double maxTop = max + margin;
const double minBottom = min - margin;
for (size_t k = 0; k < m_ChannelList.size(); ++k)
{
m_OuterTopMargin[k] = maxTop;
m_InnerTopMargin[k] = max;
m_InnerBottomMargin[k] = min;
m_OuterBottomMargin[k] = minBottom;
}
updateDisplayParameters();
}
void CSignalChannelDisplay::setLocalScaleParameters(const size_t subChannelIdx, const double min, const double max, const double margin)
{
m_OuterTopMargin[subChannelIdx] = max + margin;
m_InnerTopMargin[subChannelIdx] = max;
m_InnerBottomMargin[subChannelIdx] = min;
m_OuterBottomMargin[subChannelIdx] = min - margin;
// std::cout << "Scaling to [" << min << "," << max << "]S\n";
}
// Assume [a,b] is the range between MinimumBottom and MaxiMumTop of k. If c \in [a,b],
// then m_scales[k]*(m_translates[k]-c) should be in what???
void CSignalChannelDisplay::updateDisplayParameters()
{
//compute the translation needed to center the signal correctly in the window
// m_TranslateX = 0;
for (size_t k = 0; k < m_ChannelList.size(); ++k)
{
if (m_OuterTopMargin[k] == m_OuterBottomMargin[k]) { m_ScaleY[k] = 1; }
else { m_ScaleY[k] = 1 / (m_OuterTopMargin[k] - m_OuterBottomMargin[k]); }
m_TranslateY[k] = (m_OuterTopMargin[k] + m_OuterBottomMargin[k]) / 2;
}
//reflect changes
#ifdef DEBUG
std::cout << "Requesting full redraw, F (display params changed)\n";
#endif
#if SUPPORT_REDRAW
redrawAllAtNextRefresh(true);
#else
// Side effect: draw a little boxes to denote discontinuity in the signal due
// to the runtime change of scale.
GdkColor lineColor = InitGDKColor(0, 65535, 0, 0);
gdk_gc_set_rgb_fg_color(m_DrawingArea->style->fg_gc[GTK_WIDGET_STATE(m_DrawingArea)], &lineColor);
size_t bufferIdx = 0;
size_t sampleIdx = 0;
size_t bufferPos = 0;
getFirstBufferToDisplayInformation(bufferIdx, sampleIdx, bufferPos);
GdkRegion* region = gdk_drawable_get_visible_region(m_DrawingArea->window);
GdkRectangle box;
gdk_region_get_clipbox(region, &box);
const double startX = getSampleXCoordinate(bufferPos, sampleIdx, 0);
gdk_draw_rectangle(m_DrawingArea->window, m_DrawingArea->style->fg_gc[GTK_WIDGET_STATE(m_DrawingArea)], true, gint(startX) - 2, box.y, gint(2), 4);
gdk_draw_rectangle(m_DrawingArea->window, m_DrawingArea->style->fg_gc[GTK_WIDGET_STATE(m_DrawingArea)], true, gint(startX) - 2, box.y + box.height - 4,
gint(2), 4);
lineColor.red = 0 * 65535 / 255;
lineColor.green = 0 * 65535 / 255;
lineColor.blue = 0 * 65535 / 255;
gdk_gc_set_rgb_fg_color(m_DrawingArea->style->fg_gc[GTK_WIDGET_STATE(m_DrawingArea)], &lineColor);
#endif
}
void CSignalChannelDisplay::getFirstBufferToDisplayInformation(size_t& bufferIdx, size_t& sampleIdx, size_t& bufferPos) const
{
bufferIdx = 0;
sampleIdx = 0;
bufferPos = 0;
if (m_Database->getDisplayMode() == Scan)
{
if (m_RedrawAll)
{
m_Database->getIndexOfBufferStartingAtTime(m_ParentDisplayView->m_LeftmostDisplayedTime, bufferIdx);
return;
}
const bool haveLatestBufferDisplayed = m_Database->getIndexOfBufferStartingAtTime(m_LatestDisplayedTime, bufferIdx);
if (!haveLatestBufferDisplayed)
{
#if SUPPORT_REDRAW
//chances are drawing is up to date and this call was triggered following an "external" expose event
//(e.g. the window was covered by a widget which was just moved, resulting in an expose event)
#ifdef DEBUG
std::cout << "Requesting full redraw, B1 (buffer not displayed)\n";
#endif
m_Database->getIndexOfBufferStartingAtTime(m_ParentDisplayView->m_leftmostDisplayedTime, rFirstBufferToDisplay);
redrawAllAtNextRefresh(true);
return;
#else
//=>let's just start from the last sample
bufferIdx = size_t(m_Database->m_SampleBuffers.size() - 1);
#endif
}
//partial redraw
size_t leftmostBufferIdx = 0;
m_Database->getIndexOfBufferStartingAtTime(m_ParentDisplayView->m_LeftmostDisplayedTime, leftmostBufferIdx);
if (leftmostBufferIdx > bufferIdx)
{
// @fixme not sure why this happens...
bufferPos = 0;
}
else
{
//get position of first new buffer
bufferPos = bufferIdx - leftmostBufferIdx;
//redraw from last sample of last drawn buffer, if we're not restarting from left edge
if (bufferPos > 0)
{
bufferIdx--;
bufferPos--;
sampleIdx = size_t(m_Database->m_DimSizes[1]) - 1;
}
}
}
}
int CSignalChannelDisplay::getBufferStartX(const size_t pos) const { return int((pos * m_WidthPerBuffer - 0) * 1); }
double CSignalChannelDisplay::getSampleXCoordinate(const size_t bufferPos, const size_t sampleIdx, const double offset) const
{
return double((offset + bufferPos * m_WidthPerBuffer + sampleIdx * m_PointStep - 0) * 1);
}
double CSignalChannelDisplay::getSampleYCoordinate(const double value, const size_t channelIdx)
{
//TODO : precompute some factors!
const double sizePerChannel = m_Height / double(m_ChannelList.size());
const double translatedData = m_TranslateY[channelIdx] - value;
return m_ScaleY[channelIdx] * m_ZoomScaleY * sizePerChannel * translatedData + (channelIdx + 1) * sizePerChannel * m_ZoomScaleY - m_ZoomTranslateY *
m_ZoomScaleY - sizePerChannel / 2;
}
double CSignalChannelDisplay::getSampleYMultiViewCoordinate(const double value)
{
const double translatedData = m_TranslateY[0] - value;
return m_ScaleY[0] * m_ZoomScaleY * m_Height * translatedData + (m_Height * m_ZoomScaleY) / 2 - m_ZoomTranslateY * m_ZoomScaleY;
}
bool CSignalChannelDisplay::drawSignals(const size_t firstBuffer, const size_t lastBuffer, const size_t firstSample, const double startX,
const size_t firstChannel, size_t lastChannel)
{
//compute and draw sample points
const auto samplesPerBuffer = size_t(m_Database->m_DimSizes[1]);
if (samplesPerBuffer == 0) { return false; } // @FIXME silent fail, but no logManager here, so can't print
GdkColor lineColor = InitGDKColor(0, 0, 0, 0);
lastChannel = std::min(lastChannel, size_t(m_ChannelList.size() - 1));
#ifdef DEBUG
// std::cout << "Channel range [" << firstChannelToDisplay << "," << lastChannelToDisplay << "]\n";
#endif
for (size_t k = firstChannel; k <= lastChannel; ++k)
{
if (m_MultiView) { m_ParentDisplayView->getMultiViewColor(m_ChannelList[k], lineColor); }
else { if (m_CurrentSignalMode != GlobalBestFit) { lineColor.red = 65535; } }
gdk_gc_set_rgb_fg_color(m_DrawingArea->style->fg_gc[GTK_WIDGET_STATE(m_DrawingArea)], &lineColor);
size_t index = 0;
for (size_t j = firstBuffer; j <= lastBuffer; ++j)
{
const double* buffer = (m_Database->m_SampleBuffers)[j] + (m_ChannelList[k] * samplesPerBuffer);
//for all samples in current buffer
for (size_t i = (j == firstBuffer) ? firstSample : 0; i < samplesPerBuffer; ++i, ++index)
{
if (m_MultiView)
{
(m_ParentDisplayView->m_RawPoints)[index].first = getSampleXCoordinate(size_t(j - firstBuffer), size_t(i), startX);
(m_ParentDisplayView->m_RawPoints)[index].second = getSampleYMultiViewCoordinate(buffer[i]);
}
else
{
(m_ParentDisplayView->m_RawPoints)[index].first = getSampleXCoordinate(j - firstBuffer, i, size_t(startX));
(m_ParentDisplayView->m_RawPoints)[index].second = getSampleYCoordinate(buffer[i], size_t(k));
}
}
}
//crop points
const uint64_t nPointsToDisplay = cropCurve(index);
#ifdef DEBUG
if(numberOfPointsToDisplay>2000) { std::cout << "points " << numberOfPointsToDisplay << " in " << k << "\n"; }
#endif
if (nPointsToDisplay != 0)
{
//draw all the points and link them
gdk_draw_lines(m_DrawingArea->window, m_DrawingArea->style->fg_gc[GTK_WIDGET_STATE(m_DrawingArea)], &m_ParentDisplayView->m_Points[0],
gint(nPointsToDisplay));
}
}
if (!m_Database->m_Stimulations.empty())
{
//switch to dashed line
gdk_gc_set_line_attributes(m_DrawingArea->style->fg_gc[GTK_WIDGET_STATE(m_DrawingArea)], 1, GDK_LINE_ON_OFF_DASH, GDK_CAP_BUTT, GDK_JOIN_BEVEL);
//compute current time window start and end time
#if 0
uint64_t startTime = m_Database->m_startTimes[bufferIdx] + m_Database->m_BufferDuration * sampleIdx / samplesPerBuffer;
uint64_t endTime = m_Database->m_startTimes[lastBufferToDisplay] + m_Database->m_BufferDuration;
#else
const uint64_t firstBufferDuration = m_Database->m_EndTime[firstBuffer] - m_Database->m_StartTime[firstBuffer];
const uint64_t lastBufferDuration = m_Database->m_EndTime[lastBuffer] - m_Database->m_StartTime[lastBuffer];
const uint64_t startTime = m_Database->m_StartTime[firstBuffer] + firstBufferDuration * firstSample / samplesPerBuffer;
const uint64_t endTime = m_Database->m_StartTime[lastBuffer] + lastBufferDuration;
#endif
for (auto it = m_Database->m_Stimulations.begin(); it != m_Database->m_Stimulations.end(); ++it)
{
//look for stimulations lying in current time window
if (it->first >= startTime && it->first <= endTime)
{
size_t j = firstBuffer;
while (it->first > m_Database->m_EndTime[j]) { j++; }
m_ParentDisplayView->getStimulationColor(it->second, lineColor);
gdk_gc_set_rgb_fg_color(m_DrawingArea->style->fg_gc[GTK_WIDGET_STATE(m_DrawingArea)], &lineColor);
#if 0
const auto i = size_t((it->first - m_Database->m_startTimes[j]) * samplesPerBuffer / m_Database->m_BufferDuration);
#else
const uint64_t duration = m_Database->m_EndTime[j] - m_Database->m_StartTime[j];
const auto i = size_t((it->first - m_Database->m_StartTime[j]) * samplesPerBuffer / duration);
#endif
const auto x = size_t(getSampleXCoordinate(j - firstBuffer, i, startX));
gdk_draw_line(m_DrawingArea->window, m_DrawingArea->style->fg_gc[GTK_WIDGET_STATE(m_DrawingArea)], x, m_StartY, x, m_StopY);
}
}
}
lineColor.red = 0;
lineColor.green = 0;
lineColor.blue = 0;
gdk_gc_set_rgb_fg_color(m_DrawingArea->style->fg_gc[GTK_WIDGET_STATE(m_DrawingArea)], &lineColor);
gdk_gc_set_line_attributes(m_DrawingArea->style->fg_gc[GTK_WIDGET_STATE(m_DrawingArea)], 1, GDK_LINE_SOLID, GDK_CAP_BUTT, GDK_JOIN_BEVEL);
return true;
}
void CSignalChannelDisplay::drawProgressLine(const size_t firstBufferIdx, const size_t firstBufferPos) const
{
//draw line only if there's more data to be drawn after it
if (m_Database->m_SampleBuffers.size() < m_Database->m_NBufferToDisplay ||
m_ParentDisplayView->m_LeftmostDisplayedTime > m_Database->m_StartTime[0])
{
const auto samplesPerBuffer = size_t(m_Database->m_DimSizes[1]);
//position on screen of latest buffer
const auto latestBufferPosition = size_t(firstBufferPos + m_Database->m_SampleBuffers.size() - 1 - firstBufferIdx);
//X position of last sample that will be drawn when channel is refreshed
const double endX = getSampleXCoordinate(latestBufferPosition, samplesPerBuffer - 1, 0);
GdkColor lineColor = InitGDKColor(0, 0, 65535, 0);
gdk_gc_set_rgb_fg_color(m_DrawingArea->style->fg_gc[GTK_WIDGET_STATE(m_DrawingArea)], &lineColor);
//draw line one pixel after last sample
gdk_draw_line(m_DrawingArea->window, m_DrawingArea->style->fg_gc[GTK_WIDGET_STATE(m_DrawingArea)], size_t(endX) + 1, 0, size_t(endX) + 1, m_Height - 1);
GdkColor black = InitGDKColor(0, 0, 0, 0);
gdk_gc_set_rgb_fg_color(m_DrawingArea->style->fg_gc[GTK_WIDGET_STATE(m_DrawingArea)], &black);
}
}
void CSignalChannelDisplay::drawZeroLine()
{
//switch to dashed line
gdk_gc_set_line_attributes(m_DrawingArea->style->fg_gc[GTK_WIDGET_STATE(m_DrawingArea)], 1, GDK_LINE_ON_OFF_DASH, GDK_CAP_BUTT, GDK_JOIN_BEVEL);
const double sizePerChannel = m_Height / double(m_ChannelList.size());
//draw Y=0 line
if (m_MultiView)
{
const gint midPoint = gint(getSampleYMultiViewCoordinate(m_TranslateY[0]));
const gint zeroY = gint(getSampleYMultiViewCoordinate(0));
if (zeroY >= 0 && zeroY < gint(m_Height) && std::abs(midPoint - zeroY) < sizePerChannel / 2.0)
{
gdk_draw_line(m_DrawingArea->window, m_DrawingArea->style->fg_gc[GTK_WIDGET_STATE(m_DrawingArea)], 0, zeroY, m_Width, zeroY);
}
}
else
{
for (size_t k = m_FirstChannelToDisplay; k <= m_LastChannelToDisplay; ++k)
{
const gint midPoint = gint(getSampleYCoordinate(m_TranslateY[k], k));
const gint zeroY = gint(getSampleYCoordinate(0, k));
if (zeroY >= 0 && zeroY < gint(m_Height) && std::abs(midPoint - zeroY) < sizePerChannel / 2.0)
{
gdk_draw_line(m_DrawingArea->window, m_DrawingArea->style->fg_gc[GTK_WIDGET_STATE(m_DrawingArea)], 0, zeroY, m_Width, zeroY);
}
}
}
//switch back to normal line
gdk_gc_set_line_attributes(m_DrawingArea->style->fg_gc[GTK_WIDGET_STATE(m_DrawingArea)], 1, GDK_LINE_SOLID, GDK_CAP_BUTT, GDK_JOIN_BEVEL);
}
void CSignalChannelDisplay::updateLimits()
{
if (m_Height == 0)
{
// Bail out, no use setting the limits based on 0 ... this happens sometimes (todo: why?)
// display->m_FirstChannelToDisplay = 0;
// display->m_LastChannelToDisplay = display->m_ChannelList.size() - 1;
return;
}
if (m_MultiView)
{
m_FirstChannelToDisplay = 0;
m_LastChannelToDisplay = size_t(m_ChannelList.size() - 1);
// keep as is: display->m_StartY, display->m_StopY
return;
}
GtkWidget* widget = GTK_WIDGET(gtk_builder_get_object(m_ParentDisplayView->m_Builder, "SignalDisplayChannelsScrolledWindow"));
GtkAdjustment* vadj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(widget));
const double areaStartY = vadj->value;
const double areaSizeY = vadj->page_size;
const double sizePerChannel = m_Height / double(m_ChannelList.size()) * m_ZoomScaleY;
m_FirstChannelToDisplay = size_t(std::floor(areaStartY / sizePerChannel));
m_LastChannelToDisplay = std::min(size_t(m_ChannelList.size() - 1), size_t(m_FirstChannelToDisplay + std::floor(areaSizeY / sizePerChannel) + 1));
m_StartY = size_t(sizePerChannel * m_FirstChannelToDisplay);
m_StopY = std::min(m_Height, size_t(sizePerChannel * (m_LastChannelToDisplay + 1)));
#ifdef DEBUG
std::cout << "Requesting full redraw, Q (updated limits)\n";
#endif
#if SUPPORT_REDRAW
redrawAllAtNextRefresh(true);
#else
// gdk_window_clear(m_drawingArea->window);
// gtk_widget_queue_draw(static_cast<GtkWidget*>(m_drawingArea));
#endif
// std::cout << "SetChns " << display->m_FirstChannelToDisplay << " to " << display->m_LastChannelToDisplay << "\n";
// std::cout << "SetLim " << display->m_StartY << " to " << display->m_StopY << "\n";
}
//
//CALLBACKS
//
// DrawingArea visible region may have changed, estimate the limits again
gboolean VisibleRegionChangedCB(GtkWidget* /*pWidget*/, gpointer data)
{
auto* display = reinterpret_cast<CSignalChannelDisplay*>(data);
display->updateLimits();
return false; // propagate
}
gboolean DrawingAreaConfigureCB(GtkWidget* /*pWidget*/, GdkEventExpose* /*pEvent*/, gpointer data)
{
auto* display = reinterpret_cast<CSignalChannelDisplay*>(data);
display->updateLimits();
return false; // propagate
}
gboolean DrawingAreaExposeEventCB(GtkWidget* /*widget*/, GdkEventExpose* event, gpointer data)
{
// std::cout << "EE for " << pEvent->area.x << " " << pEvent->area.y << " " << pEvent->area.width << " " << pEvent->area.height << "\n";
auto* display = reinterpret_cast<CSignalChannelDisplay*>(data);
#if SUPPORT_REDRAW
//check if a full redrawn was asked for
if(pEvent->area.width == (gint)display->m_Width && pEvent->area.height == (gint)display->m_Height)
{
#ifdef DEBUG
std::cout << "Requesting full redraw, G (full window exposed)\n";
#endif
display->redrawAllAtNextRefresh(true);
}
#endif
//redraw signals
display->draw(event->area);
//don't propagate this signal to the children if any
return TRUE;
}
gboolean DrawingAreaResizeEventCB(GtkWidget* /*widget*/, GtkAllocation* allocation, gpointer data)
{
auto* display = reinterpret_cast<CSignalChannelDisplay*>(data);
display->onResizeEventCB(allocation->width, allocation->height);
display->updateLimits();
return FALSE;
}
void DrawingAreaClickedEventCB(GtkWidget* /*widget*/, GdkEventButton* event, gpointer data)
{
if (event->type != GDK_BUTTON_PRESS) { return; }
auto* display = reinterpret_cast<CSignalChannelDisplay*>(data);
bool zoomChanged = false;
display->m_CurrentSignalMode = GlobalBestFit;
if (event->button == 1)
{
display->m_CurrentSignalMode = ZoomIn;
display->computeZoom(true, event->x, event->y);
zoomChanged = true;
}
else if (event->button == 3)
{
if (display->m_ZoomScaleY != 1.0)
{
display->m_CurrentSignalMode = ZoomOut;
display->computeZoom(false, event->x, event->y);
zoomChanged = true;
if (display->m_ZoomScaleY == 1.0)
{
display->m_CurrentSignalMode = GlobalBestFit;
display->updateDisplayParameters();
}
else { display->m_CurrentSignalMode = ZoomOut; }
}
}
//if the zoom level has changed, redraw the signal and left ruler
if (zoomChanged)
{
display->redrawAllAtNextRefresh(true);
if (GTK_WIDGET(display->m_DrawingArea)->window) { gdk_window_invalidate_rect(GTK_WIDGET(display->m_DrawingArea)->window, nullptr, true); }
for (auto it = display->m_LeftRuler.begin(); it != display->m_LeftRuler.end(); ++it)
{
if (GTK_WIDGET(it->second->getWidget())->window) { gdk_window_invalidate_rect(GTK_WIDGET(it->second->getWidget())->window, nullptr, true); }
}
}
}
void DrawingAreaEnterEventCB(GtkWidget* widget, GdkEventCrossing* /*event*/, gpointer data)
{
auto* display = reinterpret_cast<CSignalChannelDisplay*>(data);
//change the cursor to the zooming one
gdk_window_set_cursor(widget->window, display->m_ParentDisplayView->m_Cursor[1]);
}
void DrawingAreaLeaveEventCB(GtkWidget* widget, GdkEventCrossing* /*event*/, gpointer data)
{
auto* display = reinterpret_cast<CSignalChannelDisplay*>(data);
//change the cursor back to the normal one
gdk_window_set_cursor(widget->window, display->m_ParentDisplayView->m_Cursor[0]);
}
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,267 @@
#pragma once
#include "../../ovp_defines.h"
#include "ovpCSignalDisplayLeftRuler.h"
#include <glib.h>
#include <gtk/gtk.h>
#include <openvibe/ov_all.h>
#include <map>
#include <vector>
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
class CSignalDisplayView;
class CBufferDatabase;
class CSignalChannelDisplay
{
public:
/**
* \brief Constructor
* \param displayView Parent view
* \param channelDisplayW Width to be requested by widget
* \param channelDisplayH Height to be requested by widget
* \param leftRulerW Width to be requested by left ruler
* \param leftRulerH Height to be requested by left ruler
*/
CSignalChannelDisplay(CSignalDisplayView* displayView, int channelDisplayW, int channelDisplayH, int leftRulerW, int leftRulerH);
/**
* \brief Destructor
*/
~CSignalChannelDisplay();
/**
* \brief Get ruler widget
* \return Pointer to ruler widget
*/
GtkWidget* getRulerWidget(size_t index) const;
/**
* \brief Get signal display widget
* \return Pointer to signal display widget
*/
GtkWidget* getSignalDisplayWidget() const { return m_DrawingArea; }
/**
* \brief Callback notified upon resize events
* \param width New window width
* \param height New window height
*/
void onResizeEventCB(gint width, gint height);
/**
* \brief Updates scale following a resize event or a time scale change
*/
void updateScale();
// Updates some drawing limits, i.e. to limit drawing to [chn_i,...,chn_j]
void updateLimits();
/**
* \brief Reset list of channels displayed by this object
*/
void resetChannelList();
/**
* \brief Add a channel to the list of channels to be displayed
* \param channel Index of channel to be displayed
*/
void addChannel(size_t channel);
void addChannelList(size_t channel);
/**
* \brief Get rectangle to clear and redraw based on latest signal data received
* \param[out] rect Rectangle holding part of drawing area to clear and update
*/
void getUpdateRectangle(GdkRectangle& rect) const;
/**
* \brief Flag widget so that its whole window is redrawn at next refresh
*/
void redrawAllAtNextRefresh(bool redraw);
/**
* \brief Check whether the whole window must be redrawn
* \return True if the whole window must be redrawn, false otherwise
*/
bool mustRedrawAll() const { return m_RedrawAll; }
/**
* \brief Draws the signal on the signal's drawing area.
* \param area Exposed area that needs to be redrawn
*/
void draw(const GdkRectangle& area);
/**
* \brief Clip signals to drawing area
* Computes the list of points used to draw the lines (m_ParentDisplayView->m_Points) using the raw points list
* (m_ParentDisplayView->m_RawPoints) and by cropping the lines when they go out of the window.
* \param pointCount Number of points to clip
* \return The number of points to display.
*/
uint64_t cropCurve(uint64_t pointCount) const;
/**
* \brief Computes the parameters necessary for the signal to be zoomed at the selected coordinates.
* \param zoomIn If true, the operation is a zoom In, if false it's a zoom Out.
* \param x The X-coordinate of the center of the area we want to zoom in.
* \param y The Y-coordinate of the center of the area we want to zoom in.
*/
void computeZoom(bool zoomIn, double x, double y);
/**
* \brief Returns empiric y min and maxes of the currently shown signal chunks for all subchannels.
* Note that the actually used display limits may be different. This function can be used
* to get the data extremal values and then use these to configure the display appropriately.
*/
void getDisplayedValueRange(std::vector<double>& min, std::vector<double>& max) const;
/*
* \brief Sets scale for all subchannels.
*/
void setGlobalScaleParameters(double min, double max, double margin);
/*
* \brief Sets scale for a single subchannel.
*/
void setLocalScaleParameters(size_t subChannelIdx, double min, double max, double margin);
/**
* \brief Updates signal scale and translation based on latest global range and margin
*/
void updateDisplayParameters();
private:
/**
* \brief Get first buffer to display index and position and first sample to display index
* \param[out] bufferIdx Index of first buffer to display
* \param[out] sampleIdx Index of first sample to display
* \param[out] bufferPos Position of first buffer to display (0-based, from left edge)
*/
void getFirstBufferToDisplayInformation(size_t& bufferIdx, size_t& sampleIdx, size_t& bufferPos) const;
/**
* \brief Get start X coord of a buffer
* \param pos Position of buffer on screen (0-based, from left edge)
* \return Floored X coordinate of buffer
*/
int getBufferStartX(size_t pos) const;
/**
* \brief Get X coordinate of a sample
* \param bufferPos Position of buffer on screen (0-based, from left edge)
* \param sampleIdx Index of sample in buffer
* \param offset X offset from which to start drawing. Used in scroll mode only.
* \return X coordinate of sample
*/
double getSampleXCoordinate(size_t bufferPos, size_t sampleIdx, double offset) const;
/**
* \brief Get Y coordinate of a sample
* \param value Sample value and index of channel
* \param channelIdx Index of channel
* \return Y coordinate of sample
*/
double getSampleYCoordinate(double value, size_t channelIdx);
/**
* \brief Get Y coordinate of a sample in Multiview mode
* \param value Sample value and index of channel
* \return Y coordinate of sample
*/
double getSampleYMultiViewCoordinate(double value);
/**
* \brief Draw signals (and stimulations, if any) displayed by this channel
* \param firstBuffer Index of first buffer to display
* \param lastBuffer Index of last buffer to display
* \param firstSample Index of first sample to display in first buffer (subsequent buffers will start at sample 0)
* \param startX Start X Coordinate
* \param firstChannel Index of first channel to display
* \param lastChannel Index of last channel to display
* \return True if all went ok, false otherwise
*/
bool drawSignals(size_t firstBuffer, size_t lastBuffer, size_t firstSample, double startX, size_t firstChannel, size_t lastChannel);
/**
* \brief Draw vertical line highlighting where data was last drawn
*/
void drawProgressLine(size_t firstBufferIdx, size_t firstBufferPos) const;
/**
* \brief Draw Y=0 line
*/
void drawZeroLine();
public:
//! Vector of Left rulers displaying signal scale. Indexed by channel id. @note This is a map as the active number of channels
// may change by the toolbar whereas this total set of rulers doesn't...
std::map<size_t, CSignalDisplayLeftRuler*> m_LeftRuler;
//! The drawing area where the signal is to be drawn
GtkWidget* m_DrawingArea = nullptr;
//! Drawing area dimensions, in pixels
size_t m_Width = 0, m_Height = 0;
//! Available width per buffer, in pixels
double m_WidthPerBuffer = 0;
//! Available width per sample point, in pixels
double m_PointStep = 0;
//! The index list of the channels to display
std::vector<size_t> m_ChannelList;
//! The "parent" view (which uses this widget)
CSignalDisplayView* m_ParentDisplayView = nullptr;
//! The database from which the information are to be read
CBufferDatabase* m_Database = nullptr;
/** \ name Extrema of displayed values for all channel in this display */
//@{
std::vector<double> m_LocalMaximum, m_LocalMinimum;
//@}
/** \name Auto scaling parameters */
//@{
// double m_ScaleX, m_TranslateX;
std::vector<double> m_ScaleY, m_TranslateY;
//@}
/** \name Zooming parameters (user controlled) */
//@{
double m_ZoomTranslateX = 0, m_ZoomTranslateY = 0, m_ZoomScaleX = 1, m_ZoomScaleY = 1;
//! The zoom factor step
const double m_ZoomFactor = 1.5;
//@}
/** \name Scale margin parameters */
//@{
std::vector<double> m_OuterTopMargin, m_InnerTopMargin, m_InnerBottomMargin, m_OuterBottomMargin;
//@}
size_t m_LeftRulerW = 0, m_LeftRulerH = 0;
//! Current signal display mode
EDisplayMode m_CurrentSignalMode = GlobalBestFit;
//! Time of latest displayed data
uint64_t m_LatestDisplayedTime = 0;
//! Should the whole window be redrawn at next redraw?
bool m_RedrawAll = false;
//! Is it a multiview display ?
bool m_MultiView = false;
// These parameters control that we don't unnecessarily draw parts of the signal which are not in view
// Currently visible y segment in the drawing area
size_t m_StartY = 0, m_StopY = 0;
// First and last channel to draw
size_t m_FirstChannelToDisplay = 0, m_LastChannelToDisplay = 0;
};
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,137 @@
#include "ovpCSignalDisplayLeftRuler.h"
#include <cmath>
#include <glib.h>
#include <glib/gprintf.h>
#include <iostream>
#include <sstream>
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
#define CONVERT_TIME(i) (double((i)>>32) + double(double((i)&0xFFFFFFFF) / double((uint64_t)1<<32)))
//! Callback to redraw the bottom ruler
static gboolean LeftRulerExposeEventCallback(GtkWidget* /*widget*/, GdkEventExpose* /*event*/, gpointer data)
{
//redraw the ruler
auto* leftRuler = reinterpret_cast<CSignalDisplayLeftRuler*>(data);
leftRuler->draw();
//don't propagate this signal to the children if any
return TRUE;
}
CSignalDisplayLeftRuler::CSignalDisplayLeftRuler(const int width, const int height) : m_Width(width)
{
//creates the main drawing area
m_Ruler = gtk_drawing_area_new();
gtk_widget_set_size_request(m_Ruler, width, height);
g_signal_connect_after(G_OBJECT(m_Ruler), "expose_event", G_CALLBACK(LeftRulerExposeEventCallback), this);
//get left ruler widget's font description
PangoContext* ctx = gtk_widget_get_pango_context(m_Ruler);
PangoFontDescription* desc = pango_context_get_font_description(ctx);
//adapt the allocated height per label to the font's height (plus 4 pixel to add some spacing)
if (pango_font_description_get_size_is_absolute(desc)) { m_PixelsPerLabel = pango_font_description_get_size(desc) + 4; }
else { m_PixelsPerLabel = pango_font_description_get_size(desc) / PANGO_SCALE + 4; }
}
void CSignalDisplayLeftRuler::update(const double min, const double max)
{
m_MaxDisplayedValue = max;
m_MinDisplayedValue = min;
//redraw the ruler
if (m_Ruler->window) { gdk_window_invalidate_rect(m_Ruler->window, nullptr, true); }
}
void CSignalDisplayLeftRuler::draw() const
{
if (!GTK_WIDGET_VISIBLE(m_Ruler)) { return; }
gint width, height;
gdk_drawable_get_size(m_Ruler->window, &width, &height);
//draw ruler base (vertical line)
gdk_draw_line(m_Ruler->window, m_Ruler->style->fg_gc[GTK_WIDGET_STATE(m_Ruler)], width - 1, 0, width - 1, height);
//computes the step in values for the ruler
const double intervalWidth = m_MaxDisplayedValue - m_MinDisplayedValue;
double valueStep = 0;
double baseValue = 0;
//if the signal is not constant
if (intervalWidth > 0)
{
//computes the step
const auto nearestSmallerPowerOf10 = double(pow(10, floor(log10(intervalWidth))));
//get max number of labels that fit in widget
const size_t maxNLabels = size_t(height / m_PixelsPerLabel);
//ensure there is room for at least one label
if (maxNLabels > 0)
{
//get the current number of labels to display based on the nearest inferior power of ten value
const size_t tempNLabels = size_t(floor(intervalWidth / nearestSmallerPowerOf10));
if (tempNLabels > 2 * maxNLabels) { valueStep = 4 * nearestSmallerPowerOf10; }
else if (tempNLabels > maxNLabels) { valueStep = 2 * nearestSmallerPowerOf10; }
else if (tempNLabels < (maxNLabels / 4)) { valueStep = nearestSmallerPowerOf10 / 4; }
else if (tempNLabels < (maxNLabels / 2)) { valueStep = nearestSmallerPowerOf10 / 2; }
else { valueStep = nearestSmallerPowerOf10; }
//recompute base value of the step
baseValue = valueStep * floor(m_MinDisplayedValue / valueStep);
}
}
else
{
valueStep = 1;
baseValue = floor(m_MinDisplayedValue - 0.5);
}
int textW;
int textH;
//if the step is too small, it causes problems, so don't display anything and return
if (valueStep < 0.5e-5) { return; }
double i = baseValue;
while (i < double(0.5 + m_MaxDisplayedValue))
{
//computes the coordinate of the current label
const gint textY = gint((m_MaxDisplayedValue - i) * (height / intervalWidth));
if (textY >= 0 && textY <= height)
{
gchar value[40];
//if the current value is (almost) 0, displays 0
abs(i) < 0.5e-10 ? g_sprintf(value, "0") : g_sprintf(value, "%g", i);
PangoLayout* text = gtk_widget_create_pango_layout(m_Ruler, value);
pango_layout_set_width(text, 28);
pango_layout_set_justify(text, PANGO_ALIGN_RIGHT);
pango_layout_get_pixel_size(text, &textW, &textH);
gdk_draw_layout(m_Ruler->window, m_Ruler->style->fg_gc[GTK_WIDGET_STATE(m_Ruler)], 0, textY - (textH / 2), text);
if (i < 0.5e-10 && i > -0.5e-10)
{
gdk_draw_line(m_Ruler->window, m_Ruler->style->fg_gc[GTK_WIDGET_STATE(m_Ruler)], width - 6, textY, width, textY);
}
else { gdk_draw_line(m_Ruler->window, m_Ruler->style->fg_gc[GTK_WIDGET_STATE(m_Ruler)], width - 4, textY, width, textY); }
}
i += valueStep;
}
}
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,52 @@
#pragma once
#include <openvibe/ov_all.h>
#include <gtk/gtk.h>
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
/**
* Used to display a vertical "amplitude" ruler.
* */
class CSignalDisplayLeftRuler
{
public:
/**
* \brief Constructor
* \param width Width to be requested by widget
* \param height Height to be requested by widget
*/
CSignalDisplayLeftRuler(int width, int height);
// CSignalChannelDisplay* pParentChannelDisplay);
/**
* \brief Destructor
*/
~CSignalDisplayLeftRuler() = default;
/*
* \brief Update ruler with latest min/max values and request a redraw
* \param min Minimum value to be displayed
* \param max Maximum value to be displayed
*/
void update(double min, double max);
//! returns the widget, so it can be added to the main interface
GtkWidget* getWidget() const { return m_Ruler; }
/**
* \brief Draws the ruler by using the information from the database.
*/
void draw() const;
GtkWidget* m_Ruler = nullptr;
int m_Width = 0;
double m_MaxDisplayedValue = -DBL_MAX;
double m_MinDisplayedValue = DBL_MAX;
uint64_t m_PixelsPerLabel = 10;
// CSignalChannelDisplay* m_pParentChannelDisplay = nullptr;
};
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,259 @@
#pragma once
#include <openvibe/ov_all.h>
#include "ovpCSignalChannelDisplay.h"
#include "../../ovpCBufferDatabase.h"
#include "../../ovpCBottomTimeRuler.h"
#include <gtk/gtk.h>
#include <map>
#include <string>
#include <vector>
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
/**
* This class contains everything necessary to setup a GTK window and display
* a signal thanks to a CBufferDatabase's information.
*/
class CSignalDisplayView : public CSignalDisplayDrawable
{
public:
/**
* \brief Constructor
* \param [in] buffer Signal database
* \param [in] timeScale Initial time scale value
* \param [in] displayMode Initial signal display mode
* \param [in] scalingMode Initial signal scaling mode
* \param [in] verticalScale Initial vertical scale value
* \param [in] verticalOffset Initial vertical offset value
* \param [in] timeScale Initial time scale value
* \param [in] horizontalRuler Initial horizontal ruler activation
* \param [in] verticalRuler Initial vertical ruler activation
* \param [in] multiview Initial multiview ruler activation
*/
CSignalDisplayView(CBufferDatabase& buffer, const CIdentifier& displayMode, const CIdentifier& scalingMode,
double verticalScale, double verticalOffset, double timeScale, bool horizontalRuler, bool verticalRuler, bool multiview);
/**
* \brief Base constructor
* \param [in] buffer Signal database
* \param [in] timeScale Initial time scale value
* \param [in] displayMode Initial signal display mode
*/
void construct(CBufferDatabase& buffer, double timeScale, const CIdentifier& displayMode);
/**
* \brief Destructor
*/
~CSignalDisplayView() override;
/**
* \brief Get pointers to plugin main widget and (optional) toolbar widget
* \param [out] widget Pointer to main widget
* \param [out] toolbar Pointer to (optional) toolbar widget
*/
void getWidgets(GtkWidget*& widget, GtkWidget*& toolbar) const;
/**
* Initializes the window.
*/
void init() override;
/**
* Invalidates the window's content and tells it to redraw itself.
*/
void redraw() override;
/**
* Toggle left rulers on/off
* \param active Show rulers if true.
*/
void toggleLeftRulers(bool active);
/**
* Toggle time ruler on/off
* \param active Show the ruler if true.
*/
void toggleBottomRuler(bool active);
/**
* Toggle a channel on/off
* \param index The index of the channel to toggle.
* \param active Show the channel if true.
*/
void toggleChannel(size_t index, bool active);
void toggleChannelMultiView(bool active);
void changeMultiView();
void removeOldWidgets();
void recreateWidgets(size_t nChannel);
void updateMainTableStatus();
void updateDisplayTableSize() const;
void activateToolbarButtons(bool active) const;
/**
* Callback called when display mode changes
* \param mode New display mode
* \return True
*/
bool onDisplayModeToggledCB(const CIdentifier& mode);
bool onUnitsToggledCB(bool active);
/**
* Callback called when vertical scale mode changes
* \param button Radio button toggled
* \return True
*/
bool onVerticalScaleModeToggledCB(GtkToggleButton* button);
/**
* Callback called when custom vertical scale is changed
* \param button Custom vertical scale widget
* \return True if custom vertical scale value could be retrieved, false otherwise
*/
bool onCustomVerticalScaleChangedCB(GtkSpinButton* button);
bool onCustomVerticalOffsetChangedCB(GtkSpinButton* button);
/**
* \brief Get a channel display object
* \param index Index of channel display
* \return Channel display object
*/
CSignalChannelDisplay* getChannelDisplay(size_t index);
bool isChannelDisplayVisible(size_t index);
void onStimulationReceivedCB(uint64_t id, const CString& name);
bool setChannelUnits(const std::vector<std::pair<CString, CString>>& channelUnits);
/**
* \brief Get a color from a stimulation code
* \remarks Only the lower 32 bits of the stimulation code are currently used to compute the color.
* \param[in] id Stimulation code
* \param[out] color Color computed from stimulation code
*/
void getStimulationColor(uint64_t id, GdkColor& color);
/**
* \brief Get a color from a signal
* \param[in] index channel index
* \param[out] color Color computed from stimulation code
*/
void getMultiViewColor(size_t index, GdkColor& color);
void refreshScale();
private:
/**
* \brief Update stimulations color dialog with a new (stimulation, color) pair
* \param[in] stimulation Stimulation label
* \param[in] color Stimulation color
*/
void updateStimulationColorsDialog(const CString& stimulation, const GdkColor& color) const;
public:
//! The Builder handler used to create the interface
GtkBuilder* m_Builder = nullptr;
//! The table containing the CSignalChannelDisplays
GtkWidget* m_SignalDisplayTable = nullptr;
GtkWidget* m_Separator = nullptr;
//! Array of the channel's labels
std::vector<GtkWidget*> m_ChannelLabel;
//! Array of CSignalChannelDisplays (one per channel, displays the corresponding channel)
std::vector<CSignalChannelDisplay*> m_ChannelDisplay;
std::map<size_t, GtkWidget*> m_Separators;
//! Show left rulers when true
bool m_ShowLeftRulers = false;
//!Show bottom time ruler when true
bool m_ShowBottomRuler = false;
//! Time of displayed signals at the left of channel displays
uint64_t m_LeftmostDisplayedTime = 0;
//! Largest displayed value range, to be matched by all channels in global best fit mode
double m_LargestDisplayedValueRange = 0;
double m_LargestDisplayedValue = 0;
double m_SmallestDisplayedValue = 0;
//! Current value range margin, used to avoid redrawing signals every time the largest value range changes
double m_ValueRangeMargin = 0;
// double m_ValueMaxMargin;
/*! Margins added to largest and subtracted from smallest displayed values are computed as :
m_MarginFactor * m_LargestDisplayedValueRange. If m_MarginFactor = 0, there's no margin at all.
If factor is 0.1, largest displayed value range is extended by 10% above and below its extremums at the time
when margins are computed. */
double m_MarginFactor = 0.4;
//! Normal/zooming cursors
std::array<GdkCursor*, 2> m_Cursor;
/** \name Vertical scale */
//@{
//! Flag set to true when you'd like the display to *check* if the scale needs to change and possibly update
bool m_VerticalScaleRefresh = true;
//! Flag set to true when you'd like the display to update in any case
bool m_VerticalScaleForceUpdate = false;
//! Value of custom vertical scale
double m_CustomVerticalScaleValue = 0;
//! Value of custom vertical offset
double m_CustomVerticalOffset = 0;
//@}
//! The database that contains the information to use to draw the signals
CBufferDatabase* m_Buffer = nullptr;
//! Vector of gdk points. Used to draw the signals.
std::vector<GdkPoint> m_Points;
//! Vector of raw points. Stores the points' coordinates before cropping.
std::vector<std::pair<double, double>> m_RawPoints;
std::vector<CString> m_ChannelName;
//! Vector of indexes of the channels to display
std::map<size_t, bool> m_SelectedChannels;
size_t m_NSelectedChannel = 0;
std::map<size_t, std::pair<CString, CString>> m_ChannelUnits;
//! Flag set to true once multi view configuration dialog is initialized
bool m_MultiViewEnabled = false;
//! Vector of indices of selected channels
std::map<size_t, bool> m_MultiViewSelectedChannels;
//Map of stimulation codes received so far, and their corresponding name and color
std::map<uint64_t, std::pair<CString, GdkColor>> m_Stimulations;
//Map of signal indices received so far, and their corresponding name and color
std::map<uint64_t, std::pair<CString, GdkColor>> m_Signals;
//! Bottom box containing bottom ruler
GtkBox* m_BottomBox = nullptr;
//! Bottom time ruler
CBottomTimeRuler* m_BottomRuler = nullptr;
//! Widgets for left rulers
std::vector<GtkWidget*> m_LeftRulers;
CIdentifier m_ScalingMode = CIdentifier::undefined();
static const std::vector<std::string> SCALING_MODES;
std::vector<CString> m_ErrorState;
bool m_StimulationColorsShown = false;
};
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,167 @@
#include "ovpCBottomTimeRuler.h"
#include <cmath>
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
#define CONVERT_TIME(i) (double((i)>>32) + double(double((i)&0xFFFFFFFF) / double((uint64_t)1<<32)))
gboolean BottomRulerExposeEventCB(GtkWidget* /*widget*/, GdkEventExpose* /*event*/, gpointer data)
{
//redraw the ruler
auto* bottomRuler = reinterpret_cast<CBottomTimeRuler*>(data);
bottomRuler->draw();
//don't propagate this signal to the children if any
return TRUE;
}
gboolean ResizeBottomRulerCB(GtkWidget* /*widget*/, GtkAllocation* allocation, gpointer data)
{
auto* bottomRuler = reinterpret_cast<CBottomTimeRuler*>(data);
bottomRuler->onResizeEventCB(allocation->width, allocation->height);
return FALSE;
}
CBottomTimeRuler::CBottomTimeRuler(CBufferDatabase& database, const int width, const int height)
: m_database(&database), m_height(height)
{
//creates the main drawing area
m_bottomRuler = gtk_drawing_area_new();
gtk_widget_set_size_request(m_bottomRuler, width, height);
g_signal_connect_after(G_OBJECT(m_bottomRuler), "expose_event", G_CALLBACK(BottomRulerExposeEventCB), this);
}
void CBottomTimeRuler::draw()
{
//if the widget is invisible, no need to redraw it
if (!GTK_WIDGET_VISIBLE(m_bottomRuler)) { return; }
//gets the number of buffers to display
const uint64_t nBufferToDisplay = m_database->m_NBufferToDisplay;
if (m_database->m_DimSizes[1] == 1 && nBufferToDisplay != 1) { /* nBufferToDisplay--;*/ }
//gets the widget's size
gint bottomRulerWidth;
gint bottomRulerHeight;
gdk_drawable_get_size(m_bottomRuler->window, &bottomRulerWidth, &bottomRulerHeight);
//in ms
const double intervalWidth = CONVERT_TIME(nBufferToDisplay * m_database->m_BufferDuration);
//if(m_Database->areEpochsContiguous() == true){intervalWidth = CONVERT_TIME(nBufferToDisplay * m_Database->m_BufferDuration);}
//else { intervalWidth = CONVERT_TIME(nBufferToDisplay * m_Database->m_BufferDuration); }
//available width per buffer
const double widthPerBuffer = double(bottomRulerWidth) / double(nBufferToDisplay);
//computes the step of the values displayed on the ruler
const auto nearestSmallerPowerOf10 = double(pow(10, floor(log10(intervalWidth))));
const auto maxNumberOfLabels = uint64_t(bottomRulerWidth / m_pixelsPerLabel);
double valueStep = nearestSmallerPowerOf10;
if (uint64_t(floor(intervalWidth / nearestSmallerPowerOf10)) > maxNumberOfLabels) { valueStep = 2 * nearestSmallerPowerOf10; }
else if (uint64_t(floor(intervalWidth / nearestSmallerPowerOf10)) < maxNumberOfLabels / 2) { valueStep = nearestSmallerPowerOf10 / 2; }
if (m_database->getDisplayMode() == Scroll)
{
//compute start, end time and base value of the step
double startTime = 0;
if (!m_database->m_StartTime.empty()) { startTime = CONVERT_TIME(m_database->m_StartTime[0]); }
const double endTime = startTime + intervalWidth;
const double baseValue = valueStep * floor(startTime / valueStep);
//X position of the first label (if there are less buffers than needed)
auto baseX = int64_t(floor(bottomRulerWidth - (m_database->m_SampleBuffers.size() * widthPerBuffer)));
if (baseX < 0) { baseX = 0; }
//draw ruler base (horizontal line)
gdk_draw_line(m_bottomRuler->window, m_bottomRuler->style->fg_gc[GTK_WIDGET_STATE(m_bottomRuler)], gint(baseX), 0, gint(bottomRulerWidth), 0);
const int clipLeft = 0;
const int clipRight = bottomRulerWidth - 1;
drawRuler(baseX, bottomRulerWidth, startTime, endTime, intervalWidth, baseValue, valueStep, clipLeft, clipRight);
}
else //scan mode
{
//draw ruler base (horizontal line)
gdk_draw_line(m_bottomRuler->window, m_bottomRuler->style->fg_gc[GTK_WIDGET_STATE(m_bottomRuler)], 0, 0, gint(bottomRulerWidth), 0);
//left part of the ruler (recent data)
size_t leftmostBufferToDisplay = 0;
m_database->getIndexOfBufferStartingAtTime(m_leftmostDisplayedTime, leftmostBufferToDisplay);
double startTime = 0;
if (!m_database->m_StartTime.empty()) { startTime = CONVERT_TIME(m_leftmostDisplayedTime); }
double endTime = startTime + intervalWidth;
double baseValue = valueStep * floor(startTime / valueStep);
int clipLeft = 0;
int clipRight = int(double(m_database->m_NBufferToDisplay - leftmostBufferToDisplay) * widthPerBuffer);
drawRuler(0, bottomRulerWidth, startTime, endTime, intervalWidth, baseValue, valueStep, clipLeft, clipRight);
//right part (older data)
startTime -= intervalWidth;
endTime = startTime + intervalWidth;
baseValue = valueStep * floor(startTime / valueStep);
clipLeft = clipRight + 1;
clipRight = bottomRulerWidth - 1;
drawRuler(0, bottomRulerWidth, startTime, endTime, intervalWidth, baseValue, valueStep, clipLeft, clipRight);
}
}
void CBottomTimeRuler::onResizeEventCB(const gint width, gint /*height*/) const { gtk_widget_set_size_request(m_bottomRuler, width, m_height); }
void CBottomTimeRuler::drawRuler(const int64_t baseX, const int rulerWidth, const double startTime, const double endTime, const double length,
const double baseValue, const double valueStep, const int clipLeft, const int clipRight)
{
for (double i = baseValue; i < double(0.5 + endTime); i += valueStep)
{
//compute the position of the label
const gint textX = gint(baseX + ((i - startTime) * ((double(rulerWidth)) / length)));
//is text clipped?
if (textX < clipLeft) { continue; }
std::string timeLabel = std::to_string(i);
PangoLayout* text = gtk_widget_create_pango_layout(m_bottomRuler, timeLabel.c_str());
int textWidth;
pango_layout_get_pixel_size(text, &textWidth, nullptr);
//is text beyond visible range?
if (textX + textWidth > clipRight)
{
g_object_unref(text);
break;
}
//if the width allocated per label becomes too small compared to the effective width of the label
if (uint64_t(textWidth) >= m_pixelsPerLabel - 20)
{
//increases the allocated width per label
m_pixelsPerLabel = textWidth + 30;
}
//display it
gdk_draw_layout(m_bottomRuler->window, m_bottomRuler->style->fg_gc[GTK_WIDGET_STATE(m_bottomRuler)], textX, 4, text);
//draw a small line above it
gdk_draw_line(m_bottomRuler->window, m_bottomRuler->style->fg_gc[GTK_WIDGET_STATE(m_bottomRuler)], textX, 0, textX, 3);
g_object_unref(text);
}
}
void CBottomTimeRuler::linkWidthToWidget(GtkWidget* widget)
{
//adds a callback to the widget for the size-allocate signal
g_signal_connect(G_OBJECT(widget), "size-allocate", G_CALLBACK(ResizeBottomRulerCB), this);
}
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,65 @@
#pragma once
#include "ovpCBufferDatabase.h"
#include <gtk/gtk.h>
#include <openvibe/ov_all.h>
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
class CBufferDatabase;
/**
* Used to display an horizontal temporal ruler.
* Uses information fetched from a signal database object.
* \param database Object from which to fetch time data
* \param width Width to be requested by widget
* \param height Height to be requested by widget
*/
class CBottomTimeRuler
{
public:
CBottomTimeRuler(CBufferDatabase& database, int width, int height);
~CBottomTimeRuler() = default;
//! returns the widget, so it can be added to the main interface
GtkWidget* getWidget() const { return m_bottomRuler; }
//! draws the ruler
void draw();
/**
* \brief Resize this ruler when the widget passed in parameter is resized
* \param widget Widget whose width is matched by this widget
*/
void linkWidthToWidget(GtkWidget* widget);
//! in scan mode, leftmost displayed time is not always the one of the oldest buffer
void setLeftmostDisplayedTime(const uint64_t time) { m_leftmostDisplayedTime = time; }
/**
* \brief Callback notified upon resize events
* \param width New window width
* \param height New window height
*/
void onResizeEventCB(gint width, gint height) const;
private:
//! Draw ruler
void drawRuler(int64_t baseX, int rulerWidth, double startTime, double endTime, double length, double baseValue, double valueStep, int clipLeft,
int clipRight);
//! Gtk widget
GtkWidget* m_bottomRuler = nullptr;
//! Signal database from which time information is retrieved
CBufferDatabase* m_database = nullptr;
//! Height request
int m_height = 0;
//! Space allocated per label
uint64_t m_pixelsPerLabel = 20;
//! When in scan mode, current leftmost displayed time
uint64_t m_leftmostDisplayedTime = 0;
};
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,663 @@
#include "ovpCBufferDatabase.h"
#include <cmath>
#include <cstring>
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
CBufferDatabase::CBufferDatabase(Toolkit::TBoxAlgorithm<IBoxAlgorithm>& parent)
: m_ParentPlugin(parent)
{
m_decoder = &m_ParentPlugin.getAlgorithmManager().getAlgorithm(
m_ParentPlugin.getAlgorithmManager().createAlgorithm(OVP_GD_ClassId_Algorithm_ChannelLocalisationDecoder));
m_decoder->initialize();
m_DimSizes[0] = m_DimSizes[1] = 0;
}
CBufferDatabase::~CBufferDatabase()
{
m_decoder->uninitialize();
m_ParentPlugin.getAlgorithmManager().releaseAlgorithm(*m_decoder);
//delete all the remaining buffers
while (!m_SampleBuffers.empty())
{
delete[] m_SampleBuffers.front();
m_SampleBuffers.pop_front();
}
//delete channel localisation matrices
while (!m_channelLocalisationCoords.empty())
{
delete m_channelLocalisationCoords.front().first;
m_channelLocalisationCoords.pop_front();
}
/*while(m_oChannelLocalisationAlternateCoords.size() > 0)
{
delete[] m_oChannelLocalisationAlternateCoords.front().first;
m_oChannelLocalisationAlternateCoords.pop_front();
}*/
}
bool CBufferDatabase::decodeChannelLocalisationMemoryBuffer(const IMemoryBuffer* buffer, uint64_t startTime, uint64_t endTime)
{
//feed memory buffer to decoder
m_decoder->getInputParameter(OVP_GD_Algorithm_ChannelLocalisationDecoder_InputParameterId_MemoryBufferToDecode)->setReferenceTarget(&buffer);
//process buffer
m_decoder->process();
//copy header if needed
if (m_decoder->isOutputTriggerActive(OVP_GD_Algorithm_ChannelLocalisationDecoder_OutputTriggerId_ReceivedHeader))
{
//retrieve matrix header
Kernel::TParameterHandler<CMatrix*> matrix;
matrix.initialize(m_decoder->getOutputParameter(OVP_GD_Algorithm_ChannelLocalisationDecoder_OutputParameterId_Matrix));
//copy channel labels
m_channelLocalisationLabels.resize(matrix->getDimensionSize(0));
for (std::vector<CString>::size_type i = 0; i < m_channelLocalisationLabels.size(); ++i)
{
m_channelLocalisationLabels[i] = matrix->getDimensionLabel(0, i);
}
//retrieve dynamic flag
Kernel::TParameterHandler<bool> dynamic;
dynamic.initialize(m_decoder->getOutputParameter(OVP_GD_Algorithm_ChannelLocalisationDecoder_OutputParameterId_Dynamic));
m_dynamicChannelLocalisation = dynamic;
if (matrix->getDimensionSize(1) == 3)
{
m_cartesianCoords = true;
/*m_pChannelLocalisationCartesianCoords = &m_channelLocalisationStreamedCoords;
m_pChannelLocalisationSphericalCoords = &m_oChannelLocalisationAlternateCoords;*/
}
else if (matrix->getDimensionSize(1) == 2)
{
m_cartesianCoords = false;
/*m_pChannelLocalisationCartesianCoords = &m_oChannelLocalisationAlternateCoords;
m_pChannelLocalisationSphericalCoords = &m_channelLocalisationStreamedCoords;*/
}
else
{
m_ParentPlugin.getLogManager() << Kernel::LogLevel_Error <<
"Wrong size found for dimension 1 of Channel localisation header! Can't process header!\n";
return false;
}
//header information received
m_channelLocalisationHeaderReceived = true;
}
//has a chanloc buffer been received?
if (m_decoder->isOutputTriggerActive(OVP_GD_Algorithm_ChannelLocalisationDecoder_OutputTriggerId_ReceivedBuffer))
{
//number of buffers required to cover displayed time range
uint64_t maxBufferCount = 1;
//resize channel localisation queue if necessary
if (m_dynamicChannelLocalisation)
{
const uint64_t bufferDuration = endTime - startTime;
if (bufferDuration != 0)
{
maxBufferCount = uint64_t(ceil(m_TotalDuration / bufferDuration));
if (maxBufferCount == 0) { maxBufferCount = 1; }
}
//if new number of buffers decreased, resize list and destroy useless buffers
while (m_channelLocalisationCoords.size() > maxBufferCount)
{
delete[] m_channelLocalisationCoords.front().first;
m_channelLocalisationCoords.pop_front();
// delete[] m_oChannelLocalisationAlternateCoords.front().first;
// m_oChannelLocalisationAlternateCoords.pop_front();
m_channelLocalisationTimes.pop_front();
}
}
//retrieve coordinates matrix
Kernel::TParameterHandler<CMatrix*> matrix;
matrix.initialize(m_decoder->getOutputParameter(OVP_GD_Algorithm_ChannelLocalisationDecoder_OutputParameterId_Matrix));
//get pointer to destination matrix
CMatrix* channelLocalisation;
//CMatrix* alternateChannelLocalisation = nullptr;
if (m_channelLocalisationCoords.size() < maxBufferCount)
{
//create a new matrix and resize it
channelLocalisation = new CMatrix();
channelLocalisation->copyDescription(*matrix);
// alternateChannelLocalisation = new CMatrix();
// TODO : resize it appropriately depending on whether it is spherical or cartesian
}
else //m_channelLocalisationStreamedCoords.size() == maxBufferCount
{
channelLocalisation = m_channelLocalisationCoords.front().first;
m_channelLocalisationCoords.pop_front();
// alternateChannelLocalisation = m_oChannelLocalisationAlternateCoords.front().first;
// m_oChannelLocalisationAlternateCoords.pop_front();
m_channelLocalisationTimes.pop_front();
}
if (channelLocalisation)
{
//copy coordinates and times
channelLocalisation->copyContent(*matrix);
m_channelLocalisationCoords.emplace_back(channelLocalisation, true);
//m_oChannelLocalisationAlternateCoords.push_back(std::pair<CMatrix*, bool>(alternateChannelLocalisation, true));
m_channelLocalisationTimes.emplace_back(startTime, endTime);
}
}
return true;
}
bool CBufferDatabase::onChannelLocalisationBufferReceived(const size_t index)
{
m_channelLocalisationCoords[index].second = false;
return true;
}
bool CBufferDatabase::isFirstChannelLocalisationBufferProcessed()
{
//at least one chanloc buffer must have been received and processed
return (!m_channelLocalisationCoords.empty()) && (!m_channelLocalisationCoords[0].second);
}
bool CBufferDatabase::adjustNumberOfDisplayedBuffers(const double time)
{
bool change = false;
if (time > 0)
{
m_TotalDuration = time;
m_TotalDurationOV = 0;
m_TotalStep = 0;
}
//return if buffer length is not known yet
if (m_DimSizes[1] == 0) { return false; }
size_t newN = size_t(ceil((m_TotalDuration * m_Sampling) / m_DimSizes[1]));
//displays at least one buffer
newN = (newN == 0) ? 1 : newN;
if (newN != m_NBufferToDisplay || time <= 0)
{
m_NBufferToDisplay = newN;
change = true;
//if new number of buffers decreased, resize lists and destroy useless buffers
while (m_NBufferToDisplay < m_SampleBuffers.size())
{
delete[] m_SampleBuffers.front();
m_SampleBuffers.pop_front();
m_StartTime.pop_front();
m_EndTime.pop_front();
//suppress the corresponding minmax values
for (size_t c = 0; c < m_DimSizes[0]; ++c) { m_LocalMinMaxValue[c].pop_front(); }
}
}
return change;
}
void CBufferDatabase::setMatrixDimensionCount(const size_t count)
{
if (count != 2)
{
m_Error = true;
m_ParentPlugin.getBoxAlgorithmContext()->getPlayerContext()->getLogManager() << Kernel::LogLevel_Error << "Caller tried to set a " << count <<
"-dimensional matrix. Only 2-dimensional matrices are supported (e.g. [rows X cols]).\n";
}
if (count == 1)
{
m_ParentPlugin.getBoxAlgorithmContext()->getPlayerContext()->getLogManager() << Kernel::LogLevel_Error <<
"Note: For 1-dimensional matrices, you may try Matrix Transpose box to upgrade the stream to [N X 1] first.\n";
}
}
void CBufferDatabase::setMatrixDimensionSize(const size_t index, const size_t size)
{
if (index >= 2)
{
m_Error = true;
m_ParentPlugin.getBoxAlgorithmContext()->getPlayerContext()->getLogManager() << Kernel::LogLevel_Error << "Tried to access dimension " << index <<
", only 0 and 1 supported\n";
return;
}
if (m_DimSizes[index] != 0 && m_DimSizes[index] != size)
{
m_ParentPlugin.getBoxAlgorithmContext()->getPlayerContext()->getLogManager() << Kernel::LogLevel_Error <<
"Upstream tried to change the data chunk size after the first header, this is not supported.\n";
m_Error = true;
return;
}
m_DimSizes[index] = size;
m_DimLabels[index].resize(size);
if (index == 0)
{
m_NElectrodes = m_DimSizes[index];
//resize min/max values vector
m_LocalMinMaxValue.resize(size_t(m_NElectrodes));
}
}
void CBufferDatabase::setMatrixDimensionLabel(const size_t idx1, const size_t idx2, const char* label)
{
if (m_Error) { return; }
if (idx1 >= 2)
{
m_Error = true;
m_ParentPlugin.getBoxAlgorithmContext()->getPlayerContext()->getLogManager() << Kernel::LogLevel_Error << "Tried to access dimension " << idx1 <<
", only 0 and 1 supported\n";
return;
}
m_DimLabels[idx1][idx2] = label;
}
bool CBufferDatabase::setMatrixBuffer(const double* buffer, const uint64_t startTime, const uint64_t endTime)
{
//if an error has occurred, do nothing
if (m_Error) { return false; }
// Check for time-continuity
if (startTime < m_LastBufferEndTime && !m_WarningPrinted)
{
m_ParentPlugin.getBoxAlgorithmContext()->getPlayerContext()->getLogManager() << Kernel::LogLevel_Warning <<
"Your signal does not appear to be continuous in time. "
<< "Previously inserted buffer ended at " << CTime(m_LastBufferEndTime).toSeconds()
<< "s, the current starts at " << CTime(startTime).toSeconds()
<< "s. The display may be incorrect.\n";
m_WarningPrinted = true;
}
m_LastBufferEndTime = endTime;
//if this the first buffer, perform some precomputations
if (!m_HasFirstBuffer)
{
m_BufferDuration = endTime - startTime;
//test if it is equal to zero : Error
if (m_BufferDuration == 0)
{
m_ParentPlugin.getBoxAlgorithmContext()->getPlayerContext()->getLogManager() << Kernel::LogLevel_Warning <<
"Error : buffer start time and end time are equal : " << startTime << "\n";
m_Error = true;
return false;
}
//computes the sampling frequency for sanity checking or if the setter has not been called
const uint64_t sampleDuration = (uint64_t(1) << 32) * m_DimSizes[1];
auto estimatedFrequency = size_t(sampleDuration / m_BufferDuration);
if (estimatedFrequency == 0)
{
// Complain if estimate is bad
m_ParentPlugin.getBoxAlgorithmContext()->getPlayerContext()->getLogManager() << Kernel::LogLevel_Warning <<
"The integer sampling frequency was estimated from the chunk size to be 0"
<< " (nSamples " << m_DimSizes[1] << " / bufferLength " << CTime(m_BufferDuration).toSeconds() <<
"s = 0). This is not supported. Forcing the rate to 1. This may lead to problems.\n";
estimatedFrequency = 1;
}
if (m_Sampling == 0)
{
// use chunking duration estimate if setter hasn't been used
m_Sampling = estimatedFrequency;
}
if (m_Sampling != estimatedFrequency)
{
m_ParentPlugin.getBoxAlgorithmContext()->getPlayerContext()->getLogManager() << Kernel::LogLevel_Warning
<< "Sampling rate [" << estimatedFrequency << "] suggested by chunk properties differs from stream-specified rate [" << m_Sampling
<< "]. There may be a problem with an upstream box. Trying to use the estimated rate.\n";
m_Sampling = estimatedFrequency;
}
//computes the number of buffer necessary to display the interval
adjustNumberOfDisplayedBuffers(-1);
m_Drawable->init();
m_HasFirstBuffer = true;
}
if (!m_ChannelLookupTableInitialized)
{
fillChannelLookupTable(); //to retrieve the unrecognized electrode warning
// The above call will fail if no electrode localisation data...
// m_parentPlugin.getBoxAlgorithmContext()->getPlayerContext()->getLogManager() << Kernel::LogLevel_Error << "Unable to fill lookup table\n";
// return false;
}
else
{
//look for chanloc buffers recently received
for (size_t i = 0; i < m_channelLocalisationCoords.size(); ++i)
{
//if a new set of coordinates was received
if (m_channelLocalisationCoords[i].second) { onChannelLocalisationBufferReceived(i); }
}
}
double* bufferToWrite = nullptr;
const uint64_t nSamplesPerBuffer = m_DimSizes[0] * m_DimSizes[1];
//if old buffers need to be removed
if (m_SampleBuffers.size() == m_NBufferToDisplay)
{
if (m_TotalDurationOV == 0) { m_TotalDurationOV = (m_StartTime.back() - m_StartTime.front()) + (m_EndTime.back() - m_StartTime.back()); }
if (m_BufferStep == 0)
{
if (m_StartTime.size() <= 1) { m_BufferStep = m_TotalDurationOV; }
else { m_BufferStep = m_StartTime[1] - m_StartTime[0]; }
}
if (m_TotalStep == 0) { m_TotalStep = (m_StartTime.back() - m_StartTime.front()) + m_BufferStep; }
//save first buffer pointer
bufferToWrite = m_SampleBuffers.front();
//pop first element from queues
m_SampleBuffers.pop_front();
m_StartTime.pop_front();
m_EndTime.pop_front();
for (uint64_t c = 0; c < m_DimSizes[0]; ++c) { m_LocalMinMaxValue[c].pop_front(); }
}
//do we need to allocate a new buffer?
if (bufferToWrite == nullptr) { bufferToWrite = new double[size_t(nSamplesPerBuffer)]; }
//copy new buffer into internal buffer
memcpy(bufferToWrite, buffer, nSamplesPerBuffer * sizeof(double));
//push new buffer and its timestamps
m_SampleBuffers.push_back(bufferToWrite);
m_StartTime.push_back(startTime);
m_EndTime.push_back(endTime);
//compute and push min and max values of new buffer
uint64_t currentSample = 0;
//for each channel
for (uint64_t c = 0; c < m_DimSizes[0]; ++c)
{
double min = DBL_MAX, max = -DBL_MAX;
//for each sample
for (uint64_t i = 0; i < m_DimSizes[1]; i++, ++currentSample)
{
//get channel local min/max
if (buffer[currentSample] < min) { min = buffer[currentSample]; }
if (buffer[currentSample] > max) { max = buffer[currentSample]; }
}
//adds the minmax pair to the corresponding channel's list
m_LocalMinMaxValue[c].push_back(std::pair<double, double>(min, max));
if (max > m_MaxValue) { m_MaxValue = max; }
if (min < m_MinValue) { m_MinValue = min; }
}
//tells the drawable to redraw himself since the signal information has been updated
if (m_RedrawOnNewData) { m_Drawable->redraw(); }
return true;
}
bool CBufferDatabase::setSampling(const size_t sampling)
{
m_Sampling = sampling;
return true;
}
void CBufferDatabase::getDisplayedChannelLocalMinMaxValue(const size_t channel, double& min, double& max)
{
min = +DBL_MAX;
max = -DBL_MAX;
for (auto& pair : m_LocalMinMaxValue[size_t(channel)])
{
if (min > pair.first) { min = pair.first; }
if (max < pair.second) { max = pair.second; }
}
}
bool CBufferDatabase::isTimeInDisplayedInterval(const uint64_t& time) const
{
return !m_StartTime.empty() && time >= m_StartTime.front() && time <= m_EndTime.back();
}
bool CBufferDatabase::getIndexOfBufferStartingAtTime(const uint64_t& time, size_t& index) const
{
index = 0;
if (m_SampleBuffers.empty() || time < m_StartTime.front() || time > m_StartTime.back()) { return false; }
for (size_t i = 0; i < m_StartTime.size(); ++i)
{
if (m_StartTime[i] == time)
{
index = i;
return true;
}
}
return false;
}
void CBufferDatabase::getDisplayedGlobalMinMaxValue(double& min, double& max)
{
min = +DBL_MAX;
max = -DBL_MAX;
for (size_t c = 0; c < m_LocalMinMaxValue.size(); ++c)
{
for (auto& pair : m_LocalMinMaxValue[size_t(c)])
{
if (min > pair.first) { min = pair.first; }
if (max < pair.second) { max = pair.second; }
}
}
}
bool CBufferDatabase::getElectrodePosition(const size_t index, double* position)
{
//TODO : add time parameter and look for coordinates closest to that time!
if (index < m_channelLocalisationLabels.size())
{
//if(m_cartesianStreamedCoords == true)
//{
*position = *(m_channelLocalisationCoords[0].first->getBuffer() + 3 * index);
*(position + 1) = *(m_channelLocalisationCoords[0].first->getBuffer() + 3 * index + 1);
*(position + 2) = *(m_channelLocalisationCoords[0].first->getBuffer() + 3 * index + 2);
//}
return true;
}
return false;
}
bool CBufferDatabase::getElectrodePosition(const CString& label, double* position)
{
//TODO : add time parameter and look for coordinates closest to that time!
for (size_t i = 0; i < m_channelLocalisationLabels.size(); ++i)
{
if (strcmp(label.toASCIIString(), m_channelLocalisationLabels[i].toASCIIString()) == 0)
{
//if(m_cartesianStreamedCoords == true)
//{
*position = *(m_channelLocalisationCoords[0].first->getBuffer() + 3 * i);
*(position + 1) = *(m_channelLocalisationCoords[0].first->getBuffer() + 3 * i + 1);
*(position + 2) = *(m_channelLocalisationCoords[0].first->getBuffer() + 3 * i + 2);
//}
return true;
}
}
return false;
}
bool CBufferDatabase::getElectrodeLabel(const size_t index, CString& label)
{
if (index >= m_channelLocalisationLabels.size()) { return false; }
label = m_channelLocalisationLabels[index].toASCIIString();
return true;
}
bool CBufferDatabase::getChannelPosition(const size_t index, double*& position)
{
//TODO : add time parameter and look for coordinates closest to that time!
if (index >= 0 && index < m_ChannelLookupIdxs.size())
{
if (m_cartesianCoords) { position = m_channelLocalisationCoords[0].first->getBuffer() + 3 * m_ChannelLookupIdxs[index]; }
// else { } //TODO
return true;
}
return false;
}
bool CBufferDatabase::getChannelSphericalCoordinates(const size_t index, double& theta, double& phi)
{
//TODO : add time parameter and look for coordinates closest to that time!
if (index >= 0 && index < m_ChannelLookupIdxs.size())
{
if (m_cartesianCoords)
{
//get cartesian coords
double* coords = m_channelLocalisationCoords[0].first->getBuffer() + 3 * m_ChannelLookupIdxs[index];
//convert to spherical coords
return convertCartesianToSpherical(coords, theta, phi);
}
// else { return false; } //TODO streamed coordinates are spherical already
return false;
}
return false;
}
bool CBufferDatabase::getChannelLabel(const size_t index, CString& label)
{
if (index >= 0 && index < m_ChannelLookupIdxs.size())
{
label = m_channelLocalisationLabels[m_ChannelLookupIdxs[index]];
return true;
}
label = "";
return false;
}
void CBufferDatabase::setStimulation(const size_t /*index*/, const uint64_t identifier, const uint64_t date)
{
// m_parentPlugin.getBoxAlgorithmContext()->getPlayerContext()->getLogManager() << Kernel::LogLevel_Trace << "Received new stimulation id:" << stimulationIdentifier << " date:" << stimulationDate << "\n";
m_Stimulations.emplace_back(date, identifier);
if (!m_StartTime.empty())
{
while (m_Stimulations.begin() != m_Stimulations.end() && m_Stimulations.begin()->first < m_StartTime.front()) { m_Stimulations.pop_front(); }
}
}
bool CBufferDatabase::fillChannelLookupTable()
{
if (!m_HasFirstBuffer || !m_channelLocalisationHeaderReceived) { return false; }
bool res = true;
//resize lookup array and initialize lookup indices to 0
m_ChannelLookupIdxs.resize(size_t(m_NElectrodes), 0);
//for all channels
for (uint64_t i = 0; i < m_DimSizes[0]; ++i)
{
//trim leading spaces
size_t firstNonWhitespaceChar = 0;
for (; firstNonWhitespaceChar < m_DimLabels[0][i].size(); ++firstNonWhitespaceChar)
{
if (!isspace(m_DimLabels[0][i][firstNonWhitespaceChar])) { break; }
}
//trim trailing spaces
size_t lastNonWhitespaceChar = 0;
if (!m_DimLabels[0][i].empty())
{
for (lastNonWhitespaceChar = m_DimLabels[0][i].size() - 1; lastNonWhitespaceChar >= 0; lastNonWhitespaceChar--)
{
if (!isspace(m_DimLabels[0][i][lastNonWhitespaceChar])) { break; }
}
}
//look for label in channel localisation labels database
bool recognized = false;
if (firstNonWhitespaceChar < lastNonWhitespaceChar)
{
std::string label(m_DimLabels[0][i].substr(firstNonWhitespaceChar, lastNonWhitespaceChar - firstNonWhitespaceChar + 1));
for (size_t j = 0; j < m_channelLocalisationLabels.size(); ++j)
{
if (strcmp(label.c_str(), m_channelLocalisationLabels[j].toASCIIString()) == 0)
{
recognized = true;
m_ChannelLookupIdxs[i] = j;
break;
}
}
}
//unrecognized electrode!
if (!recognized)
{
m_ParentPlugin.getLogManager() << Kernel::LogLevel_Warning << "Unrecognized electrode name (index=" << i << ", name=" << m_DimLabels[0][i].c_str()
<<
")!\n";
res = false;
}
}
m_ParentPlugin.getLogManager() << Kernel::LogLevel_Trace << "Electrodes list : ";
for (uint64_t i = 0; i < m_DimSizes[0]; ++i)
{
m_ParentPlugin.getLogManager() << CString(m_DimLabels[0][i].c_str());
if (i < m_DimSizes[0] - 1) { m_ParentPlugin.getLogManager() << ", "; }
else { m_ParentPlugin.getLogManager() << "\n"; }
}
if (res) { m_ChannelLookupTableInitialized = true; }
return res;
}
bool CBufferDatabase::convertCartesianToSpherical(const double* cartesian, double& theta, double& phi) const
{
#define MY_THRESHOLD 1e-3
#define PI 3.1415926535
const double radToDeg = 180 / PI;
//compute theta
theta = acos(cartesian[2]) * radToDeg;
//compute phi so that it lies in [0, 360]
if (fabs(cartesian[0]) < MY_THRESHOLD) { phi = (cartesian[1] > 0) ? 90 : 270; }
else
{
phi = atan(cartesian[1] / cartesian[0]) * radToDeg;
if (cartesian[0] < 0) { phi += 180; }
else if (cartesian[1] < 0) { phi += 360; }
}
return true;
}
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,367 @@
#pragma once
#include <cfloat>
#include <deque>
#include <string>
#include <vector>
#include <array>
#include "ovp_defines.h"
#include <openvibe/ov_all.h>
#include <toolkit/ovtk_all.h>
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
class CSignalDisplayDrawable;
/**
* Abtract class of objects than can be updated by a CBufferDatabase
*/
class CSignalDisplayDrawable
{
public:
virtual ~CSignalDisplayDrawable() = default;
virtual void init() = 0;
virtual void redraw() = 0;
};
/**
* This class is used to store information about the incoming signal stream. It can request a CSignalDisplayDrawable
* object to redraw himself in case of some changes in its data.
*/
class CBufferDatabase
{
public:
//! Number of channels
int64_t m_NElectrodes = 0;
//! Number of channels and number of samples per buffer
std::array<size_t, 2> m_DimSizes;
//! Channel labels, buffer labels
std::array<std::vector<std::string>, 2> m_DimLabels;
//! Flag set to true once first buffer is received
bool m_HasFirstBuffer = false;
//! Sampling frequency of the incoming stream
size_t m_Sampling = 0;
//! double-linked list of pointers to the samples buffers of the current time window
std::deque<double*> m_SampleBuffers;
//! stimulations to display. pair values are <date, stimcode>
std::deque<std::pair<uint64_t, uint64_t>> m_Stimulations;
//electrode spherical coordinates (in degrees)
//CMatrix m_oElectrodesSphericalCoords;
//flag set to true once channel lookup indices are determined
bool m_ChannelLookupTableInitialized = false;
//indices of electrodes in channel localisation database
std::vector<size_t> m_ChannelLookupIdxs;
//electrode labels (standardized)
//std::vector<CString> m_oElectrodesLabels;
//! Number of buffer to display at the same time
uint64_t m_NBufferToDisplay = 2;
//! The global maximum value of the signal (up to now)
double m_MaxValue = -DBL_MAX;
//! The global minimum value of the signal (up to now)
double m_MinValue = +DBL_MAX;
//! Double-linked list of the start times of the current buffers
std::deque<uint64_t> m_StartTime;
//! Double-linked list of the end times of the current buffers
std::deque<uint64_t> m_EndTime;
//! Duration to display in seconds
double m_TotalDuration = 0;
/*! Duration to display in openvibe time units.
Computed once every time the user changes the total duration to display,
when the maximum number of buffers to store are received.*/
uint64_t m_TotalDurationOV = 0;
/*! Duration of a single buffer.
Computed once, but not constant when sampling frequency is not a multiple of buffer size!*/
uint64_t m_BufferDuration = 0;
/*! Time step separating the start times of m_NBufferToDisplay+1 buffers.
Recomputed once every time the user changes the total duration to display,
but not constant when sampling frequency is not a multiple of buffer size!*/
uint64_t m_TotalStep = 0;
/*! Time step separating the start times of 2 consecutive buffers.
Computed once, but not constant when sampling frequency is not a multiple of buffer size!*/
uint64_t m_BufferStep = 0;
// When did the last inserted buffer end
uint64_t m_LastBufferEndTime = 0;
// Did we print a warning about noncontinuity?
bool m_WarningPrinted = false;
//! Pointer to the drawable object to update (if needed)
CSignalDisplayDrawable* m_Drawable = nullptr;
std::vector<std::deque<std::pair<double, double>>> m_LocalMinMaxValue;
Toolkit::TBoxAlgorithm<IBoxAlgorithm>& m_ParentPlugin;
bool m_Error = false;
//! Redraws the associated SignalDisplayDrawable upon new data reception if true (default)
bool m_RedrawOnNewData = true;
protected:
/* \name Channel localisation */
//@{
//channel localisation decoder
Kernel::IAlgorithmProxy* m_decoder = nullptr;
//flag set to true once channel localisation buffer is received
bool m_channelLocalisationHeaderReceived = false;
//dynamic channel localisation flag (e.g. localisation is constantly updated with MEG)
bool m_dynamicChannelLocalisation = false;
//channel labels database
std::vector<CString> m_channelLocalisationLabels;
//flag stating whether streamed coordinates are cartesian (as opposed to spherical)
bool m_cartesianCoords = false;
//! double-linked list of streamed channel coordinates (if cartesian, expressed in normalized space (X right Y front Z up))
std::deque<std::pair<CMatrix*, bool>> m_channelLocalisationCoords;
//! double-linked list of channel coordinates (spherical if streamed coords aere cartesian and vice versa)
//std::deque< std::pair<CMatrix*, bool> > m_oChannelLocalisationAlternateCoords;
//pointer to double linked list of cartesian coordinates
//std::deque< std::pair<CMatrix*, bool> > * m_pChannelLocalisationCartesianCoords = nullptr;
//pointer to double linked list of spherical coordinates
//std::deque< std::pair<CMatrix*, bool> > * m_pChannelLocalisationSphericalCoords = nullptr;
//! double-linked list of start/end times of channel coordinates
std::deque<std::pair<uint64_t, uint64_t>> m_channelLocalisationTimes;
//@}
//! Redraw mode (shift or scan)
CIdentifier m_displayMode = Scan;
public:
explicit CBufferDatabase(Toolkit::TBoxAlgorithm<IBoxAlgorithm>& parent);
virtual ~CBufferDatabase();
/**
* \brief Decode a channel localisation memory buffer
* \param buffer Memory buffer to decode
* \param startTime Start time of memory buffer
* \param endTime End time of memory buffer
* \return True if memory buffer could be properly decoded, false otherwise
*/
virtual bool decodeChannelLocalisationMemoryBuffer(const IMemoryBuffer* buffer, uint64_t startTime, uint64_t endTime);
/**
* \brief Callback called upon channel localisation buffer reception
* \param index Index of newly received channel localisation buffer
* \return True if buffer data was correctly processed, false otherwise
*/
virtual bool onChannelLocalisationBufferReceived(const size_t index);
/**
* \brief Sets the drawable object to update.
* \param drawable drawable object to update.
*/
virtual void setDrawable(CSignalDisplayDrawable* drawable) { m_Drawable = drawable; }
/**
* \brief Get error status
* \return Error status. If true, an error occurred.
*/
virtual bool getErrorStatus() { return m_Error; }
/**
* \brief Determines whether first buffer has been received yet
* \return True if first buffer has been received already, false otherwise
*/
virtual bool hasFirstBuffer() { return m_HasFirstBuffer; }
/**
* \brief Determines whether first channel localisation buffer has been processed yet
* When this condition is true, channel coordinates may be retrieved using the
* corresponding methods in this class.
* \return True if first chanloc buffer was processed
*/
virtual bool isFirstChannelLocalisationBufferProcessed();
/**
* Compute the number of buffers needed to display the signal for a certain time period.
* \param time the time window's width in seconds.
*/
virtual bool adjustNumberOfDisplayedBuffers(double time);
/**
* \brief Get time interval covered by data held in this object
* \return Time interval in ms
*/
virtual double getDisplayedTimeIntervalWidth() const { return (m_NBufferToDisplay * ((m_DimSizes[1] * 1000.0) / m_Sampling)); }
/**
* \brief Determine whether time passed in parameter lies in displayed data interval
* \param time Time to test
* \return True if time lies in displayed time interval, false otherwise
*/
virtual bool isTimeInDisplayedInterval(const uint64_t& time) const;
/**
* \brief Get index of sample buffer which starts at a given time
* \param time[in] Start time of buffer
* \param index[out] Buffer index
* \return True if buffer index could be determined, false otherwise
*/
virtual bool getIndexOfBufferStartingAtTime(const uint64_t& time, size_t& index) const;
//! Returns the min/max values currently displayed for the given channel
virtual void getDisplayedChannelLocalMinMaxValue(const size_t channel, double& min, double& max);
//! Returns the min/max values currently displayed (all channels taken into account)
virtual void getDisplayedGlobalMinMaxValue(double& min, double& max);
virtual void getDisplayedChannelLocalMeanValue(const size_t /*channel*/, double& /*mean*/) {}
//! Returns the min/max values of the last buffer arrived for the given channel
virtual void getLastBufferChannelLocalMinMaxValue(const size_t channel, double& min, double& max)
{
min = m_LocalMinMaxValue[channel].back().first;
max = m_LocalMinMaxValue[channel].back().second;
}
//! Returns the min/max values of the last buffer arrived (all channels taken into account)
virtual void getLastBufferMinMaxValue(double& min, double& max)
{
min = +DBL_MAX;
max = -DBL_MAX;
for (auto& pair : m_LocalMinMaxValue)
{
min = (pair.back().first < min) ? pair.back().first : min;
max = (pair.back().second > max) ? pair.back().second : max;
}
}
/**
* \brief Get number of eletrodes in database
* \return Number of electrodes
*/
virtual size_t getElectrodeCount() { return m_channelLocalisationLabels.size(); }
/**
* \brief Get electrode normalized position
* \remarks Position expressed in normalized cartesian frame where X is right, Y front, Z up
* \param[in] index Index of electrode in database whose position is to be retrieved
* \param[out] position Pointer to an array of 3 floats where to store coordinates
* \return True if electrode position could be retrieved
*/
virtual bool getElectrodePosition(const size_t index, double* position);
/**
* \brief Get electrode normalized position
* \remarks Position expressed in normalized cartesian frame where X is right, Y front, Z up
* \param[in] label Label of electrode whose position is to be retrieved
* \param[out] position Pointer to an array of 3 floats where to store coordinates
* \return True if electrode position could be retrieved
*/
virtual bool getElectrodePosition(const CString& label, double* position);
/**
* \brief Get electrode label
* \param[in] index Index of electrode in database whose label is to be retrieved
* \param[out] label Electrode label
* \return True if electrode label could be retrieved
*/
virtual bool getElectrodeLabel(const size_t index, CString& label);
/**
* \brief Get number of channels
* \return Number of channels
*/
virtual size_t getChannelCount() const { return m_DimSizes[0]; }
/**
* \brief Get channel normalized position
* \remarks Position expressed in normalized cartesian frame where X is right, Y front, Z up
* \param[in] index Index of channel whose position is to be retrieved
* \param[out] position Reference on a double pointer
* \return True if channel position could be retrieved (position then points to an array of 3 floats)
*/
virtual bool getChannelPosition(const size_t index, double*& position);
/**
* \brief Get channel spherical coordinates in degrees
* \param[in] index Index of channel whose coordinates are to be retrieved
* \param[out] theta Reference on a float to be set with theta angle
* \param[out] phi Reference on a float to be set with phi angle
* \return True if channel coordinates could be retrieved
*/
virtual bool getChannelSphericalCoordinates(const size_t index, double& theta, double& phi);
/**
* \brief Get channel label
* \param[in] index Index of channel whose label is to be retrieved
* \param[out] label Channel label
* \return True if channel label could be retrieved
*/
virtual bool getChannelLabel(const size_t index, CString& label);
virtual void setMatrixDimensionCount(const size_t count);
virtual void setMatrixDimensionSize(const size_t index, const size_t size);
virtual void setMatrixDimensionLabel(const size_t idx1, const size_t idx2, const char* label);
// Returns false on failure
virtual bool setMatrixBuffer(const double* buffer, uint64_t startTime, uint64_t endTime);
// Sets the sampling frequency. If this is not called, the frequency is estimated from the stream chunk properties.
// Mainly used to force a warning if stream-specified rate differs from the chunk-estimated rate.
virtual bool setSampling(const size_t sampling);
virtual void setStimulationCount(const size_t /*count*/) {}
virtual void setStimulation(const size_t index, uint64_t identifier, uint64_t date);
/**
* \brief Set display mode
* \remarks Used by signal display and time ruler to determine how they should be updated
* \param mode New display mode
*/
virtual void setDisplayMode(const CIdentifier& mode) { m_displayMode = mode; }
/**
* \brief Get current display mode
* \return Current display mode
*/
virtual CIdentifier getDisplayMode() { return m_displayMode; }
/**
* \brief Set flag stating whether to redraw associated SignalDisplayDrawable objet when new data is available
* \param set Value to set flag with
*/
virtual void setRedrawOnNewData(const bool set) { m_RedrawOnNewData = set; }
protected:
/**
* \brief Initialize table storing indices of electrodes in channel localisation database
* \return True if table could be initialized
*/
virtual bool fillChannelLookupTable();
/**
* \brief Convert a cartesian coordinates triplet to spherical coordinates
* \param[in] cartesian Pointer to cartesian coordinates triplet
* \param[out] theta Equivalent theta angle
* \param[out] phi Equivalent phi angle
* \return True if coordinates were successfully converted
*/
bool convertCartesianToSpherical(const double* cartesian, double& theta, double& phi) const;
};
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,53 @@
#include "ovpCSpectrumDatabase.h"
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
bool CSpectrumDatabase::initialize()
{
if (m_decoder != nullptr) { return false; }
m_decoder = &m_parentPlugin.getAlgorithmManager().getAlgorithm(
m_parentPlugin.getAlgorithmManager().createAlgorithm(OVP_GD_ClassId_Algorithm_SpectrumDecoder));
m_decoder->initialize();
return true;
}
//double CSpectrumDatabase::getFrequencyBandWidth()
//{
// if(m_frequencyAbscissa.size() == 0) { return 0; }
// else { return m_oFrequencyBands[0].second - m_oFrequencyBands[0].first; }
//}
//double CSpectrumDatabase::getFrequencyBandStart(const size_t index)
//{
// if(m_oFrequencyBands.size() == 0) { return 0; }
// else { return m_oFrequencyBands[index].first; }
//}
//double CSpectrumDatabase::getFrequencyBandStop(const size_t index)
//{
// if(index >= m_oFrequencyBands.size()) { return 0; }
// else { return m_oFrequencyBands[index].second; }
//}
bool CSpectrumDatabase::decodeHeader()
{
//retrieve spectrum header
Kernel::TParameterHandler<CMatrix*> frequencyAbscissaMatrix;
frequencyAbscissaMatrix.initialize(m_decoder->getOutputParameter(OVP_GD_Algorithm_SpectrumDecoder_OutputParameterId_FrequencyAbscissa));
//store frequency bands
for (size_t i = 0; i < frequencyAbscissaMatrix->getDimensionSize(0); ++i) { m_frequencyAbscissa.push_back(frequencyAbscissaMatrix->getBuffer()[i]); }
CStreamedMatrixDatabase::decodeHeader();
return true;
}
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,74 @@
#pragma once
#include "ovpCStreamedMatrixDatabase.h"
#include <openvibe/ov_all.h>
#include <toolkit/ovtk_all.h>
#include <vector>
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
/**
* This class is used to store information about the incoming spectrum stream. It can request a IStreamDisplayDrawable
* object to redraw itself upon changes in its data.
*/
class CSpectrumDatabase final : public CStreamedMatrixDatabase
{
public:
explicit CSpectrumDatabase(Toolkit::TBoxAlgorithm<IBoxAlgorithm>& parent) : CStreamedMatrixDatabase(parent) { }
~CSpectrumDatabase() override = default;
bool initialize() override;
/**
* \brief Get number of frequency bands
* \return Number of frequency bands
*/
size_t getFrequencyAbscissaCount() const { return m_frequencyAbscissa.size(); }
protected:
bool decodeHeader() override;
/**
* \brief Set displayed frequency range
* \param minimumDisplayedFrequency Minimum frequency to display
* \param maximumDisplayedFrequency Maximum frequency to display
*/
//TODO (if min/max computation should be restricted to this range)
/*
void setDisplayedFrequencyRange(double minimumDisplayedFrequency, double maximumDisplayedFrequency);*/
/** \name Frequency bands management */
//@{
/**
* \brief Get width of a frequency band (in Hz)
* \return Frequency band width
*/
// double getFrequencyBandWidth();
/**
* \brief Get frequency band start frequency
* \param index Index of frequency band
* \return Frequency band start if it could be retrieved, 0 otherwise
*/
// double getFrequencyBandStart(const size_t index);
/**
* \brief Get frequency band stop frequency
* \param index Index of frequency band
* \return Frequency band stop if it could be retrieved, 0 otherwise
*/
// double getFrequencyBandStop(const size_t index);
//@}
private:
std::vector<double> m_frequencyAbscissa;
};
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,290 @@
#include "ovpCStreamedMatrixDatabase.h"
#include <algorithm>
#include <cmath>
#include <cfloat> // For unix system
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
CStreamedMatrixDatabase::~CStreamedMatrixDatabase()
{
if (m_decoder != nullptr)
{
m_decoder->uninitialize();
m_parentPlugin.getAlgorithmManager().releaseAlgorithm(*m_decoder);
}
while (!m_matrices.empty())
{
delete m_matrices.front();
m_matrices.pop_front();
}
}
bool CStreamedMatrixDatabase::initialize()
{
if (m_decoder != nullptr) { return false; }
m_decoder = &m_parentPlugin.getAlgorithmManager().getAlgorithm(
m_parentPlugin.getAlgorithmManager().createAlgorithm(OVP_GD_ClassId_Algorithm_StreamedMatrixDecoder));
m_decoder->initialize();
return true;
}
bool CStreamedMatrixDatabase::setMaxBufferCount(const size_t count)
{
//max buffer count computed directly
m_ignoreTimeScale = true;
m_nMaxBuffer = count;
onBufferCountChanged();
return true;
}
bool CStreamedMatrixDatabase::setTimeScale(const double timeScale)
{
if (timeScale <= 0) { return false; }
//max buffer count computed from time scale
m_ignoreTimeScale = false;
//update time scale
m_timeScale = timeScale;
//if step between buffers is not known yet, this method will have to be called again later
if (!m_bufferTimeStepComputed) { return false; }
//compute maximum number of buffers needed to cover time scale
size_t maxBufferCount = 0;
if (m_bufferTimeStep > 0) { maxBufferCount = size_t(ceil(double(CTime(m_timeScale).time()) / m_bufferTimeStep)); }
//display at least one buffer
if (maxBufferCount == 0) { maxBufferCount = 1; }
//acknowledge maximum buffer count
bool maxBufferCountChanged = false;
if (maxBufferCount != m_nMaxBuffer)
{
m_nMaxBuffer = maxBufferCount;
maxBufferCountChanged = true;
onBufferCountChanged();
}
return maxBufferCountChanged;
}
bool CStreamedMatrixDatabase::onBufferCountChanged()
{
//if new number of buffers is smaller than before, destroy extra buffers
while (m_matrices.size() > m_nMaxBuffer)
{
delete m_matrices.front();
m_matrices.pop_front();
m_startTimes.pop_front();
m_endTimes.pop_front();
for (auto& i : m_channelMinMaxValues) { i.pop_front(); }
}
return true;
}
bool CStreamedMatrixDatabase::decodeMemoryBuffer(const IMemoryBuffer* buffer, const uint64_t startTime, const uint64_t endTime)
{
//feed memory buffer to algorithm
m_decoder->getInputParameter(OVP_GD_Algorithm_StreamedMatrixDecoder_InputParameterId_MemoryBufferToDecode)->setReferenceTarget(&buffer);
//process buffer
m_decoder->process();
//has flow header been received?
if (m_decoder->isOutputTriggerActive(OVP_GD_Algorithm_StreamedMatrixDecoder_OutputTriggerId_ReceivedHeader))
{
decodeHeader();
//create widgets
m_drawable->init();
}
//has a buffer been received?
if (m_decoder->isOutputTriggerActive(OVP_GD_Algorithm_StreamedMatrixDecoder_OutputTriggerId_ReceivedBuffer))
{
decodeBuffer(startTime, endTime);
//redraw widgets
if (m_redrawOnNewData) { m_drawable->redraw(); }
}
return true;
}
bool CStreamedMatrixDatabase::getChannelLabel(const size_t index, CString& label)
{
if (m_matrixHeader.getDimensionCount() == 0 || m_matrixHeader.getDimensionSize(0) <= index)
{
label = "";
return false;
}
label = m_matrixHeader.getDimensionLabel(0, index);
return true;
}
bool CStreamedMatrixDatabase::getChannelMinMaxValues(const size_t index, double& min, double& max)
{
min = +DBL_MAX;
max = -DBL_MAX;
if (!m_hasFirstBuffer || index >= getChannelCount()) { return false; }
for (const auto& values : m_channelMinMaxValues[index])
{
if (min > values.first) { min = values.first; }
if (max < values.second) { max = values.second; }
}
return true;
}
bool CStreamedMatrixDatabase::getGlobalMinMaxValues(double& min, double& max)
{
min = +DBL_MAX;
max = -DBL_MAX;
if (!m_hasFirstBuffer) { return false; }
for (size_t c = 0; c < getChannelCount(); ++c)
{
for (const auto& values : m_channelMinMaxValues[c])
{
if (min > values.first) { min = values.first; }
if (max < values.second) { max = values.second; }
}
}
return true;
}
bool CStreamedMatrixDatabase::getLastBufferChannelMinMaxValues(const size_t index, double& min, double& max)
{
min = +DBL_MAX;
max = -DBL_MAX;
if (!m_hasFirstBuffer) { return false; }
if (index >= getChannelCount()) { return false; }
min = m_channelMinMaxValues[index].back().first;
max = m_channelMinMaxValues[index].back().second;
return true;
}
bool CStreamedMatrixDatabase::getLastBufferGlobalMinMaxValues(double& min, double& max)
{
min = +DBL_MAX;
max = -DBL_MAX;
if (!m_hasFirstBuffer) { return false; }
for (const auto& values : m_channelMinMaxValues)
{
if (min > values.back().first) { min = values.back().first; }
if (max < values.back().second) { max = values.back().second; }
}
return true;
}
bool CStreamedMatrixDatabase::decodeHeader()
{
//copy streamed matrix header
Kernel::TParameterHandler<CMatrix*> streamedMatrix;
streamedMatrix.initialize(m_decoder->getOutputParameter(OVP_GD_Algorithm_StreamedMatrixDecoder_OutputParameterId_Matrix));
m_matrixHeader.copyDescription(*streamedMatrix);
m_channelMinMaxValues.resize(getChannelCount());
return true;
}
bool CStreamedMatrixDatabase::decodeBuffer(const uint64_t startTime, const uint64_t endTime)
{
//first buffer received
if (!m_hasFirstBuffer)
{
const uint64_t bufferDuration = endTime - startTime;
if (bufferDuration == 0)
{
m_parentPlugin.getBoxAlgorithmContext()->getPlayerContext()->getLogManager() << Kernel::LogLevel_Warning <<
"Error : buffer start time and end time are equal : " << startTime << "\n";
//m_error = true;
return false;
}
m_hasFirstBuffer = true;
}
//compute time step between two buffers
if (!m_bufferTimeStepComputed && m_matrices.size() >= 2)
{
m_bufferTimeStep = m_startTimes[1] - m_startTimes[0];
m_bufferTimeStepComputed = true;
if (!m_ignoreTimeScale)
{
//compute maximum number of buffers from time scale
setTimeScale(m_timeScale);
}
}
//store new buffer data
CMatrix* currentMatrix;
if (m_matrices.size() < m_nMaxBuffer)
{
currentMatrix = new CMatrix();
currentMatrix->copyDescription(m_matrixHeader);
m_matrices.push_back(currentMatrix);
}
else //reuse memory for new buffer
{
//move front matrix to back of list
currentMatrix = m_matrices.front();
m_matrices.push_back(currentMatrix);
//remove first matrix data
m_matrices.pop_front();
m_startTimes.pop_front();
m_endTimes.pop_front();
for (size_t c = 0; c < getChannelCount(); ++c) { m_channelMinMaxValues[c].pop_front(); }
}
//store samples
Kernel::TParameterHandler<CMatrix*> streamedMatrix;
streamedMatrix.initialize(m_decoder->getOutputParameter(OVP_GD_Algorithm_StreamedMatrixDecoder_OutputParameterId_Matrix));
currentMatrix->copyContent(*streamedMatrix);
//store time stamps
m_startTimes.push_back(startTime);
m_endTimes.push_back(endTime);
//store min/max values
double* buffer = currentMatrix->getBuffer();
for (size_t c = 0; c < getChannelCount(); ++c)
{
double min = DBL_MAX;
double max = -DBL_MAX;
for (uint64_t i = 0; i < getSampleCountPerBuffer(); ++i, ++buffer)
{
if (*buffer < min) { min = *buffer; }
if (*buffer > max) { max = *buffer; }
}
m_channelMinMaxValues[c].push_back(std::pair<double, double>(min, max));
}
return true;
}
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,101 @@
#pragma once
#include "ovpIStreamDatabase.h"
#include <openvibe/ov_all.h>
#include <toolkit/ovtk_all.h>
#include <deque>
#include <vector>
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
/**
* This class is used to store information about the incoming matrix stream. It can request a IStreamDisplayDrawable
* object to redraw itself upon changes in its data.
*/
class CStreamedMatrixDatabase : public IStreamDatabase
{
public:
/**
* \brief Constructor
* \param
*/
explicit CStreamedMatrixDatabase(Toolkit::TBoxAlgorithm<IBoxAlgorithm>& parent) : m_parentPlugin(parent) {}
/**
* \brief Destructor
*/
~CStreamedMatrixDatabase() override;
bool initialize() override;
void setDrawable(IStreamDisplayDrawable* drawable) override { m_drawable = drawable; }
void setRedrawOnNewData(const bool redrawOnNewData) override { m_redrawOnNewData = redrawOnNewData; }
bool hasFirstBuffer() override { return m_hasFirstBuffer; }
bool setMaxBufferCount(const size_t count) override;
bool setTimeScale(const double timeScale) override;
bool decodeMemoryBuffer(const IMemoryBuffer* buffer, const uint64_t startTime, const uint64_t endTime) override;
size_t getMaxBufferCount() override { return m_nMaxBuffer; }
size_t getCurrentBufferCount() override { return m_matrices.size(); }
const double* getBuffer(const size_t index) override { return (index >= m_matrices.size()) ? nullptr : m_matrices[index]->getBuffer(); }
uint64_t getStartTime(const size_t index) override { return (index >= m_startTimes.size()) ? 0 : m_startTimes[index]; }
uint64_t getEndTime(const size_t index) override { return (index >= m_endTimes.size()) ? 0 : m_endTimes[index]; }
size_t getBufferElementCount() override { return (m_matrices.empty()) ? 0 : m_matrices[0]->getBufferElementCount(); }
uint64_t getBufferDuration() override { return (m_startTimes.empty() || m_endTimes.empty()) ? 0 : m_endTimes[0] - m_startTimes[0]; }
bool isBufferTimeStepComputed() override { return m_bufferTimeStepComputed; }
uint64_t getBufferTimeStep() override { return m_bufferTimeStepComputed ? m_bufferTimeStep : 0; }
size_t getSampleCountPerBuffer() override { return m_matrixHeader.getDimensionCount() == 0 ? 0 : m_matrixHeader.getDimensionSize(1); }
size_t getChannelCount() override { return (m_matrixHeader.getDimensionCount() == 0) ? 0 : m_matrixHeader.getDimensionSize(0); }
bool getChannelLabel(const size_t index, CString& label) override;
bool getChannelMinMaxValues(const size_t index, double& min, double& max) override;
bool getGlobalMinMaxValues(double& min, double& max) override;
bool getLastBufferChannelMinMaxValues(const size_t index, double& min, double& max) override;
bool getLastBufferGlobalMinMaxValues(double& min, double& max) override;
protected:
bool onBufferCountChanged();
virtual bool decodeHeader();
virtual bool decodeBuffer(const uint64_t startTime, const uint64_t endTime);
// parent plugin
Toolkit::TBoxAlgorithm<IBoxAlgorithm>& m_parentPlugin;
//decoder algorithm
Kernel::IAlgorithmProxy* m_decoder = nullptr;
//drawable object to update (if needed)
IStreamDisplayDrawable* m_drawable = nullptr;
//flag stating whether to redraw the IStreamDisplayDrawable upon new data reception if true (default)
bool m_redrawOnNewData = true;
//flag stating whether first samples buffer has been received
bool m_hasFirstBuffer = false;
//flag stating whether buffer time step was computed
bool m_bufferTimeStepComputed = false;
//time difference between start times of two consecutive buffers
uint64_t m_bufferTimeStep = 0;
//maximum number of buffers stored in database
size_t m_nMaxBuffer = 2;
//flag stating whether time scale should be ignored (max buffer count externally set)
bool m_ignoreTimeScale = false;
//maximum duration of displayed buffers (in seconds)
double m_timeScale = 10;
//double-linked list of start times of stored buffers
std::deque<uint64_t> m_startTimes;
//double-linked list of end times of stored buffers
std::deque<uint64_t> m_endTimes;
//streamed matrix header
CMatrix m_matrixHeader;
//streamed matrix history
std::deque<CMatrix*> m_matrices;
//min/max values for each channel
std::vector<std::deque<std::pair<double, double>>> m_channelMinMaxValues;
};
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,117 @@
#include "ovpCTimeRuler.h"
#include <cmath>
#include <sstream>
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
#define CONVERT_TIME(i) (double((i)>>32) + double(double((i)&0xFFFFFFFF) / double((uint64_t)1<<32)))
//CALLBACKS
//! Callback to redraw the bottom ruler
gboolean TimeRulerExposeEventCB(GtkWidget* /*widget*/, GdkEventExpose* /*event*/, gpointer data)
{
//redraw the ruler
auto* timeRuler = reinterpret_cast<CTimeRuler*>(data);
timeRuler->draw();
//don't propagate this signal to the children if any
return TRUE;
}
//! Called when the widget whose width is associated with the ruler is resized.
gboolean TimeRulerResizeCB(GtkWidget* /*widget*/, GtkAllocation* allocation, gpointer data)
{
auto* timeRuler = reinterpret_cast<CTimeRuler*>(data);
timeRuler->onResizeEventCB(allocation->width, allocation->height);
return FALSE;
}
CTimeRuler::CTimeRuler(IStreamDatabase& streamDatabase, const int width, const int height)
: m_stream(streamDatabase), m_height(height)
{
m_widget = gtk_drawing_area_new();
gtk_widget_set_size_request(m_widget, width, height);
g_signal_connect_after(G_OBJECT(m_widget), "expose_event", G_CALLBACK(TimeRulerExposeEventCB), this);
}
void CTimeRuler::draw()
{
//if the widget is invisible, no need to redraw it
if (!GTK_WIDGET_VISIBLE(m_widget)) { return; }
//return if time between two consecutive buffers hasn't been computed yet
if (!m_stream.isBufferTimeStepComputed()) { return; }
//get widget size
gint bottomRulerW;
gdk_drawable_get_size(m_widget->window, &bottomRulerW, nullptr);
const double startTime = CONVERT_TIME(m_stream.getStartTime(0));
const double endTime = CONVERT_TIME(m_stream.getStartTime(0) + m_stream.getMaxBufferCount() * m_stream.getBufferTimeStep());
const double intervalW = endTime - startTime;
//compute step between two values displayed on the ruler
const auto nearestSmallerPowerOf10 = double(pow(10, floor(log10(intervalW))));
const auto maxNLabels = uint64_t(bottomRulerW / m_pixelsPerLabel);
double valueStep = nearestSmallerPowerOf10;
if (uint64_t(floor(intervalW / nearestSmallerPowerOf10)) > maxNLabels) { valueStep = 2 * nearestSmallerPowerOf10; }
else if (uint64_t(floor(intervalW / nearestSmallerPowerOf10)) < maxNLabels / 2) { valueStep = nearestSmallerPowerOf10 / 2; }
//recompute step base value
const double baseValue = valueStep * floor(startTime / valueStep);
//X position of the first label
const double bufferW = double(bottomRulerW) / double(m_stream.getMaxBufferCount());
auto baseX = int64_t(floor(bottomRulerW - (m_stream.getCurrentBufferCount() * bufferW)));
if (baseX < 0) { baseX = 0; }
//draw ruler base (horizontal line)
gdk_draw_line(m_widget->window, m_widget->style->fg_gc[GTK_WIDGET_STATE(m_widget)], gint(baseX), 0, bottomRulerW, 0);
//draw labels
std::stringstream timeLabel;
for (double i = baseValue; i < double(0.5 + endTime); i += valueStep)
{
//clear stringstream
timeLabel.str("");
//compute label position
const gint textX = gint(baseX + ((i - startTime) * ((double(bottomRulerW)) / intervalW)));
if (textX >= bottomRulerW) { break; }
timeLabel << i;
PangoLayout* text = gtk_widget_create_pango_layout(m_widget, timeLabel.str().c_str());
int textW;
pango_layout_get_pixel_size(text, &textW, nullptr);
//if the width allocated per label becomes too small compared to the effective width of the label
if (uint64_t(textW) >= m_pixelsPerLabel - 20)
{
//increases the allocated width per label
m_pixelsPerLabel = textW + 30;
}
//display it
gdk_draw_layout(m_widget->window, m_widget->style->fg_gc[GTK_WIDGET_STATE(m_widget)], textX, 4, text);
//draw a small line above it
gdk_draw_line(m_widget->window, m_widget->style->fg_gc[GTK_WIDGET_STATE(m_widget)], textX, 0, textX, 3);
}
}
void CTimeRuler::linkWidthToWidget(GtkWidget* widget)
{
//add a callback to the widget for the size-allocate signal
g_signal_connect(G_OBJECT(widget), "size-allocate", G_CALLBACK(TimeRulerResizeCB), this);
}
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,72 @@
#pragma once
#include "ovpIStreamDatabase.h"
#include <openvibe/ov_all.h>
#include <gtk/gtk.h>
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
/**
* Displays a time ruler.
* Uses information fetched from a stream database object.
*/
class CTimeRuler
{
public:
/**
* \brief Constructor
* \param streamDatabase Object from which data is retrieved
* \param width Width to be requested by widget
* \param height Height to be requested by widget
*/
CTimeRuler(IStreamDatabase& streamDatabase, int width, int height);
/**
* \brief Destructor
*/
~CTimeRuler() = default;
/**
* \brief Get widget handled by this object
* \return Gtk widget
*/
GtkWidget* getWidget() const { return m_widget; }
/**
* \brief Toggle ruler on/off
* \param active Activation flag
*/
void toggle(const bool active) const { active ? gtk_widget_show(m_widget) : gtk_widget_hide(m_widget); }
/**
* \brief Draw ruler
*/
void draw();
/**
* \brief Link ruler width to another widget's
* \param widget Widget whose width must be matched by this object
*/
void linkWidthToWidget(GtkWidget* widget);
/**
* \brief Callback notified upon resize events
* \param width New window width
* \remarks height is the m_heightRequest
*/
void onResizeEventCB(const gint width, gint /*height*/) const { gtk_widget_set_size_request(m_widget, width, m_height); }
private:
//! Ruler widget
GtkWidget* m_widget = nullptr;
//! Database from which stream information is retrieved
IStreamDatabase& m_stream;
//! Height request
int m_height = 0;
//! Size available per label along the ruler
uint64_t m_pixelsPerLabel = 20;
};
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,196 @@
#pragma once
#include <openvibe/ov_all.h>
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
class IStreamDisplayDrawable;
/**
* \brief Abstract class of objects than can be updated by an IStreamDatabase object
*/
class IStreamDisplayDrawable
{
public:
virtual bool init() = 0;
virtual bool redraw() = 0;
virtual ~IStreamDisplayDrawable() = default;
};
class IStreamDatabase
{
public:
virtual ~IStreamDatabase() = default;
/**
* \brief Initialize the database, including creating decoder
* \return True if initialization succeeded
*/
virtual bool initialize() = 0;
/**
* \brief Set drawable object to update.
* \param drawable drawable object to update.
*/
virtual void setDrawable(IStreamDisplayDrawable* drawable) = 0;
/**
* \brief Set flag stating whether or not to redraw upon new data reception
* \param redrawOnNewData Redraw flag
*/
virtual void setRedrawOnNewData(const bool redrawOnNewData) = 0;
/**
* \brief Determine whether first buffer has been received yet
* \return True if first buffer has been received already, false otherwise
*/
virtual bool hasFirstBuffer() = 0;
/**
* \brief Set max buffer count
* Set max buffer count directly (as opposed to computing it from time scale)
* \remarks This method sets m_ignoreTimeScale to true
*/
virtual bool setMaxBufferCount(const size_t count) = 0;
/**
* \brief Set time scale
* Computes the maximum number of buffers that can be displayed simultaneously
* \remarks This method sets m_ignoreTimeScale to false
* \param timeScale Time window's width in seconds.
* \return True if buffer count changed, false otherwise
*/
virtual bool setTimeScale(const double timeScale) = 0;
/**
* \brief Decode a memory buffer using proxy
* \param buffer Memory buffer to decode
* \param startTime Start time of memory buffer
* \param endTime End time of memory buffer
* \return True if memory buffer could be properly decoded, false otherwise
*/
virtual bool decodeMemoryBuffer(const IMemoryBuffer* buffer, const uint64_t startTime, const uint64_t endTime) = 0;
/**
* \brief Get number of buffers necessary to cover time scale
* \remarks Can't be computed before 2 buffers have been received, because
* the time step between the start of 2 consecutive buffers must be known
* \return Maximum number of buffers stored in this object
*/
virtual size_t getMaxBufferCount() = 0;
/**
* \brief Get current buffer count
* \return Current buffer count
*/
virtual size_t getCurrentBufferCount() = 0;
/**
* \brief Get pointer on a given buffer
* \param index Index of buffer to retrieve
* \return Buffer pointer if buffer exists, nullptr otherwise
*/
virtual const double* getBuffer(const size_t index) = 0;
/**
* \brief Get start time of a given buffer
* \param index Index of buffer whose start time is to be retrieved
* \return Start time if buffer exists, 0 otherwise
*/
virtual uint64_t getStartTime(const size_t index) = 0;
/**
* \brief Get end time of a given buffer
* \param index Index of buffer whose end time is to be retrieved
* \return End time if buffer exists, 0 otherwise
*/
virtual uint64_t getEndTime(const size_t index) = 0;
/**
* \brief Get number of elements contained in a buffer
* \return Buffer element count or 0 if no buffer has been received yet
*/
virtual size_t getBufferElementCount() = 0;
/**
* \brief Get time span covered by a buffer
* \return Buffer time span
*/
virtual uint64_t getBufferDuration() = 0;
/**
* \brief Determine whether buffer time step has been computed yet
* \return True if buffer time step has been computed
*/
virtual bool isBufferTimeStepComputed() = 0;
/**
* \brief Get time step between the start of 2 consecutive buffers
* \remarks This value can't be computed before the first 2 buffers are received
* \return Buffer time step
*/
virtual uint64_t getBufferTimeStep() = 0;
/**
* \brief Get number of samples per buffer
* \return Number of samples per buffer
*/
virtual size_t getSampleCountPerBuffer() = 0;
/**
* \brief Get number of channels
* \return Number of channels
*/
virtual size_t getChannelCount() = 0;
/**
* \brief Get channel label
* \param[in] index index of channel
* \param[out] label channel label
* \return true if channel label could be retrieved, false otherwise
*/
virtual bool getChannelLabel(const size_t index, CString& label) = 0;
/** \name Min/max values retrieval */
//@{
/**
* \brief Compute min/max values currently displayed for a given channel
* \param [in] index Index of channel
* \param [out] min Minimum displayed value for channel of interest
* \param [out] max Maximum displayed value for channel of interest
* \return True if values could be computed, false otherwise
*/
virtual bool getChannelMinMaxValues(const size_t index, double& min, double& max) = 0;
/**
* \brief Compute min/max values currently displayed, taking all channels into account
* \param [out] min Minimum displayed value
* \param [out] max Maximum displayed value
* \return True if values could be computed, false otherwise
*/
virtual bool getGlobalMinMaxValues(double& min, double& max) = 0;
/**
* \brief Compute min/max values in last buffer for a given channel
* \param [in] index Index of channel
* \param [out] min Minimum value for channel of interest
* \param [out] max Maximum value for channel of interest
* \return True if values could be computed, false otherwise
*/
virtual bool getLastBufferChannelMinMaxValues(const size_t index, double& min, double& max) = 0;
/**
* \brief Compute min/max values in last buffer, taking all channels into account
* \param [out] min Minimum value
* \param [out] max Maximum value
* \return True if values could be computed, false otherwise
*/
virtual bool getLastBufferGlobalMinMaxValues(double& min, double& max) = 0;
//@}
};
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,103 @@
#pragma once
// Boxes
//---------------------------------------------------------------------------------------------------
#define OVP_ClassId_SignalDisplay OpenViBE::CIdentifier(0x0055BE5F, 0x087BDD12)
#define OVP_ClassId_SignalDisplayDesc OpenViBE::CIdentifier(0x00C4F2D5, 0x58810276)
#define OVP_ClassId_DisplayCueImage OpenViBE::CIdentifier(0x005789A4, 0x3AB78A36)
#define OVP_ClassId_DisplayCueImageDesc OpenViBE::CIdentifier(0x086185A4, 0x796A854C)
#define OVP_ClassId_GrazVisualization OpenViBE::CIdentifier(0x00DD290D, 0x5F142820)
#define OVP_ClassId_GrazVisualizationDesc OpenViBE::CIdentifier(0x00F1955D, 0x38813A6A)
#define OVP_ClassId_GeneralizedGrazVisualization OpenViBE::CIdentifier(0xf0d1b4b9, 0xb420c213)
#define OVP_ClassId_GeneralizedGrazVisualizationDesc OpenViBE::CIdentifier(0x0d414c51, 0xb0ed32f2)
#define OVP_ClassId_PowerSpectrumDisplay OpenViBE::CIdentifier(0x004C0EA4, 0x713EC6D9)
#define OVP_ClassId_PowerSpectrumDisplayDesc OpenViBE::CIdentifier(0x00116B40, 0x69E1B00D)
#define OVP_ClassId_TopographicMap2DDisplay OpenViBE::CIdentifier(0x0B104632, 0x451C265F)
#define OVP_ClassId_TopographicMap2DDisplayDesc OpenViBE::CIdentifier(0x7154037A, 0x4BC52A9F)
#define OVP_ClassId_Simple3DDisplay OpenViBE::CIdentifier(0x31A00483, 0x35924E6B)
#define OVP_ClassId_Simple3DDisplayDesc OpenViBE::CIdentifier(0x443E145F, 0x77205DA0)
#define OVP_ClassId_TopographicMap3DDisplay OpenViBE::CIdentifier(0x36F95BE4, 0x0EF06290)
#define OVP_ClassId_TopographicMap3DDisplayDesc OpenViBE::CIdentifier(0x6AD52C48, 0x6E1C1746)
#define OVP_ClassId_VoxelDisplay OpenViBE::CIdentifier(0x76E42EA2, 0x66FB5265)
#define OVP_ClassId_VoxelDisplayDesc OpenViBE::CIdentifier(0x79321659, 0x642D3D0C)
#define OVP_ClassId_TimeFrequencyMapDisplay OpenViBE::CIdentifier(0x3AE63330, 0x76532117)
#define OVP_ClassId_TimeFrequencyMapDisplayDesc OpenViBE::CIdentifier(0x1BAE74F3, 0x20FB7C89)
#define OVP_ClassId_BoxAlgorithm_P300SpellerVisualization OpenViBE::CIdentifier(0x195E41D6, 0x6E684D47)
#define OVP_ClassId_BoxAlgorithm_P300SpellerVisualizationDesc OpenViBE::CIdentifier(0x31DE2B0D, 0x028202E7)
#define OVP_ClassId_BoxAlgorithm_P300IdentifierCardVisualization OpenViBE::CIdentifier(0x3AF7FF20, 0xA68745DB)
#define OVP_ClassId_BoxAlgorithm_P300IdentifierCardVisualizationDesc OpenViBE::CIdentifier(0x84F146EF, 0x4AA712A4)
#define OVP_ClassId_BoxAlgorithm_P300MagicCardVisualization OpenViBE::CIdentifier(0x841F46EF, 0x471AA2A4)
#define OVP_ClassId_BoxAlgorithm_P300MagicCardVisualizationDesc OpenViBE::CIdentifier(0x37FAFF20, 0xA74685DB)
#define OVP_ClassId_BoxAlgorithm_ErpPlot OpenViBE::CIdentifier(0x10DC6917, 0x2B29B2A0)
#define OVP_ClassId_BoxAlgorithm_ErpPlotDesc OpenViBE::CIdentifier(0x10DC6917, 0x2B29B2A0)
#define OVP_ClassId_BoxAlgorithm_LevelMeasure OpenViBE::CIdentifier(0x657138E4, 0x46D6586F)
#define OVP_ClassId_BoxAlgorithm_LevelMeasureDesc OpenViBE::CIdentifier(0x4D061428, 0x11B02233)
#define OVP_ClassId_Algorithm_LevelMeasure OpenViBE::CIdentifier(0x63C71764, 0x34A9717F)
#define OVP_ClassId_Algorithm_LevelMeasureDesc OpenViBE::CIdentifier(0x3EB6754F, 0x22FB1722)
// Global defines
//---------------------------------------------------------------------------------------------------
#ifdef TARGET_HAS_ThirdPartyOpenViBEPluginsGlobalDefines
#include "ovp_global_defines.h"
#endif // TARGET_HAS_ThirdPartyOpenViBEPluginsGlobalDefines
#define OV_AttributeId_Box_FlagIsUnstable OpenViBE::CIdentifier(0x666FFFFF, 0x666FFFFF)
#define OVP_TypeId_SphericalLinearInterpolationType OpenViBE::CIdentifier(0x44B76D9E, 0x618229BC)
#define OVP_TypeId_SignalDisplayMode OpenViBE::CIdentifier(0x5DE046A6, 0x086340AA)
#define OVP_TypeId_SignalDisplayScaling OpenViBE::CIdentifier(0x33A30739, 0x00D5299B)
#define OVP_TypeId_FeedbackMode OpenViBE::CIdentifier(0x5261636B, 0x464d4f44)
enum EInterpolationType { Spline = 1, Laplacian = 2 };
enum ESignalDisplayMode { Scroll, Scan };
enum ESignalDisplayScaling { PerChannel, Global, None }; // Note: the code relies on the following indexing starting from 0
enum EFeedbackMode { Positive, Best, All, No };
#define OVP_Algorithm_LevelMeasure_InputParameterId_Matrix OpenViBE::CIdentifier(0x59430053, 0x67C23A83)
#define OVP_Algorithm_LevelMeasure_OutputParameterId_MainWidget OpenViBE::CIdentifier(0x101C4641, 0x466C71E3)
#define OVP_Algorithm_LevelMeasure_OutputParameterId_ToolbarWidget OpenViBE::CIdentifier(0x14905FFC, 0x6FE425B2)
#define OVP_Algorithm_LevelMeasure_InputTriggerId_Reset OpenViBE::CIdentifier(0x3EAF36C5, 0x74490C56)
#define OVP_Algorithm_LevelMeasure_InputTriggerId_Refresh OpenViBE::CIdentifier(0x71356FE4, 0x3E8F62DC)
#define OVP_Algorithm_LevelMeasure_OutputTriggerId_Refreshed OpenViBE::CIdentifier(0x3C3C1B06, 0x360305D9)
#define OVP_ClassId_Algorithm_SphericalSplineInterpolation OpenViBE::CIdentifier(0x4F112803, 0x661D4029)
#define OVP_ClassId_Algorithm_SphericalSplineInterpolationDesc OpenViBE::CIdentifier(0x00D67A20, 0x3D3D4729)
#define OVP_Algorithm_SphericalSplineInterpolation_InputParameterId_SplineOrder OpenViBE::CIdentifier(0x3B8200F6, 0x205162C7)
#define OVP_Algorithm_SphericalSplineInterpolation_InputParameterId_ControlPointsCount OpenViBE::CIdentifier(0x2ABF11FC, 0x174A2CFE)
#define OVP_Algorithm_SphericalSplineInterpolation_InputParameterId_ControlPointsCoordinates OpenViBE::CIdentifier(0x36F743FE, 0x37897AB9)
#define OVP_Algorithm_SphericalSplineInterpolation_InputParameterId_ControlPointsValues OpenViBE::CIdentifier(0x4EA55599, 0x670274A7)
#define OVP_Algorithm_SphericalSplineInterpolation_InputParameterId_SamplePointsCoordinates OpenViBE::CIdentifier(0x280A531D, 0x339C18AA)
#define OVP_Algorithm_SphericalSplineInterpolation_OutputParameterId_SamplePointsValues OpenViBE::CIdentifier(0x12D0319C, 0x51ED4D8B)
#define OVP_Algorithm_SphericalSplineInterpolation_OutputParameterId_MinSamplePointValue OpenViBE::CIdentifier(0x0CEE2041, 0x79455EED)
#define OVP_Algorithm_SphericalSplineInterpolation_OutputParameterId_MaxSamplePointValue OpenViBE::CIdentifier(0x1ECB03E3, 0x40EF757F)
#define OVP_Algorithm_SphericalSplineInterpolation_InputTriggerId_PrecomputeTables OpenViBE::CIdentifier(0x42A650DA, 0x62B35F76)
#define OVP_Algorithm_SphericalSplineInterpolation_InputTriggerId_ComputeSplineCoefs OpenViBE::CIdentifier(0x5B353712, 0x069F3D3B)
#define OVP_Algorithm_SphericalSplineInterpolation_InputTriggerId_ComputeLaplacianCoefs OpenViBE::CIdentifier(0x7D8C545E, 0x7C086660)
#define OVP_Algorithm_SphericalSplineInterpolation_InputTriggerId_InterpolateSpline OpenViBE::CIdentifier(0x1241610E, 0x03CB1AD9)
#define OVP_Algorithm_SphericalSplineInterpolation_InputTriggerId_InterpolateLaplacian OpenViBE::CIdentifier(0x11CE0AC3, 0x0FD85469)
#define OVP_Algorithm_SphericalSplineInterpolation_OutputTriggerId_Error OpenViBE::CIdentifier(0x08CB0679, 0x3A6F3C3A)
// Some enumerations
//---------------------------------------------------------------------------------------------------
enum EDisplayMode { ZoomIn, ZoomOut, GlobalBestFit };
namespace OpenViBE {
namespace Kernel {
/**
* \brief Standard 3D objects
*/
enum EStandard3DObject
{
Standard3DObject_Invalid = -1,
Standard3DObject_Sphere,
Standard3DObject_Cone,
Standard3DObject_Cube,
Standard3DObject_Quad
};
} // namespace Kernel
} // namespace OpenViBE
@@ -0,0 +1,63 @@
#include "ovp_defines.h"
#include "algorithms/ovpCAlgorithmLevelMeasure.h"
//Presentation
#include "box-algorithms/ovpCBoxAlgorithmP300IdentifierCardVisualization.h"
#include "box-algorithms/ovpCBoxAlgorithmP300MagicCardVisualization.h"
#include "box-algorithms/ovpCBoxAlgorithmP300SpellerVisualization.h"
#include "box-algorithms/ovpCDisplayCueImage.h"
#include "box-algorithms/ovpCGrazMultiVisualization.h"
#include "box-algorithms/ovpCGrazVisualization.h"
//2D plugins
#include "box-algorithms/ovpCBoxAlgorithmErpPlot.h"
#include "box-algorithms/ovpCBoxAlgorithmLevelMeasure.h"
#include "box-algorithms/ovpCSignalDisplay.h"
namespace OpenViBE {
namespace Plugins {
namespace SimpleVisualization {
OVP_Declare_Begin()
context.getTypeManager().registerEnumerationEntry(OV_TypeId_BoxAlgorithmFlag, OV_AttributeId_Box_FlagIsUnstable.toString(),
OV_AttributeId_Box_FlagIsUnstable.id());
context.getTypeManager().registerEnumerationType(OVP_TypeId_SphericalLinearInterpolationType, "Spherical linear interpolation type");
context.getTypeManager().registerEnumerationEntry(OVP_TypeId_SphericalLinearInterpolationType, "Spline (potentials)", Spline);
context.getTypeManager().registerEnumerationEntry(OVP_TypeId_SphericalLinearInterpolationType, "Spline laplacian (currents)", Laplacian);
context.getTypeManager().registerEnumerationType(OVP_TypeId_SignalDisplayMode, "Signal display mode");
context.getTypeManager().registerEnumerationEntry(OVP_TypeId_SignalDisplayMode, "Scroll", Scroll);
context.getTypeManager().registerEnumerationEntry(OVP_TypeId_SignalDisplayMode, "Scan", Scan);
context.getTypeManager().registerEnumerationType(OVP_TypeId_SignalDisplayScaling, "Signal display scaling");
context.getTypeManager().registerEnumerationEntry(OVP_TypeId_SignalDisplayScaling, CSignalDisplayView::SCALING_MODES[PerChannel].c_str(), PerChannel);
context.getTypeManager().registerEnumerationEntry(OVP_TypeId_SignalDisplayScaling, CSignalDisplayView::SCALING_MODES[Global].c_str(), Global);
context.getTypeManager().registerEnumerationEntry(OVP_TypeId_SignalDisplayScaling, CSignalDisplayView::SCALING_MODES[None].c_str(), None);
context.getTypeManager().registerEnumerationType(OVP_TypeId_FeedbackMode, "Feedback display mode");
context.getTypeManager().registerEnumerationEntry(OVP_TypeId_FeedbackMode, "Positive Only", Positive);
context.getTypeManager().registerEnumerationEntry(OVP_TypeId_FeedbackMode, "Best Only", Best);
context.getTypeManager().registerEnumerationEntry(OVP_TypeId_FeedbackMode, "All", All);
context.getTypeManager().registerEnumerationEntry(OVP_TypeId_FeedbackMode, "None", No);
OVP_Declare_New(CDisplayCueImageDesc)
OVP_Declare_New(CSignalDisplayDesc)
OVP_Declare_New(CAlgorithmLevelMeasureDesc)
OVP_Declare_New(CGrazVisualizationDesc)
OVP_Declare_New(CGrazMultiVisualizationDesc)
OVP_Declare_New(CBoxAlgorithmP300SpellerVisualizationDesc)
OVP_Declare_New(CBoxAlgorithmP300IdentifierCardVisualizationDesc)
OVP_Declare_New(CBoxAlgorithmP300MagicCardVisualizationDesc)
OVP_Declare_New(CBoxAlgorithmLevelMeasureDesc)
OVP_Declare_New(CBoxAlgorithmErpPlotDesc)
OVP_Declare_End()
} // namespace SimpleVisualization
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,50 @@
#pragma once
#include <openvibe/ov_all.h>
#include <gtk/gtk.h>
#include <sstream>
//---------------------------------------------------------------------------------------------------
/// <summary> Initializes the color of the GDK. with old compiler as vs2013 we can't initialize structure easily.....</summary>
/// <param name="pixel"> For allocated colors, the pixel value used to draw this color on the screen.Not used anymore.</param>
/// <param name="r"> The red component of the color. This is a value between 0 and 65535, with 65535 indicating full intensity.</param>
/// <param name="g"> The green component of the color.</param>
/// <param name="b"> The blue component of the color.</param>
/// <returns> The initialized color (</returns>
inline GdkColor InitGDKColor(const guint32 pixel = 0, const guint16 r = 0, const guint16 g = 0, const guint16 b = 0)
{
GdkColor c;
c.pixel = pixel;
c.red = r;
c.green = g;
c.blue = b;
return c;
}
//---------------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------
class CGdkcolorAutoCast
{
public:
CGdkcolorAutoCast(const OpenViBE::Kernel::IBox& box, OpenViBE::Kernel::IConfigurationManager& configManager, const size_t index)
: m_configManager(configManager)
{
box.getSettingValue(index, m_settingValue);
m_settingValue = m_configManager.expand(m_settingValue);
}
operator GdkColor() const
{
std::stringstream ss(m_settingValue.toASCIIString());
int r = 0, g = 0, b = 0;
char c;
ss >> r >> c >> g >> c >> b;
return InitGDKColor(0, guint16(r * 655.35), guint16(g * 655.35), guint16(b * 655.35));
}
protected:
OpenViBE::Kernel::IConfigurationManager& m_configManager;
OpenViBE::CString m_settingValue;
};
//---------------------------------------------------------------------------------------------------