DataView.cpp
Go to the documentation of this file.
1
2#include "DataView.h"
3
4#include <QApplication>
5#include <QClipboard>
6#include <QHBoxLayout>
7#include <QHeaderView>
8#include <QLabel>
9#include <QSplitter>
10#include <QTreeWidget>
11
12#include <SimoxUtility/color/cmaps.h>
13#include <SimoxUtility/math/SoftMinMax.h>
14
16#include <RobotAPI/libraries/armem/aron/MemoryID.aron.generated.h>
27
29{
31 splitter(new QSplitter(Qt::Orientation::Vertical)), tree(new QTreeWidget(this))
32 {
33 Logging::setTag("DataView");
34
35 QLayout* layout = new QVBoxLayout();
36 this->setLayout(layout);
37 int margin = 3;
38 layout->setContentsMargins(margin, margin, margin, margin);
39
40 layout->addWidget(splitter);
41
42 splitter->addWidget(tree);
43
44 QStringList columns;
45 columns.insert(int(Columns::KEY), "Key");
46 columns.insert(int(Columns::VALUE), "Value");
47 columns.insert(int(Columns::TYPE), "Type");
48 tree->setColumnCount(columns.size());
49 tree->setHeaderLabels(columns);
50
51 tree->header()->resizeSection(int(Columns::KEY), 250);
52 tree->header()->resizeSection(int(Columns::VALUE), 250);
53
54 treeItemData = new QTreeWidgetItem({"Data"});
55 tree->addTopLevelItem(treeItemData);
56 treeItemData->setExpanded(true);
57 tree->setContextMenuPolicy(Qt::CustomContextMenu);
58 connect(tree,
59 &QTreeWidget::customContextMenuRequested,
60 this,
62 }
63
64 void
66 {
67 this->statusLabel = statusLabel;
68 }
69
70 void
72 {
73 this->useTypeInfo = enable;
74 update();
75 emit useTypeInfoChanged(enable);
76 }
77
78 void
80 {
81 // ARMARX_IMPORTANT << "Adding instance view with toolbar for instance: " << instance.id();
82 dataView->setStatusLabel(statusLabel);
83 dataView->setUseTypeInfo(useTypeInfo);
84
85 auto* child = new WidgetsWithToolbar();
86 child->addWidget(dataView);
87
88
89 splitter->addWidget(child);
90
91 // Propagate these signals upwards.
92 connect(dataView,
94 this,
98 }
99
100 void
102 {
103 if (!data)
104 {
105 treeItemData->setText(int(Columns::TYPE), QString::fromStdString(""));
106
108 QTreeWidgetItem* item = new QTreeWidgetItem({"(No data.)"});
109 treeItemData->addChild(item);
110 }
111 else if (useTypeInfo && aronType)
112 {
113 treeItemData->setText(
114 int(Columns::TYPE),
115 QString::fromStdString(sanitizeTypeName(aronType->getFullName())));
116
117 TypedDataTreeBuilder builder;
118 builder.setColumns(int(Columns::KEY), int(Columns::VALUE), int(Columns::TYPE));
119 builder.updateTree(treeItemData, *aronType, *data);
120 }
121 else
122 {
123 treeItemData->setText(int(Columns::TYPE), QString::fromStdString(""));
124
125 DataTreeBuilder builder;
126 builder.setColumns(int(Columns::KEY), int(Columns::VALUE), int(Columns::TYPE));
127 builder.updateTree(treeItemData, data);
128 }
129 treeItemData->setExpanded(true);
130 }
131
132 void
133 DataView::showErrorMessage(const std::string& message)
134 {
135 if (statusLabel)
136 {
137 statusLabel->setText(QString::fromStdString(message));
138 }
139 }
140
141 std::optional<aron::Path>
142 DataView::getElementPath(const QTreeWidgetItem* item)
143 {
144 QStringList qpath = item->data(int(Columns::KEY), Qt::UserRole).toStringList();
145 if (qpath.empty())
146 {
147 return std::nullopt;
148 }
149 else
150 {
151 aron::Path path = deserializePath(qpath);
152 return path;
153 }
154 }
155
156 std::optional<MemoryID>
158 {
160 if (!data)
161 {
162 showErrorMessage("Cannot get Memory ID for null element.");
163 return std::nullopt;
164 }
165
167 try
168 {
169 element = data->navigateAbsolute(elementPath);
170 }
171 // This can happen when the underlying entity structure changes (a new entity has been selected).
172 catch (const aron::error::AronException&)
173 {
174 // showErrorMessage(e.what());
175 return std::nullopt;
176 }
177 catch (const armarx::LocalException& e)
178 {
179 showErrorMessage(e.what());
180 return std::nullopt;
181 }
182
183 std::stringstream couldNotParseMsg;
184 couldNotParseMsg << "Element " << elementPath.toString()
185 << " could not be parsed as MemoryID.";
186
187 auto dictElement = std::dynamic_pointer_cast<aron::data::Dict>(element);
188 if (!dictElement)
189 {
190 showErrorMessage(couldNotParseMsg.str() + " (Failed to cast to DictNavigator.)");
191 return std::nullopt;
192 }
193
194 try
195 {
196 arondto::MemoryID dto;
197 dto.fromAron(dictElement);
198
199 MemoryID id;
200 armem::fromAron(dto, id);
201 return id;
202 }
204 {
205 showErrorMessage(couldNotParseMsg.str());
206 return std::nullopt;
207 }
208 }
209
210 QAction*
211 DataView::makeActionResolveMemoryID(const MemoryID& id)
212 {
213 auto* action = new QAction("Resolve memory ID");
214
215 if (not(id.hasEntityName() and id.isWellDefined()))
216 {
217 action->setDisabled(true);
218 action->setText(action->text() + " (incomplete Memory ID)");
219 }
220 connect(action,
221 &QAction::triggered,
222 [this, id]()
223 {
224 // ARMARX_IMPORTANT << "emit memoryIdResolutionRequested(id = " << id << ")";
226 });
227
228 return action;
229 }
230
231 QAction*
233 {
234 QAction* action = new QAction("Copy memory ID to clipboard");
235
236 connect(action,
237 &QAction::triggered,
238 [/*this,*/ id]() // `this` for ARMARX_IMPORTANT
239 {
240 const QString idStr = QString::fromStdString(id.str());
241
242 // ARMARX_IMPORTANT << "Copy '" << idStr.toStdString() << "' to clipboard.";
243 QClipboard* clipboard = QApplication::clipboard();
244 clipboard->setText(idStr);
245 QApplication::processEvents();
246 });
247
248 return action;
249 }
250
251 std::vector<QAction*>
252 DataView::makeActionsCopyDataToClipboard()
253 {
254 auto data = getData();
255 if (!data)
256 {
257 return {};
258 }
259 return makeCopyActions(data, currentAronType);
260 }
261
262 std::vector<QAction*>
263 DataView::makeActionsCopyDataToClipboard(const aron::Path& path)
264 {
265 auto data = getData();
266 if (!data)
267 {
268 return {};
269 }
270 try
271 {
272 aron::data::VariantPtr element = data->navigateAbsolute(path);
273 aron::type::VariantPtr elementType = nullptr;
274 if (currentAronType)
275 {
276 // There doesn't seem to be a way to check whether the path exists
277 // without potentially throwing an exception.
278 try
279 {
280 elementType = currentAronType->navigateAbsolute(path);
281 }
282 catch (const aron::error::AronException& e)
283 {
284 // No type available, elementType remains nullptr.
285 }
286 }
287 return makeCopyActions(element, elementType);
288 }
289 catch (const aron::error::AronException& e)
290 {
291 ARMARX_WARNING << "Could not convert Aron data to JSON: " << e.getReason();
292 }
293 return {};
294 }
295
296 std::vector<QAction*>
297 DataView::makeCopyActions(const aron::data::VariantPtr& element,
298 const aron::type::VariantPtr& elementType)
299 {
300 auto* easyJsonAction = new QAction("Copy data to clipboard as easy JSON");
301 connect(easyJsonAction,
302 &QAction::triggered,
303 [this, element, elementType]()
304 {
305 try
306 {
307 TreeTypedJSONConverter conv;
308 armarx::aron::data::visitRecursive(conv, element, elementType);
309 QClipboard* clipboard = QApplication::clipboard();
310 clipboard->setText(QString::fromStdString(conv.getJSON().dump(2)));
311 QApplication::processEvents();
312 }
313 catch (const aron::error::AronException& e)
314 {
315 ARMARX_WARNING << "Could not convert Aron data to JSON: " << e.getReason();
316 }
317 });
318
319 auto* aronJsonAction = new QAction("Copy data to clipboard as aron JSON");
320 connect(aronJsonAction,
321 &QAction::triggered,
322 [this, element]()
323 {
324 try
325 {
326 nlohmann::json json =
328 element);
329 QClipboard* clipboard = QApplication::clipboard();
330 clipboard->setText(QString::fromStdString(json.dump(2)));
331 QApplication::processEvents();
332 }
333 catch (const aron::error::AronException& e)
334 {
335 ARMARX_WARNING << "Could not convert Aron data to JSON: " << e.getReason();
336 }
337 });
338
339 return {easyJsonAction, aronJsonAction};
340 }
341
342 QMenu*
343 DataView::buildActionsMenu(const QPoint& pos)
344 {
345 QMenu* menu = new QMenu(this);
346
347 const QTreeWidgetItem* item = tree->itemAt(pos);
348 if (item == nullptr)
349 {
350 return menu; // Nothing was clicked on.
351 }
352
353 if (item == this->treeItemData && getData() != nullptr)
354 {
355 auto actions = makeActionsCopyDataToClipboard();
356 for (const auto& action : actions)
357 {
358 if (action)
359 {
360 menu->addAction(action);
361 }
362 }
363 }
364
366 item->data(int(Columns::TYPE), Qt::UserRole).toInt());
367 switch (type)
368 {
370 {
371 if (const std::optional<aron::Path> path = getElementPath(item))
372 {
373 QAction* viewAction = new QAction("Show image");
374 menu->addAction(viewAction);
375 connect(viewAction,
376 &QAction::triggered,
377 [this, path]() { this->showImageView(path.value()); });
378
379 try
380 {
381 aron::data::VariantPtr element =
382 getData() != nullptr ? getData()->navigateAbsolute(path.value())
383 : nullptr;
384 if (auto imageData = aron::data::NDArray::DynamicCast(element))
385 {
386 const std::vector<int> shape = imageData->getShape();
387 if (std::find(shape.begin(), shape.end(), 0) != shape.end())
388 {
389 viewAction->setText(viewAction->text() + " (image is empty)");
390 viewAction->setEnabled(false);
391 }
392 }
393 }
394 catch (const aron::error::AronException&)
395 {
396 }
397 catch (const armarx::LocalException&)
398 {
399 }
400 }
401 }
402 break;
403 default:
404 break;
405 }
406
407 // Type name based actions
408 const std::string typeName = item->text(int(Columns::TYPE)).toStdString();
410 {
411 if (const std::optional<aron::Path> path = getElementPath(item))
412 {
413 if (std::optional<MemoryID> id = getElementMemoryID(path.value()))
414 {
415 if (QAction* action = makeActionCopyMemoryID(id.value()))
416 {
417 menu->addAction(action);
418 }
419 if (QAction* action = makeActionResolveMemoryID(id.value()))
420 {
421 menu->addAction(action);
422 }
423 }
424 }
425 }
426
427 const std::optional<aron::Path> elementPath = getElementPath(item);
428 if (elementPath)
429 {
430 auto actions = makeActionsCopyDataToClipboard(elementPath.value());
431 for (const auto& action : actions)
432 {
433 if (action)
434 {
435 menu->addAction(action);
436 }
437 }
438 }
439 return menu;
440 }
441
442 void
444 {
445 auto* menu = buildActionsMenu(pos);
446
447 if (menu->actions().isEmpty())
448 {
449 emit actionsMenuRequested(MemoryID(), this, tree->mapToGlobal(pos), nullptr);
450 }
451 else
452 {
453 emit actionsMenuRequested(MemoryID(), this, tree->mapToGlobal(pos), menu);
454 }
455 }
456
457 void
459 {
460 auto data = getData();
461 if (!data)
462 {
463 return;
464 }
465 if (!imageView)
466 {
467 WidgetsWithToolbar* toolbar = new WidgetsWithToolbar();
468
469 imageView = new ImageView();
470 imageView->toolbar = toolbar;
471 toolbar->addWidget(imageView);
472
473 splitter->addWidget(toolbar);
474
475 connect(toolbar, &WidgetsWithToolbar::closing, [this]() { imageView = nullptr; });
476 }
477 imageView->elementPath = elementPath;
479 }
480
481 void
483 {
484 imageView->toolbar->close();
485 imageView = nullptr;
486 }
487
488 QImage
490 {
491 const std::vector<int> shape = aron.getShape();
492 ARMARX_CHECK_EQUAL(shape.size(), 3);
493 ARMARX_CHECK_EQUAL(shape.at(2), 4) << "Expected Depth32 image to have 4 bytes per pixel.";
494
495 const int rows = shape.at(0);
496 const int cols = shape.at(1);
497
498 // Rendering seems to be optimized for RGB32
499 // rows go along 0 = height, cols go along 1 = width
500 QImage image(cols, rows, QImage::Format::Format_RGB32);
501 const float* data = reinterpret_cast<float*>(aron.getData());
502
503 auto updateLimits = [](float value, Limits& limits)
504 {
505 if (value > 0) // Exclude 0 from normalization (it may be only background)
506 {
507 limits.min = std::min(limits.min, value);
508 }
509 limits.max = std::max(limits.max, value);
510 };
511
512 // Find data range and adapt cmap.
513 Limits limits;
514 if (limitsHistory.empty())
515 {
516 const float* sourceRow = data;
517 for (int row = 0; row < rows; ++row)
518 {
519 for (int col = 0; col < cols; ++col)
520 {
521 float value = sourceRow[col];
522 updateLimits(value, limits);
523 }
524 sourceRow += cols;
525 }
526 cmap.set_vlimits(limits.min, limits.max);
527 }
528 // Only do it at the beginning and stop after enough samples were collected.
529 else if (limitsHistory.size() < limitsHistoryMaxSize)
530 {
531 simox::math::SoftMinMax softMin(0.25, limitsHistory.size());
532 simox::math::SoftMinMax softMax(0.25, limitsHistory.size());
533
534 for (auto& l : limitsHistory)
535 {
536 softMin.add(l.min);
537 softMax.add(l.max);
538 }
539
540 cmap.set_vlimits(softMin.getSoftMin(), softMax.getSoftMax());
541 }
542
543 // Update image
544 {
545 const float* sourceRow = data;
546
547 const int bytesPerLine = image.bytesPerLine();
548 uchar* targetRow = image.bits();
549
550 for (int row = 0; row < rows; ++row)
551 {
552 for (int col = 0; col < cols; ++col)
553 {
554 float value = sourceRow[col];
555 simox::Color color = value <= 0 ? simox::Color::white() : cmap(value);
556 targetRow[col * 4 + 0] = color.b;
557 targetRow[col * 4 + 1] = color.g;
558 targetRow[col * 4 + 2] = color.r;
559 targetRow[col * 4 + 3] = color.a;
560
561 updateLimits(value, limits);
562 }
563 sourceRow += cols;
564 targetRow += bytesPerLine;
565 }
566 }
568 {
569 limitsHistory.push_back(limits);
570 }
571
572 return image;
573 }
574
575 void
577 {
579
580 if (not imageView)
581 {
582 return;
583 }
584 if (not data)
585 {
587 return;
588 }
589
591 try
592 {
593 element = data->navigateAbsolute(imageView->elementPath);
594 }
595 // This can happen when the underlying entity structure changes (a new entity has been selected).
596 // In this case, we disable the image view.
597 catch (const aron::error::AronException&)
598 {
599 // showErrorMessage(e.what());
601 return;
602 }
603 catch (const armarx::LocalException&)
604 {
605 // showErrorMessage(e.what());
607 return;
608 }
609
610 NDArray::PointerType imageData = NDArray::DynamicCast(element);
611 if (not imageData)
612 {
613 showErrorMessage("Expected NDArrayNavigator, but got: " +
614 simox::meta::get_type_name(element));
615 return;
616 }
617
618 const std::vector<int> shape = imageData->getShape();
619 if (shape.size() != 3)
620 {
621 showErrorMessage("Expected array shape with 3 dimensions, but got: " +
622 NDArray::DimensionsToString(shape));
623 return;
624 }
625 const int rows = shape.at(0);
626 const int cols = shape.at(1);
627
628 using aron::type::image::PixelType;
629 std::optional<PixelType> pixelType;
630 try
631 {
632 // TODO We cannot know what the str in the pixeltype belongs to (e.g. coming from java, python, c++ it may contain different values!
633 // pixelType = aron::type::Image::pixelTypeFromName(imageData->getType());
634
635 // For now we assume it comes from c++ where '5' means CV_32FC1 (=5)
636 pixelType = (imageData->getType() == "5" ? PixelType::DEPTH32 : PixelType::RGB24);
637 }
638 catch (const aron::error::AronException&)
639 {
640 }
641
642 bool clearLimitsHistory = true;
643 std::optional<QImage> image;
644 if (pixelType)
645 {
646 switch (pixelType.value())
647 {
648 case PixelType::RGB24:
649 ARMARX_CHECK_EQUAL(shape.at(2), 3)
650 << "Expected Rgb24 image to have 3 bytes per pixel.";
651 image = QImage(imageData->getData(), cols, rows, QImage::Format::Format_RGB888);
652 break;
653
654 case PixelType::DEPTH32:
655 image = imageView->convertDepth32ToRGB32(*imageData);
656 clearLimitsHistory = false;
657 break;
658 }
659 }
660 else
661 {
662 QImage::Format format = QImage::Format_Invalid;
663 switch (shape.at(2))
664 {
665 case 1:
666 format = QImage::Format::Format_Grayscale8;
667 break;
668
669 case 3:
670 format = QImage::Format::Format_RGB888;
671 break;
672
673 default:
674 showErrorMessage("Expected 1 or 3 elements in last dimension, but got shape: " +
675 NDArray::DimensionsToString(shape));
676 return;
677 }
678 image = QImage(imageData->getData(), cols, rows, format);
679 }
680
681 ARMARX_CHECK(image.has_value());
682
683 std::stringstream title;
684 title << "Image element '" << imageView->elementPath.toString()
685 << "'"; // of entity instance " << currentInstance->id();
686 imageView->setTitle(QString::fromStdString(title.str()));
687 imageView->view->setImage(image.value());
688
689 if (clearLimitsHistory)
690 {
691 imageView->limitsHistory.clear();
692 }
693 }
694
696 cmap(simox::color::cmaps::plasma().reversed()), limitsHistoryMaxSize(32)
697 {
698 setLayout(new QHBoxLayout());
699 int margin = 2;
700 layout()->setContentsMargins(margin, margin, margin, margin);
701 if (/* DISABLES CODE */ (false))
702 {
703 QFont font = this->font();
704 font.setPointSizeF(font.pointSize() * 0.75);
705 setFont(font);
706 }
707
709 layout()->addWidget(view);
710 }
711
712} // namespace armarx::armem::gui::instance
uint8_t data[1]
std::string str(const T &t)
void setTag(const LogTag &tag)
Definition Logging.cpp:54
void updateTree(QTreeWidgetItem *parent, const aron::data::DictPtr &data)
const size_t limitsHistoryMaxSize
In this context, n.
Definition DataView.h:127
QImage convertDepth32ToRGB32(const aron::data::NDArray &aron)
Definition DataView.cpp:489
simox::ColorMap cmap
Color map to visualize depth images.
Definition DataView.h:123
std::deque< Limits > limitsHistory
History over first n extremal depth values used to calibrate the colormap.
Definition DataView.h:125
void memoryIdResolutionRequested(const MemoryID &id)
void addDataView(DataView *dataView)
Definition DataView.cpp:79
QAction * makeActionCopyMemoryID(const MemoryID &id)
Definition DataView.cpp:232
void actionsMenuRequested(const MemoryID &memoryID, QWidget *parent, const QPoint &pos, QMenu *menu)
virtual void updateData(const aron::data::DictPtr &data, aron::type::ObjectPtr aronType=nullptr)
Definition DataView.cpp:101
virtual aron::data::DictPtr getData()=0
void showErrorMessage(const std::string &message)
Definition DataView.cpp:133
static std::optional< aron::Path > getElementPath(const QTreeWidgetItem *item)
Definition DataView.cpp:142
void updateImageView(const aron::data::DictPtr &data)
Definition DataView.cpp:576
void showImageView(const aron::Path &elementPath)
Definition DataView.cpp:458
aron::type::ObjectPtr currentAronType
Definition DataView.h:96
virtual QMenu * buildActionsMenu(const QPoint &pos)
Definition DataView.cpp:343
void setStatusLabel(QLabel *statusLabel)
Definition DataView.cpp:65
std::optional< MemoryID > getElementMemoryID(const aron::Path &elementPath)
Definition DataView.cpp:157
virtual void prepareTreeContextMenu(const QPoint &pos)
Definition DataView.cpp:443
A widget drawing an image in itself.
Definition ImageView.h:34
void updateTree(QTreeWidgetItem *parent, const aron::type::Dict &type, const aron::data::Dict &data)
The Path class.
Definition Path.h:36
std::string toString() const
Definition Path.cpp:127
static nlohmann::json ConvertToNlohmannJSON(const data::VariantPtr &)
A base class for aron exceptions.
Definition Exception.h:37
#define ARMARX_CHECK(expression)
Shortcut for ARMARX_CHECK_EXPRESSION.
#define ARMARX_CHECK_EQUAL(lhs, rhs)
This macro evaluates whether lhs is equal (==) rhs and if it turns out to be false it will throw an E...
#define ARMARX_WARNING
The logging level for unexpected behaviour, but not a serious problem.
Definition Logging.h:193
aron::Path deserializePath(const QStringList &qpath)
std::string sanitizeTypeName(const std::string &typeName)
const std::string sanitizedMemoryIDTypeName
void fromAron(const arondto::MemoryID &dto, MemoryID &bo)
std::shared_ptr< Dict > DictPtr
Definition Dict.h:42
std::shared_ptr< Variant > VariantPtr
void visitRecursive(RecursiveVisitorImplementation &v, typename RecursiveVisitorImplementation::Input &o)
std::shared_ptr< Object > ObjectPtr
Definition Object.h:36
std::shared_ptr< Variant > VariantPtr
void clearItem(QTreeWidgetItem *item)
Clear a tree widget item.
Definition gui_utils.cpp:35
int toInt(const std::string &input)
Definition Impl.cpp:41