Skip to content
Snippets Groups Projects
MemoryViewer.cpp 34.31 KiB
#include "MemoryViewer.h"

#include <QApplication>
#include <QBoxLayout>
#include <QCheckBox>
#include <QClipboard>
#include <QDialog>
#include <QGroupBox>
#include <QLabel>
#include <QLayout>
#include <QMenu>
#include <QSettings>
#include <QTimer>

#include <Ice/Exception.h>

#include <SimoxUtility/algorithm/get_map_keys_values.h>

#include <ArmarXCore/core/ManagedIceObject.h>
#include <ArmarXCore/core/time/TimeUtil.h>
#include <ArmarXCore/observers/variant/Variant.h>
#include <ArmarXCore/core/ice_conversions/ice_conversions_templates.h>

#include <ArmarXGui/libraries/SimpleConfigDialog/SimpleConfigDialog.h>

#include <RobotAPI/libraries/aron/converter/json/NLohmannJSONConverter.h>
#include <RobotAPI/interface/armem/actions.h>
#include <RobotAPI/interface/armem/memory.h>
#include <RobotAPI/interface/armem/mns/MemoryNameSystemInterface.h>
#include <RobotAPI/libraries/armem/core/container_maps.h>
#include <RobotAPI/libraries/armem/core/wm/ice_conversions.h>
#include <RobotAPI/libraries/armem/server/query_proc/ltm/disk/ltm.h>
#include <RobotAPI/libraries/armem/server/query_proc/wm/wm.h>
#include <RobotAPI/libraries/armem_gui/ActionsMenuBuilder.h>
#include <RobotAPI/libraries/armem_gui/gui_utils.h>
#include <RobotAPI/libraries/armem_gui/instance/AronDataView.h>
#include <RobotAPI/libraries/armem_gui/instance/tree_visitors/TreeTypedJSONConverter.h>


