init
This commit is contained in:
+157
@@ -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
|
||||
+85
@@ -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
|
||||
+643
@@ -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
|
||||
+212
@@ -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
|
||||
+122
@@ -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
|
||||
+72
@@ -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
|
||||
+485
@@ -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
|
||||
+160
@@ -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
|
||||
+477
@@ -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
|
||||
+160
@@ -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
|
||||
+730
@@ -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
|
||||
+184
@@ -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
|
||||
+492
@@ -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
|
||||
+209
@@ -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
|
||||
+710
@@ -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
|
||||
+286
@@ -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
|
||||
+677
@@ -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
|
||||
+162
@@ -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
|
||||
+249
@@ -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
|
||||
+141
@@ -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
|
||||
+980
@@ -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
|
||||
+267
@@ -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
|
||||
+137
@@ -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
|
||||
+52
@@ -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
|
||||
+1362
File diff suppressed because it is too large
Load Diff
+259
@@ -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
|
||||
+167
@@ -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
|
||||
+65
@@ -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
|
||||
+663
@@ -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
|
||||
+367
@@ -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
|
||||
+53
@@ -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
|
||||
+74
@@ -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
|
||||
+290
@@ -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
|
||||
+101
@@ -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
|
||||
+117
@@ -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
|
||||
+72
@@ -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
|
||||
+196
@@ -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
|
||||
Executable
+103
@@ -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
|
||||
Executable
+63
@@ -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
|
||||
+50
@@ -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;
|
||||
};
|
||||
|
||||
//---------------------------------------------------------------------------------------------------
|
||||
Reference in New Issue
Block a user