namespace armarx::armem::gui
{
    MemoryViewer::MemoryViewer(QBoxLayout* updateWidgetLayout,
                               QGroupBox* memoryGroupBox,
                               QLayout* memoryGroupBoxParentLayout,
                               QGroupBox* instanceGroupBox,
                               QLayout* instanceGroupBoxParentLayout,
                               QBoxLayout* diskControlWidgetLayout,
                               QLabel* statusLabel)
    {
        Logging::setTag("MemoryViewer");

        this->statusLabel = statusLabel;
        this->statusLabel->clear();

        statusLabel->setContextMenuPolicy(Qt::CustomContextMenu);
        connect(statusLabel,
                &QLabel::customContextMenuRequested,
                [statusLabel](const QPoint& pos)
                {
                    QMenu menu(statusLabel);
                    menu.addAction("Copy to clipboard",
                                   [statusLabel]()
                                   { QApplication::clipboard()->setText(statusLabel->text()); });
                    menu.addAction("Clear status", [statusLabel]() { statusLabel->clear(); });
                    menu.exec(statusLabel->mapToGlobal(pos));
                });


        // Update timer
        this->updateWidgetLayout = updateWidgetLayout;
        updateWidget = new armem::gui::PeriodicUpdateWidget(2.0, 60);
        updateWidgetLayout->insertWidget(0, updateWidget);

        periodicUpdateTimer = new QTimer(this);
        periodicUpdateTimer->setInterval(1000 / 60); // Keep this stable.

        // Memory View
        auto retrieveEntityInfo = [this](const MemoryID& entityID) -> PredictionWidget::EntityInfo
        {
            client::Reader reader = memoryReaders.at(entityID.memoryName);
            if (!reader.predictionPrx)
            {
                std::stringstream sstream;
                sstream << "Predictions are not available for memory '" << entityID.memoryName
                   << "'.";
                this->statusLabel->setText(QString::fromStdString(sstream.str()));
                return {};
            }

            std::map<MemoryID, std::vector<PredictionEngine>> predictionEngines;
            client::QueryResult queryResult;
            try
            {
                predictionEngines = reader.getAvailablePredictionEngines();
                queryResult = reader.queryMemoryIDs({entityID}, armem::query::DataMode::NoData);
            }
            catch (const Ice::LocalException& e)
            {
                std::stringstream sstream;
                sstream << "Could not get prediction engines and type from memory: "
                        << e.what();
                this->statusLabel->setText(QString::fromStdString(sstream.str()));
                return {nullptr, {}};
            }

            aron::type::ObjectPtr entityType;
            if (queryResult.success)
            {
                auto* providerSegment = queryResult.memory.findProviderSegment(entityID);
                if (providerSegment != nullptr)
                {
                    entityType = providerSegment->aronType();
                }
            }
            return PredictionWidget::EntityInfo{
                .type = entityType,
                .engines = armem::accumulateEntriesContainingID(predictionEngines, entityID)
            };
        };
        memoryGroup = new armem::gui::MemoryGroupBox(std::move(retrieveEntityInfo));

        armarx::gui::replaceWidget(memoryGroupBox, memoryGroup, memoryGroupBoxParentLayout);
        ARMARX_CHECK_NULL(memoryGroupBox);

        // Instance View
        this->instanceGroup = new armem::gui::InstanceGroupBox();
        armarx::gui::replaceWidget(instanceGroupBox, instanceGroup, instanceGroupBoxParentLayout);
        this->instanceGroup->setStatusLabel(statusLabel);
        ARMARX_CHECK_NULL(instanceGroupBox);

        // Disk Control
        if (diskControlWidgetLayout)
        {
            this->diskControlLayout = diskControlWidgetLayout;
            diskControl = new armem::gui::disk::ControlWidget();
            diskControlWidgetLayout->addWidget(diskControl);
        }

        // Connections
        connect(diskControl,
                &armem::gui::disk::ControlWidget::requestedStoreOnDisk,
                this,
                &This::storeOnDisk);
        connect(diskControl,
                &armem::gui::disk::ControlWidget::requestedLoadFromDisk,
                this,
                &This::loadFromDisk);

        connect(this, &This::connected, this, &This::startQueries);
        connect(this, &This::connected, this, &This::startPeriodicUpdateTimer);
        connect(updateWidget, &armem::gui::PeriodicUpdateWidget::update, this, &This::startQueries);

        connect(periodicUpdateTimer, &QTimer::timeout, this, &This::updateListOfActiveMemories);
        connect(periodicUpdateTimer, &QTimer::timeout, this, &This::processQueryResults);

        connect(memoryGroup->queryWidget(), &armem::gui::QueryWidget::storeInLTM, this, &This::queryAndStoreInLTM);
        connect(memoryGroup->queryWidget(), &armem::gui::QueryWidget::startRecording, this, &This::startLTMRecording);
        connect(memoryGroup->queryWidget(), &armem::gui::QueryWidget::stopRecording, this, &This::stopLTMRecording);

        connect(memoryGroup->predictionWidget(),
                &armem::gui::PredictionWidget::makePrediction,
                this,
                &This::makePrediction);
        connect(
            memoryGroup->commitWidget(), &armem::gui::CommitWidget::commit, this, &This::commit);

        connect(this, &This::memoryDataChanged, this, &This::updateMemoryTree);
        connect(memoryGroup->tree(),
                &armem::gui::MemoryTreeWidget::selectedItemChanged,
                this,
                &This::updateInstanceTree);

        connect(memoryGroup->tree(),
                &armem::gui::MemoryTreeWidget::updated,
                this,
                &This::memoryTreeUpdated);
        connect(memoryGroup->tree(),
                &armem::gui::MemoryTreeWidget::actionsMenuRequested,
                this,
                &This::showActionsMenu);
        connect(instanceGroup,
                &armem::gui::InstanceGroupBox::viewUpdated,
                this,
                &This::instanceTreeUpdated);
        connect(instanceGroup->view,
                &armem::gui::InstanceView::memoryIdResolutionRequested,
                this,
                &This::resolveMemoryID);
        connect(instanceGroup->view,
                &armem::gui::InstanceView::actionsMenuRequested,
                this,
                &This::showActionsMenu);
    }


    void
    MemoryViewer::setLogTag(const std::string& _tag) // Leading _ silences a warning
    {
        Logging::setTag(_tag);
    }


    void
    MemoryViewer::onInit(ManagedIceObject& component)
    {
        if (mnsName.size() > 0)
        {
            component.usingProxy(mnsName);
        }
        if (debugObserverName.size() > 0)
        {
            component.usingProxy(debugObserverName);
        }

        is_initialized = true;
        emit initialized();
    }


    void
    MemoryViewer::onConnect(ManagedIceObject& component)
    {
        if (not mnsName.empty())
        {
            armem::mns::MemoryNameSystemInterfacePrx mnsProxy;
            component.getProxy(mnsProxy, mnsName);
            mns = client::MemoryNameSystem(mnsProxy);

            const bool update = true;
            memoryReaders = mns.getAllReaders(update);
            memoryWriters = mns.getAllWriters(update);
        }
        // DebugObserver is optional (check for null on every call)
        if (not debugObserverName.empty())
        {
            component.getProxy(debugObserver, debugObserverName, false, "", false);
        }

        updateWidget->startTimerIfEnabled();

        is_connected = true;
        emit connected();
    }


    void
    MemoryViewer::onDisconnect(ManagedIceObject&)
    {
        periodicUpdateTimer->stop();
        updateWidget->stopTimer();

        is_connected = false;

        emit disconnected();
    }

    void MemoryViewer::startPeriodicUpdateTimer()
    {
        periodicUpdateTimer->start();
    }

    const armem::wm::Memory*
    MemoryViewer::getSingleMemoryData(const std::string& memoryName)
    {
        auto it = memoryData.find(memoryName);
        if (it == memoryData.end())
        {
            std::stringstream ss;
            ss << "Memory name '" << memoryName
               << "' is unknown. Known are: " << simox::alg::to_string(simox::alg::get_keys(memoryData), ", ");
            statusLabel->setText(QString::fromStdString(ss.str()));
            return nullptr;
        }
        /*else if (not it->second.has_value())
        {
            return nullptr;
        }*/
        else
        {
            //return &it->second.value();
            return &it->second;
        }
    }


    void
    MemoryViewer::queryAndStoreInLTM()
    {
        TIMING_START(MemoryStore);

        auto enabledMemories = memoryGroup->queryWidget()->getEnabledMemories();
        for (auto& [name, reader] : memoryReaders)
        {
            // skip if memory should not be queried
            if (std::find(enabledMemories.begin(), enabledMemories.end(), name) == enabledMemories.end())
            {
                continue;
            }

            // Query memory
            auto q_res = reader.query(memoryGroup->queryInput());
            if (q_res.success)
            {
                server::dto::DirectlyStoreInput input;
                input.memory = q_res.toIce().memory;
                reader.directlyStore(input);
            }
            else
            {
                std::string s = "Query of memory " + name + " was unsuccessful.";
                statusLabel->setText(QString::fromStdString(s));
            }
        }

        TIMING_END_STREAM(MemoryStore, ARMARX_VERBOSE);
    }

    void
    MemoryViewer::startLTMRecording()
    {

        TIMING_START(MemoryStartRecording);

        auto enabledMemories = memoryGroup->queryWidget()->getEnabledMemories();
        for (auto& [name, reader] : memoryReaders)
        {
            // skip if memory should not be queried
            if (std::find(enabledMemories.begin(), enabledMemories.end(), name) == enabledMemories.end())
            {
                continue;
            }
            reader.startRecording();
        }

        TIMING_END_STREAM(MemoryStartRecording, ARMARX_VERBOSE);
    }

    void
    MemoryViewer::stopLTMRecording()
    {

        TIMING_START(MemoryStopRecording);

        auto enabledMemories = memoryGroup->queryWidget()->getEnabledMemories();
        for (auto& [name, reader] : memoryReaders)
        {
            // skip if memory should not be queried
            if (std::find(enabledMemories.begin(), enabledMemories.end(), name) == enabledMemories.end())
            {
                continue;
            }
            reader.stopRecording();
        }

        TIMING_END_STREAM(MemoryStopRecording, ARMARX_VERBOSE);
    }

    void MemoryViewer::commit()
    {

        TIMING_START(Commit);
        auto now = armem::Time::Now();

        const std::string memoryIDStr = memoryGroup->commitWidget()->getMemoryID();
        const std::string aronJSONStr = memoryGroup->commitWidget()->getAronJSON();

        ARMARX_INFO << "Committing to " << memoryIDStr << " the data: " << aronJSONStr;

        MemoryID memId(memoryIDStr);

        if (!memId.hasEntityName())
        {
            ARMARX_WARNING << "The entered MemoryID '" << memoryIDStr << "' does not contain an entity.";
        }
        else
        {

            nlohmann::json json = nlohmann::json::parse(aronJSONStr);

            // ToDo: multiple objects
            auto aron = aron::converter::AronNlohmannJSONConverter::ConvertFromNlohmannJSONObject(json);

            if (const auto& it = memoryWriters.find(memId.memoryName); it == memoryWriters.end())
            {
                ARMARX_WARNING << "No memory with name '" << memId.memoryName << "' available for commit.";
            }
            else
            {
                armem::Commit comm;
                auto& entityUpdate = comm.add();
                entityUpdate.entityID = memId;
                entityUpdate.confidence = 1.0;
                entityUpdate.instancesData = {aron};
                entityUpdate.timeCreated = now;
                it->second.commit(comm);
            }
        }

        TIMING_END_STREAM(Commit, ARMARX_VERBOSE);
    }


    void
    MemoryViewer::storeOnDisk(QString directory)
    {
        TIMING_START(MemoryExport)

        std::string status;
        std::vector<wm::Memory> memoryDataVec;
        std::transform(memoryData.begin(), memoryData.end(), std::back_inserter(memoryDataVec),
                       [](auto& el) { return el.second; });

        diskControl->storeOnDisk(directory, memoryDataVec, &status);

        statusLabel->setText(QString::fromStdString(status));
        TIMING_END_STREAM(MemoryExport, ARMARX_VERBOSE)
    }


    void
    MemoryViewer::loadFromDisk(QString directory)
    {

        std::string status;
        std::map<std::filesystem::path, wm::Memory> data =
            diskControl->loadFromDisk(directory, memoryGroup->queryInput(), &status);

        for (auto& [path, memory] : data)
        {
            std::string name = memory.id().memoryName;
            auto commit = armem::toCommit(memory);

            if (memoryWriters.count(name) > 0)
            {
                memoryWriters.at(name).commit(commit);
            }
            else
            {
                ARMARX_INFO << "No memory with name '" << name << "' available for commit. Create new virtual memory.";

                // Please note: Here we assume that a memory server with the same name does not exist.
                // I think this assumption is ok, since nobody should use filepaths as memory name.
                // Nonetheless, we did not restrict the user to do so...
                std::string virtualMemoryName = name + " (at " + path.string() + ")";
                wm::Memory virtualMemory(virtualMemoryName);
                virtualMemory.update(commit, true, false);
                memoryData[virtualMemoryName] = virtualMemory;
            }
        }

        statusLabel->setText(QString::fromStdString(status));

        emit memoryDataChanged();
    }


    void
    MemoryViewer::startQueries()
    {
        startDueQueries();
    }


    void
    MemoryViewer::processQueryResults()
    {
        const std::map<std::string, client::QueryResult> results = collectQueryResults();

        int errorCount = 0;
        applyQueryResults(results, &errorCount);

        emit memoryDataChanged();
        updateStatusLabel(errorCount);
    }


    void
    MemoryViewer::updateStatusLabel(int errorCount)
    {
        // Code to output status label information
        if (statusLabel and errorCount > 0)
        {
            auto now = std::chrono::system_clock::now();
            auto in_time_t = std::chrono::system_clock::to_time_t(now);

            std::stringstream ss;
            ss << "Last update: " << std::put_time(std::localtime(&in_time_t), "%Y-%m-%d %X");
            ss << "\nThe query produced " << errorCount << " errors! Please check log.";

            statusLabel->setText(QString::fromStdString(ss.str()));
        }
    }


    void
    MemoryViewer::startDueQueries()
    {

        armem::client::QueryInput input = memoryGroup->queryInput();
        int recursionDepth = memoryGroup->queryWidget()->queryLinkRecursionDepth();

        // Can't use a structured binding here because you can't capture those in a lambda
        // according to the C++ standard.
        auto enabledMemories = memoryGroup->queryWidget()->getEnabledMemories();
        for (const auto& pair : memoryReaders)
        {
            // skip if memory should not be queried
            if (std::find(enabledMemories.begin(), enabledMemories.end(), pair.first) == enabledMemories.end())
            {
                continue;
            }

            const auto& name = pair.first;
            const auto& reader = pair.second;

            // skip if query already running
            if (runningQueries.count(name) != 0)
            {
                continue;
            }

            // You could pass the query function itself to async here,
            // but that caused severe template headaches when I tried it.
            runningQueries[name] = std::async(std::launch::async,
                   [reader, input, recursionDepth, this]()
                   {
                       // Can't resolve MemoryLinks without data
                       return recursionDepth == 0 || input.dataMode == armem::query::DataMode::NoData
                                  ? reader.query(input.toIce())
                                  : reader.query(input.toIce(), mns, recursionDepth);
                   });
        }
    }


    std::map<std::string, client::QueryResult>
    MemoryViewer::collectQueryResults()
    {

        TIMING_START(tCollectQueryResults)

        std::map<std::string, client::QueryResult> results;
        for (auto it = runningQueries.begin(); it != runningQueries.end();)
        {
            const std::string& name = it->first;
            std::future<armem::query::data::Result>* queryPromise = &it->second;

            if (queryPromise->wait_for(std::chrono::seconds(0)) == std::future_status::ready)
            {
                if (auto jt = memoryReaders.find(name); jt != memoryReaders.end())
                {
                    try
                    {
                        results[name] = client::QueryResult::fromIce(queryPromise->get());
                    }
                    catch (const Ice::ConnectionRefusedException&)
                    {
                        // Server is gone (MNS did not know about it yet) => Skip result.
                    }
                }
                // else: Server is gone (MNS knew about it) => Skip result.

                // Promise is completed => Clean up in any case.
                it = runningQueries.erase(it);
            }
            else
            {
                ++it; // Uncompleted => Keep.
            }
        }

        TIMING_END_STREAM(tCollectQueryResults, ARMARX_VERBOSE)
        if (debugObserver)
        {
            debugObserver->begin_setDebugChannel(
                Logging::tag.tagName,
                {
                    {"t Collect Query Results [ms]",
                     new Variant(tCollectQueryResults.toMilliSecondsDouble())},
                    {"# Collected Query Results", new Variant(static_cast<int>(results.size()))},
                });
        }

        return results;
    }


    void
    MemoryViewer::applyQueryResults(const std::map<std::string, client::QueryResult>& results, int* outErrorCount)
    {
        TIMING_START(tProcessQueryResults)
        for (const auto& [name, result] : results)
        {
            if (result.success)
            {
                memoryData[name] = std::move(result.memory);
            }
            else
            {
                ARMARX_WARNING << "Querying memory server '" << name << "' produced an error: \n" << result.errorMessage;
                if (outErrorCount)
                {
                    outErrorCount++;
                }
            }
        }

        // Perhaps remove entries
        auto enabledMemories = memoryGroup->queryWidget()->getEnabledMemories();
        for (auto it = memoryData.begin(); it != memoryData.end();)
        {
            // Drop all entries in memoryData which are not in memoryReaders anymore.
            if (memoryReaders.count(it->first) == 0)
            {
                if (memoryGroup->queryWidget()->dropRemovedMemories())
                {
                    it = memoryData.erase(it);
                }
                else
                {
                    ++it;
                }
                continue;
            }

            // Drop all entries that are not enabled by user (which means that there is no query result)
            if (std::find(enabledMemories.begin(), enabledMemories.end(), it->first) == enabledMemories.end())
            {
                if (memoryGroup->queryWidget()->dropDisabledMemories())
                {
                    it = memoryData.erase(it);
                }
                else
                {
                    ++it;
                }
                continue;
            }

            // Memory found
            ++it;
        }

        TIMING_END_STREAM(tProcessQueryResults, ARMARX_VERBOSE)
        if (debugObserver)
        {
            debugObserver->begin_setDebugChannel(
                Logging::tag.tagName,
                {
                    {"t Process Query Results [ms]",
                     new Variant(tProcessQueryResults.toMilliSecondsDouble())},
                    {"# Processed Query Results", new Variant(static_cast<int>(results.size()))},
                });
        }
    }


    void
    MemoryViewer::updateInstanceTree(const armem::MemoryID& selectedID)
    {
        const armem::wm::Memory* data = getSingleMemoryData(selectedID.memoryName);
        if (data)
        {
            if (not selectedID.hasEntityName())
            {
                return;
            }
            armem::MemoryID id = selectedID;
            const armem::wm::EntitySnapshot* snapshot = nullptr;
            if (not id.hasTimestamp())
            {
                const armem::wm::Entity& entity = data->getEntity(id);
                if (entity.empty())
                {
                    return;
                }
                snapshot = &entity.getLatestSnapshot();
                id.timestamp = snapshot->time();
            }
            if (not id.hasInstanceIndex())
            {
                if (not snapshot)
                {
                    try
                    {
                        snapshot = &data->getSnapshot(id);
                    }
                    catch (const armem::error::ArMemError& e)
                    {
                        if (statusLabel)
                        {
                            statusLabel->setText(e.what());
                        }
                    }
                }
                if (snapshot && snapshot->size() > 0)
                {
                    id.instanceIndex = 0;
                }
            }
            if (id.hasInstanceIndex())
            {
                instanceGroup->view->update(id, *data);
            }
        }
    }


    void
    MemoryViewer::resolveMemoryID(const MemoryID& id)
    {
        // ARMARX_IMPORTANT << "Resolving memory ID: " << id;

        auto handleError = [this](const std::string& msg)
        {
            statusLabel->setText(QString::fromStdString(msg));
            ARMARX_WARNING << msg;
        };

        if (id.memoryName.empty())
        {
            handleError("Memory name is empty.");
        }

        aron::type::ObjectPtr segmentType;
        std::optional<wm::EntityInstance> instance;
        try
        {
            if (const wm::Memory* data = getSingleMemoryData(id.memoryName))
            {
                segmentType = data->getProviderSegment(id).aronType();

                if (id.hasInstanceIndex())
                {
                    instance = data->getInstance(id);
                }
                else if (id.hasTimestamp())
                {
                    instance = data->getSnapshot(id).getInstance(0);
                }
                else
                {
                    instance = data->getEntity(id).getLatestSnapshot().getInstance(0);
                }
            }
        }
        catch (const armem::error::ArMemError&)
        {
            // May be handled by remote lookup
        }

        if (not instance)
        {
            try
            {
                // Resolve remotely (may still fail, returns an optional).
                instance = mns.resolveEntityInstance(id);
            }
            catch (const armem::error::ArMemError& e)
            {
                ARMARX_WARNING << e.what();
                statusLabel->setText(e.what());
            }
        }

        if (instance)
        {
            auto* view = new InstanceView();
            instanceGroup->view->addDataView(view);
            view->update(*instance, segmentType);
            //instanceGroup->view->addInstanceView(*instance, segmentType);
        }
        else
        {
            // ToDo: Propagate error back to highlight selected entry in red
        }
    }


    void
    MemoryViewer::updateListOfActiveMemories()
    {
        if (is_connected and mns) // mns must be connected and mns must be available
        {
            try
            {
                memoryReaders = mns.getAllReaders(true);
                memoryWriters = mns.getAllWriters(true);

                std::vector<std::string> activeMemoryNames;

                // add all active memories to update list
                std::transform(memoryReaders.begin(), memoryReaders.end(), std::back_inserter(activeMemoryNames), [](const auto& p){return p.first;});

                TIMING_START(GuiUpdateAvailableMemories);
                memoryGroup->queryWidget()->update(activeMemoryNames);
                TIMING_END_STREAM(GuiUpdateAvailableMemories, ARMARX_VERBOSE);
            }
            catch (...)
            {
                // MNS was killed/stopped
                // ignore?!
            }
        }
        else
        {
            ARMARX_VERBOSE << deactivateSpam() << "MNS not ready yet. Skip update of available memories in query widget.";
        }
    }


    void
    MemoryViewer::updateMemoryTree()
    {
        std::map<std::string, const armem::wm::Memory*> memoriesToUpdate;

        //auto checkboxStates = memoryGroup->queryWidget()->getAvailableMemoryStates();
        for (auto& [name, data] : memoryData)
        {
            memoriesToUpdate[name] = &data;
        }

        TIMING_START(GuiUpdateMemoryTree)
        memoryGroup->tree()->update(memoriesToUpdate);
        TIMING_END_STREAM(GuiUpdateMemoryTree, ARMARX_VERBOSE)

        if (debugObserver)
        {
            try
            {
                debugObserver->setDebugDatafield(Logging::tag.tagName,
                                                 "GUI Update [ms]",
                                                 new Variant(GuiUpdateMemoryTree.toMilliSecondsDouble()));
            }
            catch (const Ice::Exception&)
            {
                // Ignore ...
            }
        }
    }


    void
    MemoryViewer::showActionsMenu(const MemoryID& memoryID,
                                  QWidget* parent,
                                  const QPoint& pos,
                                  QMenu* menu)
    {
        // Called if we have to stop because of an error.
        auto showMenu = [menu, pos]()
        {
            if (menu)
                menu->exec(pos);
        };

        if (memoryID == MemoryID())
        {
            // Empty MemoryID, don't try to generate actions.
            showMenu();
            return;
        }

        mns::dto::MemoryServerInterfaces prx;
        try
        {
            prx = mns.resolveServer(memoryID);
        }
        catch (const error::CouldNotResolveMemoryServer& e)
        {
            statusLabel->setText(
                    QString::fromStdString(
                        e.makeMsg(memoryID, "Could not resolve memory server.")));
            showMenu();
            return;
        }

        if (!prx.actions)
        {
            std::stringstream ss;
            ss << "Memory server " << memoryID
               << " does not support actions or is offline.";
            statusLabel->setText(QString::fromStdString(ss.str()));
            showMenu();
            return;
        }

        actions::GetActionsOutputSeq result;
        try
        {
            result = prx.actions->getActions({{armarx::toIce<data::MemoryID>(memoryID)}});
        }
        catch (const Ice::LocalException& e)
        {
            std::stringstream ss;
            ss << "Could not get actions for " << memoryID << ".";
            statusLabel->setText(QString::fromStdString(ss.str()));
            showMenu();
            return;
        }

        if (result.size() == 0)
        {
            showMenu();
            return;
        }
        auto builder = ActionsMenuBuilder(
            memoryID,
            parent,
            [this, prx](const MemoryID& memoryID, const actions::ActionPath& path)
            {
                actions::data::ExecuteActionOutputSeq result;
                try
                {
                    result = prx.actions->executeActions(
                            {{armarx::toIce<armem::data::MemoryID>(memoryID), path}});
                }
                catch (const Ice::LocalException& e)
                {
                    std::stringstream ss;
                    ss << "Failed to execute action: " << e.what();
                    statusLabel->setText(QString::fromStdString(ss.str()));
                }

                for (const auto& [success, errorMessage] : result)
                {
                    if (not success)
                    {
                        std::stringstream ss;
                        ss << "Failed to execute action: " << errorMessage;
                        statusLabel->setText(QString::fromStdString(ss.str()));
                        ARMARX_WARNING << ss.str();
                    }
                }
            });

        QMenu* actionsMenu = builder.buildActionsMenu(result[0]);
        if (menu == nullptr)
        {
            actionsMenu->exec(pos);
        }
        else
        {
            menu->addMenu(actionsMenu);
            menu->exec(pos);
        }
    }


    void
    MemoryViewer::makePrediction(const MemoryID& entityID,
                                 const aron::type::ObjectPtr& entityType,
                                 const armarx::DateTime& timestamp,
                                 const std::string& engineID)
    {

        std::stringstream errorStream;
        auto showError = [this, &errorStream]()
        {
            statusLabel->setText(QString::fromStdString(errorStream.str()));
        };

        if (!entityID.hasEntityName() || entityID.hasGap())
        {
            errorStream << "Could not convert " << entityID << " to valid entity ID.";
            showError();
            return;
        }
        if (memoryReaders.find(entityID.memoryName) == memoryReaders.end())
        {
            errorStream << "Not connected to memory '" << entityID.memoryName
                        << "', cannot make prediction.";
            showError();
            return;
        }
        client::Reader reader = memoryReaders.at(entityID.memoryName);
        if (!reader.predictionPrx)
        {
            errorStream << "Predictions are not available for memory '" << entityID.memoryName
                        << "'.";
            showError();
            return;
        }
        PredictionRequest request;
        request.snapshotID = entityID.withTimestamp(timestamp);
        request.predictionSettings.predictionEngineID = engineID;
        PredictionResult result;
        try
        {
            result = reader.predict({request}).at(0);
        }
        catch (const Ice::LocalException& e)
        {
            errorStream << "Could not make prediction request: " << e.what();
            showError();
            return;
        }

        if (!result.success)
        {
            errorStream << "Prediction failed: " << result.errorMessage;
            showError();
            return;
        }

        auto* view = new AronDataView();
        instanceGroup->view->addDataView(view);
        view->update(result.prediction, entityType);
    }


    const static std::string CONFIG_KEY_MEMORY = "MemoryViewer.MemoryNameSystem";
    const static std::string CONFIG_KEY_DEBUG_OBSERVER = "MemoryViewer.DebugObserverName";


    void
    MemoryViewer::loadSettings(QSettings* settings)
    {
        mnsName = settings->value(QString::fromStdString(CONFIG_KEY_MEMORY), "MemoryNameSystem")
                      .toString()
                      .toStdString();
        debugObserverName =
            settings->value(QString::fromStdString(CONFIG_KEY_DEBUG_OBSERVER), "DebugObserver")
                .toString()
                .toStdString();
    }
    void
    MemoryViewer::saveSettings(QSettings* settings)
    {
        settings->setValue(QString::fromStdString(CONFIG_KEY_MEMORY),
                           QString::fromStdString(mnsName));
        settings->setValue(QString::fromStdString(CONFIG_KEY_DEBUG_OBSERVER),
                           QString::fromStdString(debugObserverName));
    }


    void
    MemoryViewer::writeConfigDialog(SimpleConfigDialog* dialog)
    {
        dialog->addProxyFinder<armarx::armem::mns::MemoryNameSystemInterfacePrx>(
            {CONFIG_KEY_MEMORY, "MemoryNameSystem", "MemoryNameSystem"});
        dialog->addProxyFinder<armarx::DebugObserverInterfacePrx>(
            {CONFIG_KEY_DEBUG_OBSERVER, "Debug Observer", "DebugObserver"});
    }
    void
    MemoryViewer::readConfigDialog(SimpleConfigDialog* dialog)
    {
        mnsName = dialog->getProxyName(CONFIG_KEY_MEMORY);
        if (mnsName.empty())
        {
            mnsName = "MemoryNameSystem";
        }
        debugObserverName = dialog->getProxyName(CONFIG_KEY_DEBUG_OBSERVER);
    }

} // namespace armarx::armem::gui