diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5c4dfcbe1a38f714145b31140fff4ce0689b2444..151a197937e32c2536bd8c262c4b4ccd6b7cd46c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -28,6 +28,8 @@ add_subdirectory(external)
 armarx_depends_on(SYSTEM Ceres REQUIRED)
 
 # Optional dependencies
+armarx_depends_on(SYSTEM VTK)
+armarx_depends_on(SYSTEM SemanticObjectRelations QUIET)
 
 
 #include(FetchContent)
diff --git a/source/armarx/navigation/CMakeLists.txt b/source/armarx/navigation/CMakeLists.txt
index e726ba5d970fb7ebb6818e4ea97d2c168eb8daf4..39a4589e700e97fdd3c7ccde505d5856e199deab 100644
--- a/source/armarx/navigation/CMakeLists.txt
+++ b/source/armarx/navigation/CMakeLists.txt
@@ -9,6 +9,8 @@ add_subdirectory(trajectory_control)
 add_subdirectory(safety_control)
 add_subdirectory(client)
 add_subdirectory(factories)
+add_subdirectory(graph)
+add_subdirectory(location)
 add_subdirectory(memory)
 add_subdirectory(server)
 
diff --git a/source/armarx/navigation/components/ExampleClient/ExampleClient.cpp b/source/armarx/navigation/components/ExampleClient/ExampleClient.cpp
index c67cc423c370a71822e14f2c6bfd462a0a932560..2add21844e704bf696f1cc8659a5c6a063c240b7 100644
--- a/source/armarx/navigation/components/ExampleClient/ExampleClient.cpp
+++ b/source/armarx/navigation/components/ExampleClient/ExampleClient.cpp
@@ -20,166 +20,178 @@
  *             GNU General Public License
  */
 
+#include <chrono>
+#include <thread>
+
+#include <ArmarXCore/libraries/DecoupledSingleComponent/Decoupled.h>
+
 #include <armarx/navigation/components/ExampleClient/ExampleClient.h>
 #include <armarx/navigation/global_planning/AStar.h>
 #include <armarx/navigation/trajectory_control/TrajectoryFollowingController.h>
 
-// STD/STL
-#include <chrono>
-#include <thread>
 
-exns::ExampleClient::ExampleClient()
+namespace armarx::navigation::examples
 {
-    // pass
-}
 
-exns::ExampleClient::~ExampleClient()
-{
-    // pass
-}
+    ARMARX_DECOUPLED_REGISTER_COMPONENT(ExampleClient);
 
-void
-exns::ExampleClient::onInitComponent()
-{
-    // pass
-}
 
-void
-exns::ExampleClient::onConnectComponent()
-{
-    task = new armarx::RunningTask<ExampleClient>(this, &ExampleClient::exampleNavigation);
-    task->start();
-}
+    ExampleClient::ExampleClient()
+    {
+        // pass
+    }
 
-void
-exns::ExampleClient::onDisconnectComponent()
-{
-    const bool join = true;
-    task->stop(join);
-}
+    ExampleClient::~ExampleClient()
+    {
+        // pass
+    }
 
-void
-exns::ExampleClient::onExitComponent()
-{
-    // pass
-}
+    void
+    ExampleClient::onInitComponent()
+    {
+        // pass
+    }
 
-std::string
-exns::ExampleClient::getDefaultName() const
-{
-    return "ExampleClient";
-}
+    void
+    ExampleClient::onConnectComponent()
+    {
+        task = new armarx::RunningTask<ExampleClient>(this, &ExampleClient::exampleNavigation);
+        task->start();
+    }
 
-void
-exns::ExampleClient::exampleNavigation()
-{
-    using namespace std::chrono_literals;
+    void
+    ExampleClient::onDisconnectComponent()
+    {
+        const bool join = true;
+        task->stop(join);
+    }
 
-    // Import relevant namespaces.
-    using namespace armarx::navigation;
+    void
+    ExampleClient::onExitComponent()
+    {
+        // pass
+    }
 
-    ARMARX_INFO << "Configuring navigator";
+    std::string
+    ExampleClient::getDefaultName() const
+    {
+        return "ExampleClient";
+    }
 
-    // Create an example configuration valid for the following move* calls.
-    configureNavigator(
-        client::NavigationStackConfig()
-            .general({} /*{.maxVel = VelocityLimits{.linear = 400 , .angular = 0.1}}*/)
-            .globalPlanner(glob_plan::AStarParams())
-            .trajectoryController(traj_ctrl::TrajectoryFollowingControllerParams()));
+    void
+    ExampleClient::exampleNavigation()
+    {
+        using namespace std::chrono_literals;
 
-    // Example of registering a lambda as callback.
-    navigator.onGoalReached([&]() { ARMARX_IMPORTANT << "Goal reached! (lambda-style)"; });
+        // Import relevant namespaces.
+        using namespace armarx::navigation;
 
-    // Example of registering a method as callback.
-    navigator.onGoalReached([this] { goalReached(); });
+        ARMARX_INFO << "Configuring navigator";
 
-    std::this_thread::sleep_for(1s);
+        // Create an example configuration valid for the following move* calls.
+        configureNavigator(
+            client::NavigationStackConfig()
+                .general({} /*{.maxVel = VelocityLimits{.linear = 400 , .angular = 0.1}}*/)
+                .globalPlanner(glob_plan::AStarParams())
+                .trajectoryController(traj_ctrl::TrajectoryFollowingControllerParams()));
 
-    ARMARX_INFO << "Moving to goal pose";
-    // Start moving to goal position using above config.
-    core::Pose goal = core::Pose::Identity();
-    goal.translation() << 2000, 1000, 0;
+        // Example of registering a lambda as callback.
+        navigator.onGoalReached([&]() { ARMARX_IMPORTANT << "Goal reached! (lambda-style)"; });
 
-    navigator.moveTo(goal, core::NavigationFrame::Absolute);
+        // Example of registering a method as callback.
+        navigator.onGoalReached([this] { goalReached(); });
 
-    std::this_thread::sleep_for(15s);
+        std::this_thread::sleep_for(1s);
 
-    // Wait until goal is reached
-    armarx::navigation::client::StopEvent se = navigator.waitForStop();
-    if (se)
-    {
-        ARMARX_INFO << "Goal 1 reached.";
-    }
-    else
-    {
-        if (se.isSafetyStopTriggeredEvent())
+        ARMARX_INFO << "Moving to goal pose";
+        // Start moving to goal position using above config.
+        core::Pose goal = core::Pose::Identity();
+        goal.translation() << 2000, 1000, 0;
+
+        navigator.moveTo(goal, core::NavigationFrame::Absolute);
+
+        std::this_thread::sleep_for(15s);
+
+        // Wait until goal is reached
+        armarx::navigation::client::StopEvent se = navigator.waitForStop();
+        if (se)
         {
-            ARMARX_ERROR << "Safety stop was triggered!";
+            ARMARX_INFO << "Goal 1 reached.";
         }
-        else if (se.isUserAbortTriggeredEvent())
+        else
         {
-            ARMARX_ERROR << "Aborted by user!";
+            if (se.isSafetyStopTriggeredEvent())
+            {
+                ARMARX_ERROR << "Safety stop was triggered!";
+            }
+            else if (se.isUserAbortTriggeredEvent())
+            {
+                ARMARX_ERROR << "Aborted by user!";
+            }
+            else if (se.isInternalErrorEvent())
+            {
+                ARMARX_ERROR << "Unknown internal error occured! "
+                             << se.toInternalErrorEvent().message;
+            }
         }
-        else if (se.isInternalErrorEvent())
+
+        goal.translation() << -1500, 1000, 0;
+        navigator.moveTo(goal, core::NavigationFrame::Absolute);
+
+        std::this_thread::sleep_for(15s);
+
+        // Wait until goal is reached
+        se = navigator.waitForStop();
+        if (se)
         {
-            ARMARX_ERROR << "Unknown internal error occured! " << se.toInternalErrorEvent().message;
+            ARMARX_INFO << "Goal 2 reached.";
+        }
+        else
+        {
+            ARMARX_ERROR << "Could not reach goal 2!";
         }
-    }
-
-    goal.translation() << -1500, 1000, 0;
-    navigator.moveTo(goal, core::NavigationFrame::Absolute);
 
-    std::this_thread::sleep_for(15s);
+        goal.translation() << 4500, 4500, 0;
+        navigator.moveTo(goal, core::NavigationFrame::Absolute);
 
-    // Wait until goal is reached
-    se = navigator.waitForStop();
-    if (se)
-    {
-        ARMARX_INFO << "Goal 2 reached.";
-    }
-    else
-    {
-        ARMARX_ERROR << "Could not reach goal 2!";
-    }
+        // Wait until goal is reached
+        se = navigator.waitForStop();
+        if (se)
+        {
+            ARMARX_INFO << "Goal 3 reached.";
+        }
+        else
+        {
+            ARMARX_ERROR << "Could not reach goal 3!";
+        }
 
-    goal.translation() << 4500, 4500, 0;
-    navigator.moveTo(goal, core::NavigationFrame::Absolute);
+        std::this_thread::sleep_for(15s);
 
-    // Wait until goal is reached
-    se = navigator.waitForStop();
-    if (se)
-    {
-        ARMARX_INFO << "Goal 3 reached.";
-    }
-    else
-    {
-        ARMARX_ERROR << "Could not reach goal 3!";
-    }
+        // TODO example with waypoints
 
-    std::this_thread::sleep_for(15s);
+        ARMARX_INFO << "Moving into certain direction.";
+        // Start moving towards a direction
+        navigator.moveTowards(core::Direction(100, 100, 0), core::NavigationFrame::Relative);
 
-    // TODO example with waypoints
+        std::this_thread::sleep_for(3s);
 
-    ARMARX_INFO << "Moving into certain direction.";
-    // Start moving towards a direction
-    navigator.moveTowards(core::Direction(100, 100, 0), core::NavigationFrame::Relative);
+        ARMARX_INFO << "Pausing movement.";
+        navigator.pause();
+    }
 
-    std::this_thread::sleep_for(3s);
+    void
+    ExampleClient::goalReached()
+    {
+        ARMARX_IMPORTANT << "Goal reached! (method-style)";
+    }
 
-    ARMARX_INFO << "Pausing movement.";
-    navigator.pause();
-}
+    armarx::PropertyDefinitionsPtr
+    ExampleClient::createPropertyDefinitions()
+    {
+        armarx::PropertyDefinitionsPtr def =
+            new armarx::ComponentPropertyDefinitions(getConfigIdentifier());
+        return def;
+    }
 
-void
-exns::ExampleClient::goalReached()
-{
-    ARMARX_IMPORTANT << "Goal reached! (method-style)";
-}
 
-armarx::PropertyDefinitionsPtr
-exns::ExampleClient::createPropertyDefinitions()
-{
-    armarx::PropertyDefinitionsPtr def =
-        new armarx::ComponentPropertyDefinitions(getConfigIdentifier());
-    return def;
-}
+} // namespace armarx::navigation::examples
diff --git a/source/armarx/navigation/components/ExampleClient/ExampleClient.h b/source/armarx/navigation/components/ExampleClient/ExampleClient.h
index d4b460469bbc99d6162e44df8a4170fedc7a97e6..a8879fb233cd1e017087f4da821dfff65bf08c5b 100644
--- a/source/armarx/navigation/components/ExampleClient/ExampleClient.h
+++ b/source/armarx/navigation/components/ExampleClient/ExampleClient.h
@@ -22,13 +22,12 @@
 
 #pragma once
 
-// ArmarX
 #include <ArmarXCore/util/tasks.h>
 
-// Navigation
 #include <armarx/navigation/client.h>
 
-namespace exns
+
+namespace armarx::navigation::examples
 {
 
     /**
@@ -77,4 +76,5 @@ namespace exns
 
         armarx::RunningTask<ExampleClient>::pointer_type task;
     };
-} // namespace exns
+
+} // namespace armarx::navigation::examples
diff --git a/source/armarx/navigation/components/GraphImportExport/CMakeLists.txt b/source/armarx/navigation/components/GraphImportExport/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..e5d7c37d274b625cc17b2c0533bfd61e0187046d
--- /dev/null
+++ b/source/armarx/navigation/components/GraphImportExport/CMakeLists.txt
@@ -0,0 +1,93 @@
+set(LIB_NAME       GraphImportExport)
+
+armarx_component_set_name("${LIB_NAME}")
+armarx_set_target("Library: ${LIB_NAME}")
+
+# If your component needs a special ice interface, define it here:
+# armarx_add_component_interface_lib(
+#     SLICE_FILES
+#         GraphImportExport.ice
+#     ICE_LIBS
+#         # RobotAPI
+#)
+
+
+# Add the component
+armarx_add_component(
+    COMPONENT_LIBS
+        # ArmarXCore
+        ArmarXCore
+        ArmarXCoreComponentPlugins  # For DebugObserver plugin.
+        # ArmarXGui
+        ArmarXGuiComponentPlugins  # For RemoteGui plugin.
+        # RobotAPI
+        ## RobotAPICore
+        ## RobotAPIInterfaces
+        RobotAPIComponentPlugins  # For ArViz and other plugins.
+        armem
+
+        # MemoryX
+        MemoryXCore
+        MemoryXMemoryTypes
+
+        # This project.
+        Navigation::Location
+        Navigation::Graph
+
+        ## ${PROJECT_NAME}Interfaces  # For ice interfaces from this package.
+        # This component
+        ## GraphImportExportInterfaces  # If you defined a component ice interface above.
+
+    SOURCES
+        GraphImportExport.cpp
+
+    HEADERS
+        GraphImportExport.h
+)
+
+
+
+find_package(MemoryX QUIET)
+armarx_build_if(MemoryX_FOUND "MemoryX not available")
+if(MemoryX_FOUND)
+    target_include_directories(${LIB_NAME} PUBLIC ${MemoryX_INCLUDE_DIRS})
+endif()
+
+
+
+#find_package(SemanticObjectRelations QUIET)
+#armarx_build_if(SemanticObjectRelations_FOUND "SemanticObjectRelations not available")
+#if(SemanticObjectRelations_FOUND)
+#    target_link_libraries(${LIB_NAME} PRIVATE SemanticObjectRelations)
+#    target_include_directories(${LIB_NAME} PRIVATE SemanticObjectRelations)
+#endif()
+
+
+# Add dependencies
+#find_package(MyLib QUIET)
+#armarx_build_if(MyLib_FOUND "MyLib not available")
+
+# All target_include_directories must be guarded by if(Xyz_FOUND)
+# For multiple libraries write: if(X_FOUND AND Y_FOUND) ...
+#if(MyLib_FOUND)
+#    target_include_directories(GraphImportExport PUBLIC ${MyLib_INCLUDE_DIRS})
+#endif()
+
+
+# Add ARON files
+#armarx_enable_aron_file_generation_for_target(
+#    TARGET_NAME
+#        ${ARMARX_COMPONENT_NAME}
+#    ARON_FILES
+#        aron/ExampleData.xml
+#)
+
+
+# Add unit tests
+# add_subdirectory(test)
+
+# Generate the application
+armarx_generate_and_add_component_executable(
+    # If your component is not defined in ::armarx, specify its namespace here:
+    COMPONENT_NAMESPACE "armarx::nav"
+)
diff --git a/source/armarx/navigation/components/GraphImportExport/GraphImportExport.cpp b/source/armarx/navigation/components/GraphImportExport/GraphImportExport.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..92694a45fe17ac24788b9b4a9b55f8c404d05049
--- /dev/null
+++ b/source/armarx/navigation/components/GraphImportExport/GraphImportExport.cpp
@@ -0,0 +1,431 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package    MemoryX::ArmarXObjects::GraphImportExport
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#include "GraphImportExport.h"
+
+#include <iomanip>
+
+#include <VirtualRobot/VirtualRobot.h>
+
+#include <ArmarXCore/core/exceptions/local/ExpressionException.h>
+#include <ArmarXCore/libraries/DecoupledSingleComponent/Decoupled.h>
+
+#include <RobotAPI/libraries/armem/core/Commit.h>
+#include <RobotAPI/libraries/armem/core/aron_conversions.h>
+#include <RobotAPI/libraries/core/FramedPose.h>
+
+#include <MemoryX/core/MemoryXCoreObjectFactories.h>
+#include <MemoryX/libraries/memorytypes/MemoryXTypesObjectFactories.h>
+#include <armarx/navigation/graph/Graph.h>
+#include <armarx/navigation/graph/Visu.h>
+#include <armarx/navigation/graph/aron/Graph.aron.generated.h>
+#include <armarx/navigation/graph/constants.h>
+#include <armarx/navigation/location/aron/Location.aron.generated.h>
+#include <armarx/navigation/location/constants.h>
+
+
+namespace armarx::nav
+{
+
+    GraphImportExport::GraphImportExport()
+    {
+        properties.locationCoreSegmentID = loc::coreSegmentID;
+        properties.graphCoreSegmentID = graph::coreSegmentID;
+    }
+
+
+    armarx::PropertyDefinitionsPtr
+    GraphImportExport::createPropertyDefinitions()
+    {
+        armarx::PropertyDefinitionsPtr def =
+            new ComponentPropertyDefinitions(getConfigIdentifier());
+
+        def->component(proxies.priorKnowledge);
+        def->component(proxies.graphNodePoseResolver, "GraphNodePoseResolver");
+
+        return def;
+    }
+
+
+    void
+    GraphImportExport::onInitComponent()
+    {
+        // Topics and properties defined above are automagically registered.
+
+        // Keep debug observer data until calling `sendDebugObserverBatch()`.
+        setDebugObserverBatchModeEnabled(true);
+    }
+
+
+    void
+    GraphImportExport::onConnectComponent()
+    {
+        // Get proxies.
+        if (proxies.priorKnowledge->hasGraphSegment())
+        {
+            proxies.graphSegment = proxies.priorKnowledge->getGraphSegment();
+        }
+        proxies.locationWriter = memoryNameSystem.useWriter(properties.locationCoreSegmentID);
+        proxies.graphWriter = memoryNameSystem.useWriter(properties.graphCoreSegmentID);
+
+
+        // Setup and start the remote GUI.
+        refreshScenes();
+        RemoteGui_startRunningTask();
+    }
+
+
+    void
+    GraphImportExport::onDisconnectComponent()
+    {
+    }
+
+
+    void
+    GraphImportExport::onExitComponent()
+    {
+    }
+
+
+    std::string
+    GraphImportExport::getDefaultName() const
+    {
+        return "GraphImportExport";
+    }
+
+
+    void
+    GraphImportExport::createRemoteGuiTab(const std::vector<std::string>& sceneNames)
+    {
+        using namespace armarx::RemoteGui::Client;
+
+        tab.sceneComboBox.setOptions(sceneNames);
+        tab.sceneRefreshButton.setLabel("Refresh");
+
+        tab.providerSegmentLine.setValue(getName());
+
+        tab.dryRun.setValue(false);
+        tab.visuEnabled.setValue(true);
+
+        tab.locationsMemoryxToArmemButton.setLabel("Locations MemoryX -> ArMem");
+        tab.locationsArmemToMemoryxButton.setLabel("Locations ArMem -> MemoryX (WIP)");
+        tab.locationsClearArMemButton.setLabel("Clear ArMem Locations");
+
+        tab.graphMemoryxToArmemButton.setLabel("Graph MemoryX -> ArMem");
+        tab.graphArmemToMemoryxButton.setLabel("Graph ArMem -> MemoryX (WIP)");
+        tab.graphClearArMemButton.setLabel("Clear ArMem Graphs");
+
+
+        GridLayout grid;
+        int row = 0;
+        {
+            grid.add(Label("Scene:"), {row, 0})
+                .add(tab.sceneComboBox, {row, 1})
+                .add(tab.sceneRefreshButton, {row, 2});
+            ++row;
+
+            grid.add(Label("Provider Segment:"), {row, 0}).add(tab.providerSegmentLine, {row, 1});
+            ++row;
+
+            grid.add(Label("Dry Run:"), {row, 0}).add(tab.dryRun, {row, 1});
+            ++row;
+
+            grid.add(Label("Enable visu:"), {row, 0}).add(tab.visuEnabled, {row, 1});
+            ++row;
+
+            grid.add(tab.locationsMemoryxToArmemButton, {row, 0})
+                .add(tab.locationsArmemToMemoryxButton, {row, 1})
+                .add(tab.locationsClearArMemButton, {row, 2});
+            ++row;
+
+            grid.add(tab.graphMemoryxToArmemButton, {row, 0})
+                .add(tab.graphArmemToMemoryxButton, {row, 1})
+                .add(tab.graphClearArMemButton, {row, 2});
+            ++row;
+        }
+
+        VBoxLayout root = {grid, VSpacer()};
+        RemoteGui_createTab(getName(), root, &tab);
+    }
+
+
+    void
+    GraphImportExport::RemoteGui_update()
+    {
+        if (tab.sceneRefreshButton.wasClicked())
+        {
+            refreshScenes();
+        }
+
+        if (tab.locationsMemoryxToArmemButton.wasClicked())
+        {
+            locationsMemoryxToArmem(tab.sceneComboBox.getValue());
+        }
+        if (tab.locationsArmemToMemoryxButton.wasClicked())
+        {
+            locationsArmemToMemoryx(tab.sceneComboBox.getValue());
+        }
+        if (tab.graphMemoryxToArmemButton.wasClicked())
+        {
+            graphMemoryxToArmem(tab.sceneComboBox.getValue());
+        }
+        if (tab.graphArmemToMemoryxButton.wasClicked())
+        {
+            graphArmemToMemoryx(tab.sceneComboBox.getValue());
+        }
+
+        if (tab.locationsClearArMemButton.wasClicked())
+        {
+            clearArMemProviderSegment(proxies.locationWriter, getLocationProviderSegmentID());
+        }
+        if (tab.graphClearArMemButton.wasClicked())
+        {
+            clearArMemProviderSegment(proxies.graphWriter, getGraphProviderSegmentID());
+        }
+    }
+
+
+    void
+    GraphImportExport::refreshScenes()
+    {
+        const ::Ice::StringSeq sceneNames = proxies.graphSegment->getScenes();
+        createRemoteGuiTab(sceneNames);
+    }
+
+
+    void
+    GraphImportExport::locationsMemoryxToArmem(const std::string& sceneName)
+    {
+        const armem::Time time = armem::Time::now();
+        armem::Commit commit;
+
+        memoryx::GraphNodeBaseList graphNodes = proxies.graphSegment->getNodesByScene(sceneName);
+        for (memoryx::GraphNodeBasePtr& node : graphNodes)
+        {
+            ARMARX_CHECK_NOT_NULL(node);
+            if (not node->isMetaEntity())
+            {
+                // ID is just some random MongoDB hash
+                const std::string nodeId = node->getId();
+                // This is the readable name entered in the GUI.
+                const std::string name = node->getName();
+
+                armarx::FramedPosePtr pose = armarx::FramedPosePtr::dynamicCast(node->getPose());
+                ARMARX_CHECK_NOT_NULL(pose);
+
+                FramedPosePtr globalNodePose = FramedPosePtr::dynamicCast(
+                    proxies.graphNodePoseResolver->resolveToGlobalPose(node));
+                ARMARX_CHECK_NOT_NULL(globalNodePose);
+
+                // `pose` and `globalNodePose` seem to be identical. Is the last step necessary?
+                // Maybe `pose` could be non-global.
+
+                ARMARX_VERBOSE << std::setprecision(2) << std::fixed << "Processing node "
+                               << (commit.updates.size() + 1) << "\n- ID: \t" << nodeId
+                               << "\n- Name: \t" << name << "\n- Pose: \n"
+                               << pose->toEigen() << "\n- Resolved global pose: \n"
+                               << globalNodePose->toEigen();
+
+                navigation::location::arondto::Location data;
+                data.globalRobotPose = globalNodePose->toEigen();
+
+                armem::EntityUpdate& update = commit.add();
+                update.entityID = getLocationProviderSegmentID().withEntityName(name);
+                update.timeCreated = time;
+                update.instancesData = {data.toAron()};
+            }
+        }
+
+        if (not tab.dryRun.getValue())
+        {
+            armem::CommitResult result = proxies.locationWriter.commit(commit);
+            if (result.allSuccess())
+            {
+                ARMARX_IMPORTANT << "Successfully exported " << result.results.size()
+                                 << " locations from MemoryX to ArMem.";
+            }
+            else
+            {
+                ARMARX_WARNING << result.allErrorMessages();
+            }
+        }
+        else
+        {
+            ARMARX_VERBOSE << "Dry Run - skipping commit.";
+        }
+    }
+
+
+    void
+    GraphImportExport::locationsArmemToMemoryx(const std::string& sceneName)
+    {
+        ARMARX_IMPORTANT << "locationsArmemToMemoryx() is WIP!";
+        (void)sceneName;
+    }
+
+
+    void
+    GraphImportExport::graphMemoryxToArmem(const std::string& sceneName)
+    {
+        memoryx::GraphNodeBaseList graphNodes = proxies.graphSegment->getNodesByScene(sceneName);
+        navigation::graph::Graph graph = toArmemGraph(graphNodes);
+
+        if (tab.visuEnabled.getValue())
+        {
+            navigation::graph::GraphVisu visu;
+            viz::Layer layer = arviz.layer("Graph '" + sceneName + "'");
+            visu.draw(layer, graph);
+
+            ARMARX_VERBOSE << "Visualize graph '" << sceneName << "' ...";
+            arviz.commit(layer);
+        }
+
+        // Build ARON Graph
+        navigation::graph::arondto::Graph aron;
+        toAron(aron, graph);
+
+        // Build commit
+        const armem::Time time = armem::Time::now();
+        armem::EntityUpdate update;
+        update.entityID = getGraphProviderSegmentID().withEntityName(sceneName);
+        update.timeCreated = time;
+        update.instancesData = {aron.toAron()};
+
+        if (not tab.dryRun.getValue())
+        {
+            armem::EntityUpdateResult result = proxies.graphWriter.commit(update);
+            if (result.success)
+            {
+                ARMARX_IMPORTANT << "Successfully exported graph '" << sceneName
+                                 << "' from MemoryX to ArMem.";
+            }
+            else
+            {
+                ARMARX_WARNING << result.errorMessage;
+            }
+        }
+        else
+        {
+            ARMARX_VERBOSE << "Dry Run - skipping commit.";
+        }
+    }
+
+
+    void
+    GraphImportExport::graphArmemToMemoryx(const std::string& sceneName)
+    {
+        ARMARX_IMPORTANT << "graphArmemToMemoryx() is WIP!";
+        (void)sceneName;
+    }
+
+
+    void
+    GraphImportExport::clearArMemProviderSegment(armem::client::Writer& writer,
+                                                 const armem::MemoryID& providerSegmentID)
+    {
+        const bool clearWhenExists = true;
+        auto result = writer.addSegment(providerSegmentID, clearWhenExists);
+        if (result.success)
+        {
+            ARMARX_IMPORTANT << "Cleared ArMem provider segment " << providerSegmentID << ".";
+        }
+        else
+        {
+            ARMARX_WARNING << result.errorMessage;
+        }
+    }
+
+
+    armem::MemoryID
+    GraphImportExport::getLocationProviderSegmentID()
+    {
+        return properties.locationCoreSegmentID.withProviderSegmentName(
+            tab.providerSegmentLine.getValue());
+    }
+
+
+    armem::MemoryID
+    GraphImportExport::getGraphProviderSegmentID()
+    {
+        return properties.graphCoreSegmentID.withProviderSegmentName(
+            tab.providerSegmentLine.getValue());
+    }
+
+
+    navigation::graph::Graph
+    GraphImportExport::toArmemGraph(const memoryx::GraphNodeBaseList& graphNodes)
+    {
+        navigation::graph::Graph graph;
+        std::map<std::string, navigation::graph::Graph::Vertex> vertexMap;
+
+        // Add nodes
+        semrel::ShapeID nextVertexID{0};
+        for (memoryx::GraphNodeBasePtr node : graphNodes)
+        {
+            ARMARX_CHECK_NOT_NULL(node);
+            if (not node->isMetaEntity())
+            {
+                // This is the readable name entered in the GUI.
+                const std::string name = node->getName();
+
+                FramedPosePtr globalNodePose = FramedPosePtr::dynamicCast(
+                    proxies.graphNodePoseResolver->resolveToGlobalPose(node));
+                ARMARX_CHECK_NOT_NULL(globalNodePose);
+
+                ARMARX_VERBOSE << "\n- Adding node: \t" << name;
+
+                navigation::graph::Graph::Vertex& vertex =
+                    vertexMap.emplace(name, graph.addVertex(nextVertexID)).first->second;
+                vertex.attrib().aron.vertexID = static_cast<long>(nextVertexID);
+                toAron(vertex.attrib().aron.locationID,
+                       getLocationProviderSegmentID().withEntityName(name));
+                vertex.attrib().setPose(globalNodePose->toEigen());
+
+                nextVertexID++;
+            }
+        }
+
+        // Add edges
+        for (memoryx::GraphNodeBasePtr node : graphNodes)
+        {
+            const auto& sourceVertex = vertexMap.at(node->getName());
+            for (int i = 0; i < node->getOutdegree(); i++)
+            {
+                auto adjacent =
+                    memoryx::GraphNodeBasePtr::dynamicCast(node->getAdjacentNode(i)->getEntity());
+                ARMARX_CHECK_NOT_NULL(adjacent);
+                const auto& targetVertex = vertexMap.at(adjacent->getName());
+
+                ARMARX_VERBOSE << "\n- Adding edge: \t" << node->getName() << " \t-> "
+                               << adjacent->getName();
+                navigation::graph::Graph::Edge edge = graph.addEdge(sourceVertex, targetVertex);
+                edge.attrib().aron.sourceVertexID =
+                    static_cast<long>(sourceVertex.attrib().aron.vertexID);
+                edge.attrib().aron.targetVertexID =
+                    static_cast<long>(targetVertex.attrib().aron.vertexID);
+            }
+        }
+
+        return graph;
+    }
+
+} // namespace armarx::nav
diff --git a/source/armarx/navigation/components/GraphImportExport/GraphImportExport.h b/source/armarx/navigation/components/GraphImportExport/GraphImportExport.h
new file mode 100644
index 0000000000000000000000000000000000000000..1f2a97588fff6c0a1819bc26c6bcae3fd79c18fc
--- /dev/null
+++ b/source/armarx/navigation/components/GraphImportExport/GraphImportExport.h
@@ -0,0 +1,157 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package    MemoryX::ArmarXObjects::GraphImportExport
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <ArmarXCore/core/Component.h>
+#include <ArmarXCore/libraries/ArmarXCoreComponentPlugins/DebugObserverComponentPlugin.h>
+
+#include <ArmarXGui/libraries/ArmarXGuiComponentPlugins/LightweightRemoteGuiComponentPlugin.h>
+
+#include <RobotAPI/libraries/RobotAPIComponentPlugins/ArVizComponentPlugin.h>
+#include <RobotAPI/libraries/armem/client/MemoryNameSystemComponentPlugin.h>
+#include <RobotAPI/libraries/armem/client/Writer.h>
+
+#include <MemoryX/interface/components/GraphNodePoseResolverInterface.h>
+#include <MemoryX/interface/components/PriorKnowledgeInterface.h>
+#include <MemoryX/interface/memorytypes/MemoryEntities.h>
+#include <MemoryX/interface/memorytypes/MemorySegments.h>
+#include <armarx/navigation/graph/forward_declarations.h>
+
+
+namespace armarx::nav
+{
+
+    /**
+     * @defgroup Component-GraphImportExport GraphImportExport
+     * @ingroup MemoryX-Components
+     * A description of the component GraphImportExport.
+     *
+     * @class GraphImportExport
+     * @ingroup Component-GraphImportExport
+     * @brief Brief description of class GraphImportExport.
+     *
+     * Detailed description of class GraphImportExport.
+     */
+    class GraphImportExport :
+        virtual public armarx::Component,
+        virtual public armarx::DebugObserverComponentPluginUser,
+        virtual public armarx::LightweightRemoteGuiComponentPluginUser,
+        virtual public armarx::ArVizComponentPluginUser,
+        virtual public armarx::armem::client::MemoryNameSystemComponentPluginUser
+    {
+    public:
+        GraphImportExport();
+
+
+        /// @see armarx::ManagedIceObject::getDefaultName()
+        std::string getDefaultName() const override;
+
+
+    protected:
+        /// @see PropertyUser::createPropertyDefinitions()
+        armarx::PropertyDefinitionsPtr createPropertyDefinitions() override;
+
+        /// @see armarx::ManagedIceObject::onInitComponent()
+        void onInitComponent() override;
+
+        /// @see armarx::ManagedIceObject::onConnectComponent()
+        void onConnectComponent() override;
+
+        /// @see armarx::ManagedIceObject::onDisconnectComponent()
+        void onDisconnectComponent() override;
+
+        /// @see armarx::ManagedIceObject::onExitComponent()
+        void onExitComponent() override;
+
+
+        /// This function should be called once in onConnect() or when you
+        /// need to re-create the Remote GUI tab.
+        void createRemoteGuiTab(const std::vector<std::string>& sceneNames);
+
+        /// After calling `RemoteGui_startRunningTask`, this function is
+        /// called periodically in a separate thread. If you update variables,
+        /// make sure to synchronize access to them.
+        void RemoteGui_update() override;
+
+
+    private:
+        void refreshScenes();
+
+        void locationsMemoryxToArmem(const std::string& sceneName);
+        void locationsArmemToMemoryx(const std::string& sceneName);
+        void graphMemoryxToArmem(const std::string& sceneName);
+        void graphArmemToMemoryx(const std::string& sceneName);
+
+        void clearArMemProviderSegment(armem::client::Writer& writer,
+                                       const armem::MemoryID& providerSegmentID);
+
+        armem::MemoryID getLocationProviderSegmentID();
+        armem::MemoryID getGraphProviderSegmentID();
+
+        navigation::graph::Graph toArmemGraph(const memoryx::GraphNodeBaseList& graphNodes);
+
+
+    private:
+        struct Proxies
+        {
+            memoryx::PriorKnowledgeInterfacePrx priorKnowledge;
+            memoryx::GraphNodePoseResolverInterfacePrx graphNodePoseResolver;
+            memoryx::GraphMemorySegmentBasePrx graphSegment;
+
+            armem::client::Writer locationWriter;
+            armem::client::Writer graphWriter;
+        };
+        Proxies proxies;
+
+
+        /// Properties shown in the Scenario GUI.
+        struct Properties
+        {
+            armem::MemoryID locationCoreSegmentID;
+            armem::MemoryID graphCoreSegmentID;
+        };
+        Properties properties;
+
+
+        /// Tab shown in the Remote GUI.
+        struct RemoteGuiTab : armarx::RemoteGui::Client::Tab
+        {
+            armarx::RemoteGui::Client::ComboBox sceneComboBox;
+            armarx::RemoteGui::Client::Button sceneRefreshButton;
+
+            armarx::RemoteGui::Client::LineEdit providerSegmentLine;
+
+            armarx::RemoteGui::Client::Button locationsMemoryxToArmemButton;
+            armarx::RemoteGui::Client::Button locationsArmemToMemoryxButton;
+            armarx::RemoteGui::Client::Button locationsClearArMemButton;
+
+            armarx::RemoteGui::Client::Button graphMemoryxToArmemButton;
+            armarx::RemoteGui::Client::Button graphArmemToMemoryxButton;
+            armarx::RemoteGui::Client::Button graphClearArMemButton;
+
+            armarx::RemoteGui::Client::CheckBox dryRun;
+            armarx::RemoteGui::Client::CheckBox visuEnabled;
+        };
+        RemoteGuiTab tab;
+    };
+} // namespace armarx::nav
diff --git a/source/armarx/navigation/components/GraphImportExport/test/CMakeLists.txt b/source/armarx/navigation/components/GraphImportExport/test/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..1178a8b5b3f0e690748bc8c09f5fd9aeda8fd4a7
--- /dev/null
+++ b/source/armarx/navigation/components/GraphImportExport/test/CMakeLists.txt
@@ -0,0 +1,5 @@
+
+# Libs required for the tests
+SET(LIBS ${LIBS} ArmarXCore GraphImportExport)
+ 
+armarx_add_test(GraphImportExportTest GraphImportExportTest.cpp "${LIBS}")
diff --git a/source/armarx/navigation/components/GraphImportExport/test/GraphImportExportTest.cpp b/source/armarx/navigation/components/GraphImportExport/test/GraphImportExportTest.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..fa6c535cc4a3ffbba7b52f0feb6a13b3316b3739
--- /dev/null
+++ b/source/armarx/navigation/components/GraphImportExport/test/GraphImportExportTest.cpp
@@ -0,0 +1,37 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package    MemoryX::ArmarXObjects::GraphImportExport
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#define BOOST_TEST_MODULE MemoryX::ArmarXObjects::GraphImportExport
+
+#define ARMARX_BOOST_TEST
+
+#include <MemoryX/Test.h>
+#include "../GraphImportExport.h"
+
+#include <iostream>
+
+BOOST_AUTO_TEST_CASE(testExample)
+{
+    armarx::GraphImportExport instance;
+
+    BOOST_CHECK_EQUAL(true, true);
+}
diff --git a/source/armarx/navigation/components/NavigationMemory/CMakeLists.txt b/source/armarx/navigation/components/NavigationMemory/CMakeLists.txt
index 64f54400e02889d9b4eb91f738f04be4960070d0..2d2bd9cca05e4e3896f4da35699d559c2f86f798 100644
--- a/source/armarx/navigation/components/NavigationMemory/CMakeLists.txt
+++ b/source/armarx/navigation/components/NavigationMemory/CMakeLists.txt
@@ -1,24 +1,60 @@
+
+
+# If your component needs a special ice interface, define it here:
+# armarx_add_component_interface_lib(
+#     SLICE_FILES
+#         NavigationMemory.ice
+#     ICE_LIBS
+#         # RobotAPI
+#)
+
+
+# Add the component
 armarx_add_component(NavigationMemory
     DEPENDENCIES
         # ArmarXCore
         ArmarXCore
-        # ArmarXCoreInterfaces
         ## ArmarXCoreComponentPlugins  # For DebugObserver plugin.
         # ArmarXGui
-        ## ArmarXGuiComponentPlugins  # For RemoteGui plugin.
+        ArmarXGuiComponentPlugins  # For RemoteGui plugin.
         # RobotAPI
         RobotAPICore
         armem
+        ## RobotAPIInterfaces
         RobotAPIComponentPlugins  # For ArViz and other plugins.
 
         # This project
         ## ${PROJECT_NAME}Interfaces  # For ice interfaces from this package.
         # This component
         ## NavigationMemoryInterfaces  # If you defined a component ice interface above.
-        Simox::SimoxUtility
-        Eigen3::Eigen
+        armarx_navigation::graph
+        armarx_navigation::location
+
     SOURCES
         NavigationMemory.cpp
+        Visu.cpp
+
     HEADERS
         NavigationMemory.h
+        Visu.h
 )
+
+
+# Add dependencies
+#find_package(MyLib QUIET)
+#armarx_build_if(MyLib_FOUND "MyLib not available")
+
+# All target_include_directories must be guarded by if(Xyz_FOUND)
+# For multiple libraries write: if(X_FOUND AND Y_FOUND) ...
+#if(MyLib_FOUND)
+#    target_include_directories(NavigationMemory PUBLIC ${MyLib_INCLUDE_DIRS})
+#endif()
+
+
+# Add ARON files
+#armarx_enable_aron_file_generation_for_target(
+#    TARGET_NAME
+#        ${ARMARX_COMPONENT_NAME}
+#    ARON_FILES
+#        aron/ExampleData.xml
+#)
diff --git a/source/armarx/navigation/components/NavigationMemory/NavigationMemory.cpp b/source/armarx/navigation/components/NavigationMemory/NavigationMemory.cpp
index 38de6ecec5b70aa9ae60753cdc04da9616b1958e..3858e9db0d4acc8fcf1d0d65ebd1fbb375001973 100644
--- a/source/armarx/navigation/components/NavigationMemory/NavigationMemory.cpp
+++ b/source/armarx/navigation/components/NavigationMemory/NavigationMemory.cpp
@@ -22,18 +22,24 @@
 
 #include "NavigationMemory.h"
 
+#include <ArmarXCore/core/time/CycleUtil.h>
+#include <ArmarXCore/libraries/DecoupledSingleComponent/Decoupled.h>
+
+#include "Visu.h"
 #include <armarx/navigation/core/aron/Trajectory.aron.generated.h>
 #include <armarx/navigation/core/aron/Twist.aron.generated.h>
+#include <armarx/navigation/graph/Graph.h>
+#include <armarx/navigation/graph/aron/Graph.aron.generated.h>
+#include <armarx/navigation/graph/constants.h>
+#include <armarx/navigation/location/aron/Location.aron.generated.h>
+#include <armarx/navigation/location/constants.h>
 
-// Include headers you only need in function definitions in the .cpp.
-
-// #include <Eigen/Core>
 
-// #include <SimoxUtility/color/Color.h>
+namespace armarx::navigation
+{
 
+    ARMARX_DECOUPLED_REGISTER_COMPONENT(NavigationMemory);
 
-namespace armarx
-{
 
     armarx::PropertyDefinitionsPtr
     NavigationMemory::createPropertyDefinitions()
@@ -51,14 +57,15 @@ namespace armarx
         // def->component(myComponentProxy)
 
 
-        // Add a required property. (The component won't start without a value being set.)
-        // def->required(properties.boxLayerName, "p.box.LayerName", "Name of the box layer in ArViz.");
-
-        // Add an optionalproperty.
-        // def->optional(
-        //     properties.boxLayerName, "p.box.LayerName", "Name of the box layer in ArViz.");
-        // def->optional(properties.numBoxes, "p.box.Number", "Number of boxes to draw in ArViz.");
-
+        def->optional(properties.locationGraph.visuLocations,
+                      "p.locationGraph.visuLocation",
+                      "Enable visualization of locations.");
+        def->optional(properties.locationGraph.visuGraphEdges,
+                      "p.locationGraph.visuGraphEdges",
+                      "Enable visualization of navigation graph edges.");
+        def->optional(properties.locationGraph.visuFrequency,
+                      "p.locationGraph.visuFrequency",
+                      "Visualization frequeny of locations and graph edges [Hz].");
 
         return def;
     }
@@ -73,8 +80,7 @@ namespace armarx
         // (Requies the armarx::DebugObserverComponentPluginUser.)
         // setDebugObserverBatchModeEnabled(true);
 
-        workingMemory.name() = p.memoryName;
-        // longtermMemory.name() = p.memoryName;
+        setMemoryName(properties.memoryName);
 
         workingMemory.addCoreSegment("Parameterization");
 
@@ -89,6 +95,12 @@ namespace armarx
 
         workingMemory.addCoreSegment("Events"); //, armem::example::ExampleData::toAronType());
         // workingMemory.addCoreSegment("Exceptions"); //, armem::example::ExampleData::toAronType());
+
+
+        workingMemory.addCoreSegment(navigation::location::coreSegmentID.coreSegmentName,
+                                     navigation::location::arondto::Location::toAronType());
+        workingMemory.addCoreSegment(navigation::graph::coreSegmentID.coreSegmentName,
+                                     navigation::graph::arondto::Graph::toAronType());
     }
 
 
@@ -109,19 +121,14 @@ namespace armarx
         }
         */
 
-        /* (Requires the armarx::ArVizComponentPluginUser.)
-        // Draw boxes in ArViz.
-        // (Before starting any threads, we don't need to lock mutexes.)
-        drawBoxes(properties, arviz);
-        */
-
-        /* (Requires the armarx::LightweightRemoteGuiComponentPluginUser.)
         // Setup the remote GUI.
         {
             createRemoteGuiTab();
             RemoteGui_startRunningTask();
         }
-        */
+
+        tasks.visuTask = new SimpleRunningTask<>([this]() { this->visuRun(); }, "Visualization");
+        tasks.visuTask->start();
     }
 
 
@@ -144,80 +151,95 @@ namespace armarx
     }
 
 
-    /* (Requires the armarx::LightweightRemoteGuiComponentPluginUser.)
-    void NavigationMemory::createRemoteGuiTab()
+    void
+    NavigationMemory::createRemoteGuiTab()
     {
         using namespace armarx::RemoteGui::Client;
 
         // Setup the widgets.
+        tab.locationGraph.setup(*this);
 
-        tab.boxLayerName.setValue(properties.boxLayerName);
 
-        tab.numBoxes.setValue(properties.numBoxes);
-        tab.numBoxes.setRange(0, 100);
+        // Setup the layout.
+        VBoxLayout root = {tab.locationGraph.group, VSpacer()};
+        RemoteGui_createTab(getName(), root, &tab);
+    }
 
-        tab.drawBoxes.setLabel("Draw Boxes");
 
-        // Setup the layout.
+    void
+    NavigationMemory::RemoteGui_update()
+    {
+        tab.locationGraph.update(*this);
+    }
 
+
+    void
+    NavigationMemory::RemoteGuiTab::LocationGraph::setup(NavigationMemory& owner)
+    {
+        using namespace armarx::RemoteGui::Client;
         GridLayout grid;
         int row = 0;
         {
-            grid.add(Label("Box Layer"), {row, 0}).add(tab.boxLayerName, {row, 1});
-            ++row;
+            visuLocations.setValue(owner.properties.locationGraph.visuLocations);
+            visuGraphEdges.setValue(owner.properties.locationGraph.visuLocations);
 
-            grid.add(Label("Num Boxes"), {row, 0}).add(tab.numBoxes, {row, 1});
+            grid.add(Label("Visualize Locations"), {row, 0}).add(visuLocations, {row, 1});
             ++row;
 
-            grid.add(tab.drawBoxes, {row, 0}, {2, 1});
+            grid.add(Label("Visualize Graph Edges"), {row, 0}).add(visuGraphEdges, {row, 1});
             ++row;
         }
 
-        VBoxLayout root = {grid, VSpacer()};
-        RemoteGui_createTab(getName(), root, &tab);
+        group = GroupBox({grid});
     }
 
 
-    void NavigationMemory::RemoteGui_update()
+    void
+    NavigationMemory::RemoteGuiTab::LocationGraph::update(NavigationMemory& owner)
     {
-        if (tab.boxLayerName.hasValueChanged() || tab.numBoxes.hasValueChanged())
-        {
-            std::scoped_lock lock(propertiesMutex);
-            properties.boxLayerName = tab.boxLayerName.getValue();
-            properties.numBoxes = tab.numBoxes.getValue();
-
-            {
-                setDebugObserverDatafield("numBoxes", properties.numBoxes);
-                setDebugObserverDatafield("boxLayerName", properties.boxLayerName);
-                sendDebugObserverBatch();
-            }
-        }
-        if (tab.drawBoxes.wasClicked())
+        if (visuLocations.hasValueChanged() or visuGraphEdges.hasValueChanged())
         {
-            // Lock shared variables in methods running in seperate threads
-            // and pass them to functions. This way, the called functions do
-            // not need to think about locking.
-            std::scoped_lock lock(propertiesMutex, arvizMutex);
-            drawBoxes(properties, arviz);
+            std::scoped_lock lock(owner.propertiesMutex);
+            owner.properties.locationGraph.visuLocations = visuLocations.getValue();
+            owner.properties.locationGraph.visuGraphEdges = visuGraphEdges.getValue();
         }
     }
-    */
 
-    /* (Requires the armarx::ArVizComponentPluginUser.)
-    void NavigationMemory::drawBoxes(const NavigationMemory::Properties& p, viz::Client& arviz)
+
+    void
+    NavigationMemory::visuRun()
     {
-        // Draw something in ArViz (requires the armarx::ArVizComponentPluginUser.
-        // See the ArVizExample in RobotAPI for more examples.
+        memory::Visu visu{arviz,
+                          workingMemory.getCoreSegment(navigation::location::coreSegmentID),
+                          workingMemory.getCoreSegment(navigation::graph::coreSegmentID)};
 
-        viz::Layer layer = arviz.layer(p.boxLayerName);
-        for (int i = 0; i < p.numBoxes; ++i)
+        Properties::LocationGraph p;
         {
-            layer.add(viz::Box("box_" + std::to_string(i))
-                      .position(Eigen::Vector3f(i * 100, 0, 0))
-                      .size(20).color(simox::Color::blue()));
+            std::scoped_lock lock(propertiesMutex);
+            p = properties.locationGraph;
+        }
+
+        CycleUtil cycle(static_cast<int>(1000 / p.visuFrequency));
+        while (tasks.visuTask and not tasks.visuTask->isStopped())
+        {
+            {
+                std::scoped_lock lock(propertiesMutex);
+                p = properties.locationGraph;
+            }
+
+            std::vector<viz::Layer> layers;
+
+            // Locations
+            visu.drawLocations(layers, p.visuLocations);
+
+            // Graph Edges
+            visu.drawGraphs(layers, p.visuGraphEdges);
+
+            arviz.commit(layers);
+
+            cycle.waitForCycleDuration();
         }
-        arviz.commit(layer);
     }
-    */
 
-} // namespace armarx
+
+} // namespace armarx::navigation
diff --git a/source/armarx/navigation/components/NavigationMemory/NavigationMemory.h b/source/armarx/navigation/components/NavigationMemory/NavigationMemory.h
index 15d2088a135fa8ca371933a4050df0cdc40e3eb9..8c971578d45181fa6e1884df2d699c849e83d435 100644
--- a/source/armarx/navigation/components/NavigationMemory/NavigationMemory.h
+++ b/source/armarx/navigation/components/NavigationMemory/NavigationMemory.h
@@ -22,21 +22,20 @@
 
 #pragma once
 
-
-// #include <mutex>
+#include <mutex>
 
 #include <ArmarXCore/core/Component.h>
+#include <ArmarXCore/core/services/tasks/TaskUtil.h>
 
-// #include <ArmarXCore/libraries/ArmarXCoreComponentPlugins/DebugObserverComponentPlugin.h>
-
-// #include <ArmarXGui/libraries/ArmarXGuiComponentPlugins/LightweightRemoteGuiComponentPlugin.h>
+#include <ArmarXGui/libraries/ArmarXGuiComponentPlugins/LightweightRemoteGuiComponentPlugin.h>
 
-// #include <RobotAPI/libraries/RobotAPIComponentPlugins/ArVizComponentPlugin.h>
 #include <RobotAPI/libraries/RobotAPIComponentPlugins/ArVizComponentPlugin.h>
 #include <RobotAPI/libraries/armem/server/ComponentPlugin.h>
 
+// #include <ArmarXCore/libraries/ArmarXCoreComponentPlugins/DebugObserverComponentPlugin.h>
+
 
-namespace armarx
+namespace armarx::navigation
 {
 
     /**
@@ -51,12 +50,12 @@ namespace armarx
      * Detailed description of class NavigationMemory.
      */
     class NavigationMemory :
-        virtual public armarx::Component,
+        virtual public armarx::Component
         // , virtual public armarx::DebugObserverComponentPluginUser
-        // , virtual public armarx::LightweightRemoteGuiComponentPluginUser
-        // , virtual public armarx::ArVizComponentPluginUser
+        ,
+        virtual public armarx::LightweightRemoteGuiComponentPluginUser,
+        virtual public armarx::ArVizComponentPluginUser,
         virtual public armarx::armem::server::ComponentPluginUser
-
     {
     public:
         /// @see armarx::ManagedIceObject::getDefaultName()
@@ -80,63 +79,55 @@ namespace armarx
         void onExitComponent() override;
 
 
-        /* (Requires armarx::LightweightRemoteGuiComponentPluginUser.)
-        /// This function should be called once in onConnect() or when you
-        /// need to re-create the Remote GUI tab.
         void createRemoteGuiTab();
-
-        /// After calling `RemoteGui_startRunningTask`, this function is
-        /// called periodically in a separate thread. If you update variables,
-        /// make sure to synchronize access to them.
         void RemoteGui_update() override;
-        */
 
 
     private:
-        // Private methods go here.
-
-        // Forward declare `Properties` if you used it before its defined.
-        // struct Properties;
-
-        /* (Requires the armarx::ArVizComponentPluginUser.)
-        /// Draw some boxes in ArViz.
-        void drawBoxes(const Properties& p, viz::Client& arviz);
-        */
+        void visuRun();
 
 
     private:
-        // Private member variables go here.
-
-
         /// Properties shown in the Scenario GUI.
         struct Properties
         {
             std::string memoryName = "Navigation";
+
+            struct LocationGraph
+            {
+                bool visuLocations = true;
+                bool visuGraphEdges = true;
+                float visuFrequency = 2;
+            };
+            LocationGraph locationGraph;
         };
-        Properties p;
-        /* Use a mutex if you access variables from different threads
-         * (e.g. ice functions and RemoteGui_update()).
+        Properties properties;
         std::mutex propertiesMutex;
-        */
 
 
-        /* (Requires the armarx::LightweightRemoteGuiComponentPluginUser.)
         /// Tab shown in the Remote GUI.
         struct RemoteGuiTab : armarx::RemoteGui::Client::Tab
         {
-            armarx::RemoteGui::Client::LineEdit boxLayerName;
-            armarx::RemoteGui::Client::IntSpinBox numBoxes;
+            struct LocationGraph
+            {
+                armarx::RemoteGui::Client::GroupBox group;
 
-            armarx::RemoteGui::Client::Button drawBoxes;
+                armarx::RemoteGui::Client::CheckBox visuLocations;
+                armarx::RemoteGui::Client::CheckBox visuGraphEdges;
+
+                void setup(NavigationMemory& owner);
+                void update(NavigationMemory& owner);
+            };
+            LocationGraph locationGraph;
         };
         RemoteGuiTab tab;
-        */
 
 
-        /* (Requires the armarx::ArVizComponentPluginUser.)
-         * When used from different threads, an ArViz client needs to be synchronized.
-        /// Protects the arviz client inherited from the ArViz plugin.
-        std::mutex arvizMutex;
-        */
+        struct Tasks
+        {
+            SimpleRunningTask<>::pointer_type visuTask;
+        };
+        Tasks tasks;
     };
-} // namespace armarx
+
+} // namespace armarx::navigation
diff --git a/source/armarx/navigation/components/NavigationMemory/Visu.cpp b/source/armarx/navigation/components/NavigationMemory/Visu.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..244db064c5f4d7a1ecd099b8c1d7fdeee354880e
--- /dev/null
+++ b/source/armarx/navigation/components/NavigationMemory/Visu.cpp
@@ -0,0 +1,120 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package    Navigation::ArmarXObjects::NavigationMemory
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#include "Visu.h"
+
+#include <RobotAPI/libraries/armem/server/wm/memory_definitions.h>
+
+#include <armarx/navigation/graph/Graph.h>
+#include <armarx/navigation/graph/Visu.h>
+#include <armarx/navigation/graph/aron/Graph.aron.generated.h>
+#include <armarx/navigation/location/aron/Location.aron.generated.h>
+
+
+namespace armarx::navigation::memory
+{
+
+    Visu::Visu(viz::Client arviz,
+               const armem::server::wm::CoreSegment& locSegment,
+               const armem::server::wm::CoreSegment& graphSegment) :
+        arviz(arviz),
+        locSegment(locSegment),
+        graphSegment(graphSegment),
+        visu(std::make_unique<graph::GraphVisu>())
+    {
+    }
+
+
+    Visu::~Visu()
+    {
+    }
+
+
+    void
+    Visu::drawLocations(std::vector<viz::Layer>& layers, bool enabled)
+    {
+        using namespace armem::server;
+
+        viz::Layer& layer = layers.emplace_back(arviz.layer(locSegment.id().str()));
+        if (enabled)
+        {
+            std::map<armem::MemoryID, location::arondto::Location> locations;
+            locSegment.doLocked(
+                [&]()
+                {
+                    locSegment.forEachEntity(
+                        [&](const wm::Entity& entity)
+                        {
+                            if (const wm::EntityInstance* instance = entity.findLatestInstance())
+                            {
+                                locations[entity.id()].fromAron(instance->data());
+                            }
+                        });
+                });
+
+            for (auto& [id, location] : locations)
+            {
+                visu->vertex->draw(layer, id.str(), location.globalRobotPose);
+            }
+        }
+    }
+
+
+    void
+    Visu::drawGraphs(std::vector<viz::Layer>& layers, bool enabled)
+    {
+        using namespace armem::server;
+
+        std::map<armem::MemoryID, graph::Graph> graphs;
+        graphSegment.doLocked(
+            [&]()
+            {
+                graphSegment.forEachEntity(
+                    [&](const wm::Entity& entity)
+                    {
+                        graph::Graph& graph = graphs[entity.id()];
+                        if (enabled)
+                        {
+                            if (const wm::EntityInstance* instance = entity.findLatestInstance())
+                            {
+                                navigation::graph::arondto::Graph aron;
+                                aron.fromAron(instance->data());
+                                fromAron(aron, graph);
+                            }
+                        }
+                        // else: empty layer
+                    });
+            });
+
+        for (auto& [id, graph] : graphs)
+        {
+            viz::Layer& layer = layers.emplace_back(arviz.layer(id.str()));
+            if (enabled)
+            {
+                graph::resolveLocations(graph, locSegment);
+                visu->draw(layer, graph);
+            }
+            // else: clear layer
+        }
+    }
+
+} // namespace armarx::navigation::memory
diff --git a/source/armarx/navigation/components/NavigationMemory/Visu.h b/source/armarx/navigation/components/NavigationMemory/Visu.h
new file mode 100644
index 0000000000000000000000000000000000000000..f28fa6d8c7a2a208596e835003d76b19a49c2f33
--- /dev/null
+++ b/source/armarx/navigation/components/NavigationMemory/Visu.h
@@ -0,0 +1,63 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package    Navigation::ArmarXObjects::NavigationMemory
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <memory>
+#include <vector>
+
+#include <RobotAPI/components/ArViz/Client/Client.h>
+#include <RobotAPI/libraries/armem/core/forward_declarations.h>
+
+
+namespace armarx::navigation::graph
+{
+    class GraphVisu;
+}
+
+namespace armarx::navigation::memory
+{
+
+    class Visu
+    {
+    public:
+        Visu(viz::Client arviz,
+             const armem::server::wm::CoreSegment& locSegment,
+             const armem::server::wm::CoreSegment& graphSegment);
+        ~Visu();
+
+
+        void drawLocations(std::vector<viz::Layer>& layers, bool enabled);
+        void drawGraphs(std::vector<viz::Layer>& layers, bool enabled);
+
+
+    public:
+        viz::Client arviz;
+
+        const armem::server::wm::CoreSegment& locSegment;
+        const armem::server::wm::CoreSegment& graphSegment;
+
+        std::unique_ptr<navigation::graph::GraphVisu> visu;
+    };
+
+
+} // namespace armarx::navigation::memory
diff --git a/source/armarx/navigation/components/Navigator/Navigator.cpp b/source/armarx/navigation/components/Navigator/Navigator.cpp
index bd86572b20140414c145d246cc0c2e29d330273d..26eaba1fe1f14f26d7839dddb492f623228e5846 100644
--- a/source/armarx/navigation/components/Navigator/Navigator.cpp
+++ b/source/armarx/navigation/components/Navigator/Navigator.cpp
@@ -41,6 +41,7 @@
 #include <ArmarXCore/core/logging/LogSender.h>
 #include <ArmarXCore/core/services/tasks/PeriodicTask.h>
 #include <ArmarXCore/core/services/tasks/TaskUtil.h>
+#include <ArmarXCore/libraries/DecoupledSingleComponent/Decoupled.h>
 #include <ArmarXCore/util/CPPUtility/trace.h>
 
 #include "ArmarXGui/libraries/RemoteGui/Client/Widgets.h"
@@ -56,314 +57,330 @@
 #include <armarx/navigation/server/introspection/MemoryIntrospector.h>
 #include <armarx/navigation/util/util.h>
 
-std::vector<armarx::navigation::core::Pose>
-convert(const std::vector<Eigen::Matrix4f>& wps)
+
+namespace armarx::navigation::components
 {
-    using namespace armarx::navigation;
-    std::vector<core::Pose> p;
-    p.reserve(wps.size());
-    std::transform(
-        wps.begin(), wps.end(), std::back_inserter(p), [](const auto& p) { return core::Pose(p); });
-    return p;
+    ARMARX_DECOUPLED_REGISTER_COMPONENT(Navigator);
 }
 
-armarx::navigation::components::Navigator::Navigator() :
-    parameterizationReader(memoryNameSystem),
-    parameterizationWriter(memoryNameSystem),
-    eventsWriter(memoryNameSystem),
-    resultsWriter(memoryNameSystem),
-    parameterizationService(&parameterizationReader, &parameterizationWriter),
-    publisher(&resultsWriter, &eventsWriter)
+namespace armarx::navigation
 {
-    scene.timeServer = &timeServer;
-}
 
+    std::vector<core::Pose>
+    convert(const std::vector<Eigen::Matrix4f>& wps)
+    {
+        using namespace armarx::navigation;
+        std::vector<core::Pose> p;
+        p.reserve(wps.size());
+        std::transform(wps.begin(),
+                       wps.end(),
+                       std::back_inserter(p),
+                       [](const auto& p) { return core::Pose(p); });
+        return p;
+    }
 
-armarx::navigation::components::Navigator::~Navigator() = default;
+} // namespace armarx::navigation
 
-void
-armarx::navigation::components::Navigator::onInitComponent()
+namespace armarx::navigation::components
 {
-}
 
-void
-armarx::navigation::components::Navigator::onConnectComponent()
-{
-    static bool initialized{false};
-
-    // redirect to onReconnect to avoid any setup issues
-    if (initialized)
+    components::Navigator::Navigator() :
+        parameterizationReader(memoryNameSystem),
+        parameterizationWriter(memoryNameSystem),
+        eventsWriter(memoryNameSystem),
+        resultsWriter(memoryNameSystem),
+        parameterizationService(&parameterizationReader, &parameterizationWriter),
+        publisher(&resultsWriter, &eventsWriter)
     {
-        ARMARX_INFO << "Reconnecting ...";
-        onReconnectComponent();
-        return;
+        scene.timeServer = &timeServer;
     }
 
-    // TODO check if reconnecting
 
-    scene.robot = getRobot();
-    scene.staticScene = staticScene();
+    components::Navigator::~Navigator() = default;
 
-    executor = server::PlatformUnitExecutor(platformUnit);
+    void
+    components::Navigator::onInitComponent()
+    {
+    }
 
-    introspector = server::ArvizIntrospector(getArvizClient(), scene.robot);
+    void
+    components::Navigator::onConnectComponent()
+    {
+        static bool initialized{false};
 
-    // TODO dynamic scene
-    // TODO memory
-    // TODO param (10)
-    resultsWriter.connect();
-    eventsWriter.connect();
-    // parameterizationReader.connect();
-    parameterizationWriter.connect();
+        // redirect to onReconnect to avoid any setup issues
+        if (initialized)
+        {
+            ARMARX_INFO << "Reconnecting ...";
+            onReconnectComponent();
+            return;
+        }
 
+        // TODO check if reconnecting
 
-    robotStateUpdateTask = new PeriodicTask<Navigator>(
-        this, &Navigator::updateRobot, 10, false, "RobotStateUpdateTask");
-    robotStateUpdateTask->start();
+        scene.robot = getRobot();
+        scene.staticScene = staticScene();
 
-    navRemoteGui = std::make_unique<NavigatorRemoteGui>(remoteGui, *this);
-    navRemoteGui->enable();
+        executor = server::PlatformUnitExecutor(platformUnit);
 
+        introspector = server::ArvizIntrospector(getArvizClient(), scene.robot);
 
-    initialized = true;
-}
+        // TODO dynamic scene
+        // TODO memory
+        // TODO param (10)
+        resultsWriter.connect();
+        eventsWriter.connect();
+        // parameterizationReader.connect();
+        parameterizationWriter.connect();
 
-void
-armarx::navigation::components::Navigator::onReconnectComponent()
-{
-    robotStateUpdateTask->start();
 
-    // TODO not in all cases meaningful
-    //resume();
-    resultsWriter.connect();
-    eventsWriter.connect();
-    // parameterizationReader.connect();
-    parameterizationWriter.connect();
+        robotStateUpdateTask = new PeriodicTask<Navigator>(
+            this, &Navigator::updateRobot, 10, false, "RobotStateUpdateTask");
+        robotStateUpdateTask->start();
 
-    navRemoteGui->enable();
-}
+        navRemoteGui = std::make_unique<NavigatorRemoteGui>(remoteGui, *this);
+        navRemoteGui->enable();
 
-void
-armarx::navigation::components::Navigator::onDisconnectComponent()
-{
-    robotStateUpdateTask->stop();
 
-    stopAll();
+        initialized = true;
+    }
 
-    navRemoteGui->disable();
-}
+    void
+    components::Navigator::onReconnectComponent()
+    {
+        robotStateUpdateTask->start();
 
-void
-armarx::navigation::components::Navigator::onExitComponent()
-{
-}
+        // TODO not in all cases meaningful
+        //resume();
+        resultsWriter.connect();
+        eventsWriter.connect();
+        // parameterizationReader.connect();
+        parameterizationWriter.connect();
 
-void
-armarx::navigation::components::Navigator::updateContext()
-{
-    scene.robot = getRobot();
-    scene.staticScene = staticScene();
-    // TODO dynamic scene
-}
+        navRemoteGui->enable();
+    }
 
-std::string
-armarx::navigation::components::Navigator::getDefaultName() const
-{
-    return "Navigator";
-}
+    void
+    components::Navigator::onDisconnectComponent()
+    {
+        robotStateUpdateTask->stop();
 
-void
-armarx::navigation::components::Navigator::createConfig(const aron::data::AronDictPtr& stackConfig,
-                                                        const std::string& callerId,
-                                                        const Ice::Current&)
-{
-    // TODO: Not thread-safe.
-    ARMARX_TRACE;
-    ARMARX_INFO << "Creating config for caller '" << callerId << "'";
-
-    parameterizationService.store(stackConfig, callerId, timeServer.now());
-
-    server::NavigationStack stack = fac::NavigationStackFactory::create(stackConfig, scene);
-
-    memoryIntrospectors.emplace_back(
-        std::make_unique<server::MemoryIntrospector>(resultsWriter, callerId));
-
-    navigators.emplace(
-        std::piecewise_construct,
-        std::forward_as_tuple(callerId),
-        std::forward_as_tuple(
-            server::Navigator::Config{.stack = std::move(stack),
-                                      .scene = &scene,
-                                      .general = server::Navigator::Config::General{}},
-            server::Navigator::InjectedServices{.executor = &executor.value(),
-                                                .publisher = &publisher,
-                                                .introspector = &(introspector.value())}));
-}
+        stopAll();
 
-void
-armarx::navigation::components::Navigator::moveTo(const std::vector<Eigen::Matrix4f>& waypoints,
-                                                  const std::string& navigationMode,
-                                                  const std::string& callerId,
-                                                  const Ice::Current&)
-{
+        navRemoteGui->disable();
+    }
 
-    ARMARX_INFO << "moveTo() requested by caller '" << callerId << "'";
+    void
+    components::Navigator::onExitComponent()
+    {
+    }
 
-    try
+    void
+    components::Navigator::updateContext()
     {
-        navigators.at(callerId).moveTo(convert(waypoints),
-                                       core::NavigationFrameNames.from_name(navigationMode));
+        scene.robot = getRobot();
+        scene.staticScene = staticScene();
+        // TODO dynamic scene
     }
-    catch (...)
+
+    std::string
+    components::Navigator::getDefaultName() const
     {
-        // TODO: Error handling.
-        ARMARX_WARNING << "Failed to execute moveTo()."
-                       << "Movement will be paused.";
-        stopAll(); // TODO pause movement must not throw
-        throw;
+        return "Navigator";
     }
-}
 
-void
-armarx::navigation::components::Navigator::moveTowards(const Eigen::Vector3f& direction,
-                                                       const std::string& navigationMode,
-                                                       const std::string& callerId,
-                                                       const Ice::Current&)
-{
-    // TODO: Error handling.
+    void
+    components::Navigator::createConfig(const aron::data::AronDictPtr& stackConfig,
+                                        const std::string& callerId,
+                                        const Ice::Current&)
+    {
+        // TODO: Not thread-safe.
+        ARMARX_TRACE;
+        ARMARX_INFO << "Creating config for caller '" << callerId << "'";
+
+        parameterizationService.store(stackConfig, callerId, timeServer.now());
+
+        server::NavigationStack stack = fac::NavigationStackFactory::create(stackConfig, scene);
+
+        memoryIntrospectors.emplace_back(
+            std::make_unique<server::MemoryIntrospector>(resultsWriter, callerId));
+
+        navigators.emplace(
+            std::piecewise_construct,
+            std::forward_as_tuple(callerId),
+            std::forward_as_tuple(
+                server::Navigator::Config{.stack = std::move(stack),
+                                          .scene = &scene,
+                                          .general = server::Navigator::Config::General{}},
+                server::Navigator::InjectedServices{.executor = &executor.value(),
+                                                    .publisher = &publisher,
+                                                    .introspector = &(introspector.value())}));
+    }
 
-    ARMARX_INFO << "MoveTowards requested by caller '" << callerId << "'";
+    void
+    components::Navigator::moveTo(const std::vector<Eigen::Matrix4f>& waypoints,
+                                  const std::string& navigationMode,
+                                  const std::string& callerId,
+                                  const Ice::Current&)
+    {
 
-    navigators.at(callerId).moveTowards(direction,
-                                        core::NavigationFrameNames.from_name(navigationMode));
-}
+        ARMARX_INFO << "moveTo() requested by caller '" << callerId << "'";
+
+        try
+        {
+            navigators.at(callerId).moveTo(convert(waypoints),
+                                           core::NavigationFrameNames.from_name(navigationMode));
+        }
+        catch (...)
+        {
+            // TODO: Error handling.
+            ARMARX_WARNING << "Failed to execute moveTo()."
+                           << "Movement will be paused.";
+            stopAll(); // TODO pause movement must not throw
+            throw;
+        }
+    }
 
-void
-armarx::navigation::components::Navigator::pause(const std::string& configId, const Ice::Current&)
-{
-    navigators.at(configId).pause();
-}
+    void
+    components::Navigator::moveTowards(const Eigen::Vector3f& direction,
+                                       const std::string& navigationMode,
+                                       const std::string& callerId,
+                                       const Ice::Current&)
+    {
+        // TODO: Error handling.
 
-void
-armarx::navigation::components::Navigator::resume(const std::string& configId, const Ice::Current&)
-{
-    navigators.at(configId).resume();
-}
+        ARMARX_INFO << "MoveTowards requested by caller '" << callerId << "'";
 
-void
-armarx::navigation::components::Navigator::stop(const std::string& configId, const Ice::Current&)
-{
-    navigators.at(configId).stop();
-}
+        navigators.at(callerId).moveTowards(direction,
+                                            core::NavigationFrameNames.from_name(navigationMode));
+    }
 
-void
-armarx::navigation::components::Navigator::stopAll(const Ice::Current&)
-{
-    for (auto& [_, navigator] : navigators)
+    void
+    components::Navigator::pause(const std::string& configId, const Ice::Current&)
     {
-        navigator.stop();
+        navigators.at(configId).pause();
     }
-}
 
-bool
-armarx::navigation::components::Navigator::isPaused(const std::string& configId,
-                                                    const Ice::Current&)
-{
-    return navigators.at(configId).isPaused();
-}
+    void
+    components::Navigator::resume(const std::string& configId, const Ice::Current&)
+    {
+        navigators.at(configId).resume();
+    }
 
-bool
-armarx::navigation::components::Navigator::isStopped(const std::string& configId,
-                                                     const Ice::Current&)
-{
-    return navigators.at(configId).isStopped();
-}
+    void
+    components::Navigator::stop(const std::string& configId, const Ice::Current&)
+    {
+        navigators.at(configId).stop();
+    }
 
-armarx::PropertyDefinitionsPtr
-armarx::navigation::components::Navigator::createPropertyDefinitions()
-{
-    ARMARX_TRACE;
+    void
+    components::Navigator::stopAll(const Ice::Current&)
+    {
+        for (auto& [_, navigator] : navigators)
+        {
+            navigator.stop();
+        }
+    }
+
+    bool
+    components::Navigator::isPaused(const std::string& configId, const Ice::Current&)
+    {
+        return navigators.at(configId).isPaused();
+    }
 
-    PropertyDefinitionsPtr def = new ComponentPropertyDefinitions(getConfigIdentifier());
+    bool
+    components::Navigator::isStopped(const std::string& configId, const Ice::Current&)
+    {
+        return navigators.at(configId).isStopped();
+    }
 
-    // Publish to a topic (passing the TopicListenerPrx).
-    // def->topic(myTopicListener);
+    armarx::PropertyDefinitionsPtr
+    components::Navigator::createPropertyDefinitions()
+    {
+        ARMARX_TRACE;
 
-    // Subscribe to a topic (passing the topic name).
-    // def->topic<PlatformUnitListener>("MyTopic");
+        PropertyDefinitionsPtr def = new ComponentPropertyDefinitions(getConfigIdentifier());
 
-    // Use (and depend on) another component (passing the ComponentInterfacePrx).
-    def->component(platformUnit);
+        // Publish to a topic (passing the TopicListenerPrx).
+        // def->topic(myTopicListener);
 
-    def->component(remoteGui, "RemoteGuiProvider");
-    // Add a required property.
-    // def->required(properties.boxLayerName, "p.box.LayerName", "Name of the box layer in ArViz.");
+        // Subscribe to a topic (passing the topic name).
+        // def->topic<PlatformUnitListener>("MyTopic");
 
-    // Add an optionalproperty.
-    // def->optional(properties.numBoxes, "p.box.Number", "Number of boxes to draw in ArViz.");
+        // Use (and depend on) another component (passing the ComponentInterfacePrx).
+        def->component(platformUnit);
 
-    resultsWriter.registerPropertyDefinitions(def);
-    eventsWriter.registerPropertyDefinitions(def);
-    // parameterizationReader.registerPropertyDefinitions(def);
-    parameterizationWriter.registerPropertyDefinitions(def);
+        def->component(remoteGui, "RemoteGuiProvider");
+        // Add a required property.
+        // def->required(properties.boxLayerName, "p.box.LayerName", "Name of the box layer in ArViz.");
 
-    return def;
-}
+        // Add an optionalproperty.
+        // def->optional(properties.numBoxes, "p.box.Number", "Number of boxes to draw in ArViz.");
 
-armarx::navigation::core::StaticScene
-armarx::navigation::components::Navigator::staticScene()
-{
-    ARMARX_TRACE;
+        resultsWriter.registerPropertyDefinitions(def);
+        eventsWriter.registerPropertyDefinitions(def);
+        // parameterizationReader.registerPropertyDefinitions(def);
+        parameterizationWriter.registerPropertyDefinitions(def);
 
-    const objpose::ObjectPoseSeq objectPoses = ObjectPoseClientPluginUser::getObjectPoses();
-    core::StaticScene scene{.objects = util::asSceneObjects(objectPoses)};
+        return def;
+    }
 
-    ARMARX_INFO << "The scene consists of " << scene.objects->getSize() << " objects";
+    core::StaticScene
+    components::Navigator::staticScene()
+    {
+        ARMARX_TRACE;
 
-    return scene;
-}
+        const objpose::ObjectPoseSeq objectPoses = ObjectPoseClientPluginUser::getObjectPoses();
+        core::StaticScene scene{.objects = util::asSceneObjects(objectPoses)};
 
-VirtualRobot::RobotPtr
-armarx::navigation::components::Navigator::getRobot()
-{
-    ARMARX_TRACE;
+        ARMARX_INFO << "The scene consists of " << scene.objects->getSize() << " objects";
 
-    auto robot = RemoteRobot::createLocalCloneFromFile(
-        getRobotStateComponent(), VirtualRobot::RobotIO::RobotDescription::eFull);
-    // auto robot = RemoteRobot::createLocalClone(getRobotStateComponent());
-    ARMARX_CHECK_NOT_NULL(robot);
+        return scene;
+    }
 
-    // ARMARX_INFO << "Resetting robot";
-    // robot = VirtualRobot::RobotIO::loadRobot(robot->getFilename());
+    VirtualRobot::RobotPtr
+    components::Navigator::getRobot()
+    {
+        ARMARX_TRACE;
 
-    RemoteRobot::synchronizeLocalClone(robot, getRobotStateComponent());
+        auto robot = RemoteRobot::createLocalCloneFromFile(
+            getRobotStateComponent(), VirtualRobot::RobotIO::RobotDescription::eFull);
+        // auto robot = RemoteRobot::createLocalClone(getRobotStateComponent());
+        ARMARX_CHECK_NOT_NULL(robot);
 
-    return robot;
-}
+        // ARMARX_INFO << "Resetting robot";
+        // robot = VirtualRobot::RobotIO::loadRobot(robot->getFilename());
 
-void
-armarx::navigation::components::Navigator::updateRobot()
-{
-    synchronizeLocalClone(scene.robot);
-}
+        RemoteRobot::synchronizeLocalClone(robot, getRobotStateComponent());
 
-armarx::navigation::server::Navigator*
-armarx::navigation::components::Navigator::activeNavigator()
-{
-    // We define the active navigator to be the one whose movement is enabled.
-    const auto isActive = [](auto& nameNavPair) -> bool
-    {
-        server::Navigator& navigator = nameNavPair.second;
-        return not navigator.isPaused();
-    };
+        return robot;
+    }
 
-    auto it = std::find_if(navigators.begin(), navigators.end(), isActive);
+    void
+    components::Navigator::updateRobot()
+    {
+        synchronizeLocalClone(scene.robot);
+    }
 
-    // no navigator active?
-    if (it == navigators.end())
+    server::Navigator*
+    components::Navigator::activeNavigator()
     {
-        return nullptr;
+        // We define the active navigator to be the one whose movement is enabled.
+        const auto isActive = [](auto& nameNavPair) -> bool
+        {
+            server::Navigator& navigator = nameNavPair.second;
+            return not navigator.isPaused();
+        };
+
+        auto it = std::find_if(navigators.begin(), navigators.end(), isActive);
+
+        // no navigator active?
+        if (it == navigators.end())
+        {
+            return nullptr;
+        }
+
+        return &it->second;
     }
 
-    return &it->second;
-}
+} // namespace armarx::navigation::components
diff --git a/source/armarx/navigation/graph/CMakeLists.txt b/source/armarx/navigation/graph/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..5816924140bc85b5e4890840b4bf1b14e8ded5f9
--- /dev/null
+++ b/source/armarx/navigation/graph/CMakeLists.txt
@@ -0,0 +1,38 @@
+
+
+armarx_add_aron_library(graph_aron
+    ARON_FILES
+        aron/Graph.xml
+)
+
+armarx_add_library(graph
+    DEPENDENCIES
+        # ArmarXCore
+        ArmarXCoreInterfaces
+        ArmarXCore
+
+        # RobotAPI
+        aron
+        armem
+        # SemanticObjectRelations
+        SemanticObjectRelations
+        # this package
+        armarx_navigation::graph_aron
+
+        Simox::SimoxUtility
+        Simox::VirtualRobot
+    DEPENDENCIES_LEGACY
+        VTK
+    SOURCES
+        constants.cpp
+        Graph.cpp
+        Visu.cpp
+    HEADERS
+        constants.h
+        forward_declarations.h
+        Graph.h
+        Visu.h
+)
+
+# THIS IS A HACK. Simox / other libs do not set this definition properly ...
+target_compile_definitions(graph PUBLIC -DEIGEN_STL_VECTOR_SPECIFICATION_DEFINED)
diff --git a/source/armarx/navigation/graph/Graph.cpp b/source/armarx/navigation/graph/Graph.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..db69eabb2ffc6122053e609709447267403fcf79
--- /dev/null
+++ b/source/armarx/navigation/graph/Graph.cpp
@@ -0,0 +1,117 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#include "Graph.h"
+
+#include <ArmarXCore/core/exceptions/local/ExpressionException.h>
+
+#include <RobotAPI/libraries/armem/core/wm/memory_definitions.h>
+
+#include <armarx/navigation/location/aron/Location.aron.generated.h>
+
+
+namespace armarx::navigation::graph
+{
+
+    std::string
+    VertexAttribs::getName() const
+    {
+        return aron.locationID.providerSegmentName + "/" + aron.locationID.entityName;
+    }
+
+    bool
+    VertexAttribs::hasPose() const
+    {
+        return _pose.has_value();
+    }
+
+    Eigen::Matrix4f
+    VertexAttribs::getPose() const
+    {
+        ARMARX_CHECK(_pose.has_value());
+        return _pose.value();
+    }
+
+    void
+    VertexAttribs::setPose(const Eigen::Matrix4f& pose)
+    {
+        this->_pose = pose;
+    }
+
+} // namespace armarx::navigation::graph
+
+
+namespace armarx::navigation
+{
+
+    void
+    graph::toAron(arondto::Graph& dto, const Graph& bo)
+    {
+        dto = {};
+        for (auto vertex : bo.vertices())
+        {
+            auto& v = dto.vertices.emplace_back(vertex.attrib().aron);
+            v.vertexID = static_cast<long>(vertex.objectID());
+        }
+        ARMARX_CHECK_EQUAL(dto.vertices.size(), bo.numVertices());
+
+        for (auto edge : bo.edges())
+        {
+            auto& e = dto.edges.emplace_back(edge.attrib().aron);
+            e.sourceVertexID = static_cast<long>(edge.sourceObjectID());
+            e.targetVertexID = static_cast<long>(edge.targetObjectID());
+        }
+        ARMARX_CHECK_EQUAL(dto.edges.size(), bo.numEdges());
+    }
+
+
+    void
+    graph::fromAron(const arondto::Graph& dto, Graph& bo)
+    {
+        bo = {};
+        for (const arondto::Vertex& vertex : dto.vertices)
+        {
+            auto v = bo.addVertex(semrel::ShapeID(vertex.vertexID));
+            v.attrib().aron = vertex;
+        }
+        ARMARX_CHECK_EQUAL(bo.numVertices(), dto.vertices.size());
+
+        for (const arondto::Edge& edge : dto.edges)
+        {
+            auto e = bo.addEdge(semrel::ShapeID(edge.sourceVertexID),
+                                semrel::ShapeID(edge.targetVertexID));
+            e.attrib().aron = edge;
+        }
+        ARMARX_CHECK_EQUAL(bo.numEdges(), dto.edges.size());
+    }
+
+
+    void
+    graph::resolveLocation(Graph::Vertex& vertex,
+                           const aron::datanavigator::DictNavigatorPtr& locationData)
+    {
+        navigation::location::arondto::Location dto;
+        dto.fromAron(locationData);
+        vertex.attrib().setPose(dto.globalRobotPose);
+    }
+
+
+} // namespace armarx::navigation
diff --git a/source/armarx/navigation/graph/Graph.h b/source/armarx/navigation/graph/Graph.h
new file mode 100644
index 0000000000000000000000000000000000000000..388d4eae8a8b1e36d1ef40498b112ca7f598c293
--- /dev/null
+++ b/source/armarx/navigation/graph/Graph.h
@@ -0,0 +1,100 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <RobotAPI/libraries/armem/core/MemoryID.h>
+#include <RobotAPI/libraries/armem/core/aron_conversions.h>
+#include <RobotAPI/libraries/armem/core/forward_declarations.h>
+
+#include <SemanticObjectRelations/RelationGraph/RelationGraph.h>
+#include <armarx/navigation/graph/aron/Graph.aron.generated.h>
+
+
+namespace armarx::navigation::graph
+{
+
+    struct VertexAttribs : public semrel::ShapeVertex
+    {
+        armarx::navigation::graph::arondto::Vertex aron;
+
+        std::string getName() const;
+
+        bool hasPose() const;
+        Eigen::Matrix4f getPose() const;
+        void setPose(const Eigen::Matrix4f& pose);
+
+    private:
+        std::optional<Eigen::Matrix4f> _pose;
+    };
+
+    struct EdgeAttribs
+    {
+        armarx::navigation::graph::arondto::Edge aron;
+    };
+
+    struct GraphAttribs
+    {
+    };
+
+    using Graph = semrel::RelationGraph<VertexAttribs, EdgeAttribs, GraphAttribs>;
+
+
+    void toAron(arondto::Graph& dto, const Graph& bo);
+    void fromAron(const arondto::Graph& dto, Graph& bo);
+
+
+    // Location resolution
+
+    void resolveLocation(Graph::Vertex& vertex,
+                         const aron::datanavigator::DictNavigatorPtr& locationData);
+
+
+    template <class MemoryContainerT>
+    bool
+    resolveLocation(Graph::Vertex& vertex, const MemoryContainerT& locationContainer)
+    {
+        armem::MemoryID locationID;
+        fromAron(vertex.attrib().aron.locationID, locationID);
+
+        if (const auto* instance = locationContainer.findLatestInstance(locationID))
+        {
+            resolveLocation(vertex, instance->data());
+            return true;
+        }
+        else
+        {
+            return false;
+        }
+    }
+
+
+    template <class MemoryContainerT>
+    void
+    resolveLocations(Graph& graph, const MemoryContainerT& locationContainer)
+    {
+        for (graph::Graph::Vertex vertex : graph.vertices())
+        {
+            resolveLocation(vertex, locationContainer);
+        }
+    }
+
+} // namespace armarx::navigation::graph
diff --git a/source/armarx/navigation/graph/Visu.cpp b/source/armarx/navigation/graph/Visu.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..38b8a0811a6090fca76508fb15539a64e80c6fad
--- /dev/null
+++ b/source/armarx/navigation/graph/Visu.cpp
@@ -0,0 +1,142 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#include "Visu.h"
+
+#include <SimoxUtility/color/Color.h>
+#include <SimoxUtility/math/pose.h>
+#include <VirtualRobot/VirtualRobot.h>
+
+#include <RobotAPI/components/ArViz/Client/Client.h>
+
+
+namespace armarx::navigation::graph
+{
+
+    viz::Pose
+    VertexVisu::Pose::draw(const VertexAttribs& attribs) const
+    {
+        return draw(attribs.getName(), attribs.getPose());
+    }
+
+    viz::Pose
+    VertexVisu::Pose::draw(const std::string& name, const Eigen::Matrix4f& pose) const
+    {
+        return viz::Pose(name).pose(pose).scale(scale);
+    }
+
+
+    viz::Arrow
+    VertexVisu::ForwardArrow::draw(const VertexAttribs& attribs) const
+    {
+        return draw(attribs.getName(), attribs.getPose());
+    }
+
+
+    viz::Arrow
+    VertexVisu::ForwardArrow::draw(const std::string& name, const Eigen::Matrix4f& pose) const
+    {
+        return viz::Arrow(name + " forward")
+            .fromTo(simox::math::position(pose),
+                    simox::math::transform_position(pose, length * Eigen::Vector3f::UnitY()))
+            .color(color)
+            .width(width);
+    }
+
+
+    void
+    VertexVisu::draw(viz::Layer& layer, Graph::ConstVertex vertex) const
+    {
+        draw(layer, vertex.attrib());
+    }
+
+
+    void
+    VertexVisu::draw(viz::Layer& layer, const VertexAttribs& attribs) const
+    {
+        draw(layer, attribs.getName(), attribs.getPose());
+    }
+
+
+    void
+    VertexVisu::draw(viz::Layer& layer, const std::string& name, const Eigen::Matrix4f& pose) const
+    {
+        if (this->pose.has_value())
+        {
+            layer.add(this->pose->draw(name, pose));
+        }
+        if (forwardArrow.has_value())
+        {
+            layer.add(forwardArrow->draw(name, pose));
+        }
+    }
+
+
+    viz::Arrow
+    EdgeVisu::Arrow::draw(Graph::ConstEdge edge) const
+    {
+        return draw(edge.attrib(), edge.source().attrib(), edge.target().attrib());
+    }
+
+
+    viz::Arrow
+    EdgeVisu::Arrow::draw(const EdgeAttribs& edge,
+                          const VertexAttribs& source,
+                          const VertexAttribs& target) const
+    {
+        (void)edge;
+        return viz::Arrow(source.getName() + " -> " + target.getName())
+            .fromTo(simox::math::position(source.getPose()),
+                    simox::math::position(target.getPose()))
+            .width(width)
+            .color(color);
+    }
+
+
+    void
+    EdgeVisu::draw(viz::Layer& layer, Graph::ConstEdge edge) const
+    {
+        if (arrow.has_value())
+        {
+            layer.add(arrow->draw(edge));
+        }
+    }
+
+
+    void
+    GraphVisu::draw(viz::Layer& layer, const Graph& graph) const
+    {
+        if (vertex.has_value())
+        {
+            for (Graph::ConstVertex v : graph.vertices())
+            {
+                vertex->draw(layer, v);
+            }
+        }
+        if (edge.has_value())
+        {
+            for (Graph::ConstEdge e : graph.edges())
+            {
+                edge->draw(layer, e);
+            }
+        }
+    }
+} // namespace armarx::navigation::graph
diff --git a/source/armarx/navigation/graph/Visu.h b/source/armarx/navigation/graph/Visu.h
new file mode 100644
index 0000000000000000000000000000000000000000..482ae4efd45a8d69f73b7da92caf7cbcaf15c081
--- /dev/null
+++ b/source/armarx/navigation/graph/Visu.h
@@ -0,0 +1,98 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <optional>
+
+#include <SimoxUtility/color/Color.h>
+
+#include <armarx/navigation/graph/Graph.h>
+
+
+namespace armarx::viz
+{
+    class Arrow;
+    class Layer;
+    class Pose;
+} // namespace armarx::viz
+
+namespace armarx::navigation::graph
+{
+
+    struct VertexVisu
+    {
+        struct Pose
+        {
+            float scale = 1.0;
+
+            viz::Pose draw(const VertexAttribs& attribs) const;
+            viz::Pose draw(const std::string& name, const Eigen::Matrix4f& pose) const;
+        };
+        std::optional<Pose> pose = Pose{};
+
+        struct ForwardArrow
+        {
+            float width = 7.5;
+            float length = 100.0;
+            simox::Color color = simox::Color::green();
+
+            viz::Arrow draw(const VertexAttribs& attribs) const;
+            viz::Arrow draw(const std::string& name, const Eigen::Matrix4f& pose) const;
+        };
+        std::optional<ForwardArrow> forwardArrow = ForwardArrow{};
+
+
+        void draw(viz::Layer& layer, Graph::ConstVertex vertex) const;
+        void draw(viz::Layer& layer, const VertexAttribs& attribs) const;
+        void draw(viz::Layer& layer, const std::string& name, const Eigen::Matrix4f& pose) const;
+    };
+
+
+    struct EdgeVisu
+    {
+        struct Arrow
+        {
+            float width = 5.0;
+            simox::Color color = simox::Color::azure(196);
+
+            viz::Arrow draw(Graph::ConstEdge edge) const;
+            viz::Arrow draw(const EdgeAttribs& edge,
+                            const VertexAttribs& source,
+                            const VertexAttribs& target) const;
+        };
+        std::optional<Arrow> arrow = Arrow{};
+
+
+        void draw(viz::Layer& layer, Graph::ConstEdge edge) const;
+    };
+
+
+    struct GraphVisu
+    {
+        std::optional<VertexVisu> vertex = VertexVisu{};
+        std::optional<EdgeVisu> edge = EdgeVisu{};
+
+
+        void draw(viz::Layer& layer, const Graph& graph) const;
+    };
+
+} // namespace armarx::navigation::graph
diff --git a/source/armarx/navigation/graph/aron/Graph.xml b/source/armarx/navigation/graph/aron/Graph.xml
new file mode 100644
index 0000000000000000000000000000000000000000..beda2163d82f62f9e76f3d11e62d903699388ee9
--- /dev/null
+++ b/source/armarx/navigation/graph/aron/Graph.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<AronTypeDefinition>
+    <CodeIncludes>
+    </CodeIncludes>
+    <AronIncludes>
+      <!-- <Include include="<armarx/navigation/location/aron/Location.xml>" autoinclude="true" /> -->
+      <Include include="<RobotAPI/libraries/armem/aron/MemoryID.xml>" autoinclude="true" />
+    </AronIncludes>
+
+    <GenerateTypes>
+
+        <Object name='armarx::navigation::graph::arondto::Vertex'>
+
+            <ObjectChild key='vertexID'>
+                <Long />
+            </ObjectChild>
+
+            <ObjectChild key='locationID'>
+                <armarx::armem::arondto::MemoryID />
+            </ObjectChild>
+
+            <!-- Only stored in Location -->
+            <!--ObjectChild key='globalRobotPose'>
+                <Pose />
+            </ObjectChild-->
+
+        </Object>
+
+
+        <Object name='armarx::navigation::graph::arondto::Edge'>
+
+            <ObjectChild key='sourceVertexID'>
+                <Long />
+            </ObjectChild>
+
+            <ObjectChild key='targetVertexID'>
+                <Long />
+            </ObjectChild>
+
+        </Object>
+
+
+        <Object name='armarx::navigation::graph::arondto::Graph'>
+
+            <ObjectChild key='vertices'>
+                <List>
+                    <armarx::navigation::graph::arondto::Vertex />
+                </List>
+            </ObjectChild>
+
+            <ObjectChild key='edges'>
+                <List>
+                    <armarx::navigation::graph::arondto::Edge />
+                </List>
+            </ObjectChild>
+
+        </Object>
+
+    </GenerateTypes>
+</AronTypeDefinition>
diff --git a/source/armarx/navigation/graph/constants.cpp b/source/armarx/navigation/graph/constants.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a87fc00d8ba55fdcecd1bca47001aeec3df9e4d3
--- /dev/null
+++ b/source/armarx/navigation/graph/constants.cpp
@@ -0,0 +1,11 @@
+#include "constants.h"
+
+#include <RobotAPI/libraries/armem/core/MemoryID.h>
+
+
+namespace armarx::navigation
+{
+
+    const armem::MemoryID graph::coreSegmentID{"Navigation", "Graph"};
+
+}
diff --git a/source/armarx/navigation/graph/constants.h b/source/armarx/navigation/graph/constants.h
new file mode 100644
index 0000000000000000000000000000000000000000..f88b3cdedf62dbdd9518d27d72ad48cb0556e974
--- /dev/null
+++ b/source/armarx/navigation/graph/constants.h
@@ -0,0 +1,33 @@
+/**
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <RobotAPI/libraries/armem/core/forward_declarations.h>
+
+
+namespace armarx::navigation::graph
+{
+
+    // FIXME inline ...
+    extern const armem::MemoryID coreSegmentID;
+
+} // namespace armarx::navigation::graph
diff --git a/source/armarx/navigation/graph/forward_declarations.h b/source/armarx/navigation/graph/forward_declarations.h
new file mode 100644
index 0000000000000000000000000000000000000000..2f30f7839c6a80f9ad318344dc2151102518df6d
--- /dev/null
+++ b/source/armarx/navigation/graph/forward_declarations.h
@@ -0,0 +1,39 @@
+/**
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+
+namespace semrel
+{
+    template <class VertexAttribT, class EdgeAttribT, class GraphAttribT>
+    class RelationGraph;
+}
+namespace armarx::navigation::graph
+{
+
+    struct VertexAttribs;
+    struct EdgeAttribs;
+    struct GraphAttribs;
+
+    using Graph = semrel::RelationGraph<VertexAttribs, EdgeAttribs, GraphAttribs>;
+
+} // namespace armarx::navigation::graph
diff --git a/source/armarx/navigation/gui-plugins/CMakeLists.txt b/source/armarx/navigation/gui-plugins/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..0b821f039461902aa7dddab6d07b19ca82b6123c
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/CMakeLists.txt
@@ -0,0 +1,2 @@
+
+add_subdirectory(LocationGraphEditor)
\ No newline at end of file
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/CMakeLists.txt b/source/armarx/navigation/gui-plugins/LocationGraphEditor/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..0c9d633dccb4e1f929fc4afb99e0e5a11ef9c663
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/CMakeLists.txt
@@ -0,0 +1,85 @@
+set(LIB_NAME "LocationGraphEditorGuiPlugin")
+armarx_set_target("${LIB_NAME}")
+
+# most qt components will be linked against in the call armarx_gui_library
+#armarx_find_qt(QtCore QtGui QtDesigner)
+
+# ArmarXGui gets included through depends_on_armarx_package(ArmarXGui "OPTIONAL")
+# in the toplevel CMakeLists.txt
+armarx_build_if(ArmarXGui_FOUND "ArmarXGui not available")
+
+
+# do not rename this variable, it is used in armarx_gui_library()...
+set(SOURCES
+    LocationGraphEditorWidgetController.cpp
+
+    FunctionalEventFilter.cpp
+    GuiGraph.cpp
+    Visu.cpp
+
+    widgets/default_colors.cpp
+    widgets/utils.cpp
+    widgets/ConnectDialog.cpp
+    widgets/EdgeTableWidget.cpp
+    widgets/NewEntityIdDialog.cpp
+    widgets/RobotVisuWidget.cpp
+    widgets/VertexDataWidget.cpp
+    widgets/VertexTableWidget.cpp
+
+    widgets/graph_scene/ControlWidget.cpp
+    widgets/graph_scene/Scene.cpp
+    widgets/graph_scene/Widget.cpp
+)
+set(HEADERS
+    LocationGraphEditorWidgetController.h
+
+    FunctionalEventFilter.h
+    GuiGraph.h
+    Visu.h
+
+    widgets/default_colors.h
+    widgets/utils.h
+    widgets/ConnectDialog.h
+    widgets/EdgeTableWidget.h
+    widgets/NewEntityIdDialog.h
+    widgets/RobotVisuWidget.h
+    widgets/VertexDataWidget.h
+    widgets/VertexTableWidget.h
+
+    widgets/graph_scene.h
+    widgets/graph_scene/ControlWidget.h
+    widgets/graph_scene/Scene.h
+    widgets/graph_scene/Widget.h
+)
+set(GUI_UIS
+    LocationGraphEditorWidget.ui
+)
+set(GUI_RCS
+    icons.qrc
+)
+
+# Add more libraries you depend on here, e.g. ${QT_LIBRARIES}.
+set(COMPONENT_LIBS
+    # ArmarXGui
+    SimpleConfigDialog
+
+    # RobotAPI
+    ArViz
+    armem
+
+    Navigation::Location
+    Navigation::Graph
+)
+
+
+if(ArmarXGui_FOUND)
+    armarx_gui_plugin("${LIB_NAME}" "${SOURCES}" "" "${GUI_UIS}" "${GUI_RCS}" "${COMPONENT_LIBS}")
+
+    #find_package(MyLib QUIET)
+    #armarx_build_if(MyLib_FOUND "MyLib not available")
+    # all target_include_directories must be guarded by if(Xyz_FOUND)
+    # for multiple libraries write: if(X_FOUND AND Y_FOUND)....
+    #if(MyLib_FOUND)
+    #    target_include_directories(LocationGraphEditorGuiPlugi PUBLIC ${MyLib_INCLUDE_DIRS})
+    #endif()
+endif()
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/FunctionalEventFilter.cpp b/source/armarx/navigation/gui-plugins/LocationGraphEditor/FunctionalEventFilter.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..1ffe763d0c7060d2c8de191daa01c5ca3f1604f7
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/FunctionalEventFilter.cpp
@@ -0,0 +1,18 @@
+#include "FunctionalEventFilter.h"
+
+
+namespace simox::gui
+{
+
+    FunctionalEventFilter::FunctionalEventFilter(Function&& function) :
+        function(function)
+    {
+    }
+
+
+    bool FunctionalEventFilter::eventFilter(QObject* obj, QEvent* event)
+    {
+        return function(obj, event);
+    }
+
+}
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/FunctionalEventFilter.h b/source/armarx/navigation/gui-plugins/LocationGraphEditor/FunctionalEventFilter.h
new file mode 100644
index 0000000000000000000000000000000000000000..0b5fb30b720cdc5965d72622a244d84259a306ba
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/FunctionalEventFilter.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include <QObject>
+
+#include <functional>
+
+class QEvent;
+
+
+namespace simox::gui
+{
+
+    class FunctionalEventFilter : public QObject
+    {
+    public:
+
+        using Function = std::function<bool(QObject* obj, QEvent* event)>;
+
+
+        FunctionalEventFilter(Function&& function);
+
+
+    protected:
+
+        bool eventFilter(QObject* obj, QEvent* event) override;
+
+        Function function;
+
+    };
+
+}
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/GraphScene.cpp b/source/armarx/navigation/gui-plugins/LocationGraphEditor/GraphScene.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c60d7f2eea3c9992a3cfe20b71193897c9f6d75d
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/GraphScene.cpp
@@ -0,0 +1,170 @@
+#include "GraphScene.h"
+
+#include <Eigen/Core>
+
+#include <ArmarXCore/core/exceptions/local/ExpressionException.h>
+
+#include <SemanticObjectRelations/Shapes/Shape.h>
+
+
+namespace armarx::navigation::locgrapheditor
+{
+
+    QGraphicsEllipseItem*
+    GraphScene::addVertex(const graph::Graph::Vertex& vertex)
+    {
+        const Eigen::Matrix4f& pose = vertex.attrib().getPose();
+
+        // To capture by copy
+        semrel::ShapeID vertexID = vertex.objectID();
+
+        GraphVisualizerGraphicsEllipseItem* item = new GraphVisualizerGraphicsEllipseItem{
+            [this, vertexID]() { emit vertexSelected(vertexID); },
+            static_cast<qreal>(pose(0, 3)),
+            static_cast<qreal>(-pose(1, 3)),
+            0,
+            0};
+        addItem(item);
+
+        // setToolTip on graphicsItem does not work
+        item->setZValue(std::numeric_limits<qreal>::max());
+        // dynamic_cast<QGraphicsItem*>(item)->setToolTip(QString::fromStdString("Vertex '" + name + "'"));
+        item->setToolTip(QString::fromStdString("Vertex '" + vertex.attrib().getName() + "'"));
+
+        return item;
+    }
+
+
+    QGraphicsLineItem*
+    GraphScene::addEdge(const graph::Graph::Edge& edge)
+    {
+        semrel::ShapeID sourceID = edge.sourceObjectID();
+        semrel::ShapeID targetID = edge.targetObjectID();
+
+        Eigen::Matrix4d sourcePose = edge.source().attrib().getPose().cast<qreal>();
+        Eigen::Matrix4d targetPose = edge.target().attrib().getPose().cast<qreal>();
+
+        GraphVisualizerGraphicsLineItem* item = new GraphVisualizerGraphicsLineItem{
+            [this, sourceID, targetID]() { emit edgeSelected(sourceID, targetID); },
+            sourcePose(0, 3),
+            -sourcePose(1, 3),
+            targetPose(0, 3),
+            -targetPose(1, 3)};
+        addItem(item);
+
+        // setToolTip on item does not work
+        std::stringstream toolTip;
+        toolTip << "Edge '" << edge.source().attrib().getName() << "' -> '"
+                << edge.target().attrib().getName();
+        item->setToolTip(QString::fromStdString(toolTip.str()));
+
+        return item;
+    }
+
+
+    void
+    GraphScene::updateVertex(GuiGraph::Vertex& vertex)
+    {
+        QGraphicsEllipseItem* item = vertex.attrib().graphicsItem;
+        ARMARX_CHECK_NOT_NULL(item);
+
+        const Eigen::Matrix4d pose = vertex.attrib().getPose().cast<qreal>();
+
+        QColor color = vertex.attrib().highlighted ? colorSelected : colorDefault;
+        double lineWidth = vertex.attrib().highlighted ? lineWidthSelected : lineWidthDefault;
+
+        item->setPen(QPen{color});
+        item->setBrush(QBrush{color});
+        item->setRect(pose(0, 3) - lineWidth * verticesScaleFactor / 2,
+                      -pose(1, 3) - lineWidth * verticesScaleFactor / 2,
+                      lineWidth * verticesScaleFactor,
+                      lineWidth * verticesScaleFactor);
+    }
+
+
+    void
+    GraphScene::updateEdge(GuiGraph::Edge& edge)
+    {
+        QGraphicsLineItem* item = edge.attrib().graphicsItem;
+        ARMARX_CHECK_NOT_NULL(item);
+
+        Eigen::Matrix4d sourcePose = edge.source().attrib().getPose().cast<qreal>();
+        Eigen::Matrix4d targetPose = edge.target().attrib().getPose().cast<qreal>();
+
+        QColor color = edge.attrib().highlighted ? colorSelected : colorDefault;
+        double lineWidth = edge.attrib().highlighted ? lineWidthSelected : lineWidthDefault;
+
+        QPen pen{color};
+        pen.setWidthF(lineWidth * lineScaleFactor);
+
+        item->setPen(pen);
+        item->setLine(sourcePose(0, 3), -sourcePose(1, 3), targetPose(0, 3), -targetPose(1, 3));
+    }
+
+
+    void
+    GraphScene::removeVertex(QGraphicsEllipseItem*& item)
+    {
+        removeItem(item);
+        delete item;
+        item = nullptr;
+    }
+
+
+    void
+    GraphScene::removeEdge(QGraphicsLineItem*& item)
+    {
+        removeItem(item);
+        delete item;
+        item = nullptr;
+    }
+
+
+    GraphVisualizerGraphicsEllipseItem::GraphVisualizerGraphicsEllipseItem(
+        std::function<void(void)> onDoubleClicked,
+        qreal x,
+        qreal y,
+        qreal width,
+        qreal height,
+        QGraphicsItem* parent) :
+        QGraphicsEllipseItem{x, y, width, height, parent}, onDoubleClicked{onDoubleClicked}
+    {
+    }
+
+
+    GraphVisualizerGraphicsEllipseItem::~GraphVisualizerGraphicsEllipseItem()
+    {
+    }
+
+
+    void
+    GraphVisualizerGraphicsEllipseItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent*)
+    {
+        onDoubleClicked();
+    }
+
+
+    GraphVisualizerGraphicsLineItem::GraphVisualizerGraphicsLineItem(
+        std::function<void(void)> onDoubleClicked,
+        qreal x1,
+        qreal y1,
+        qreal x2,
+        qreal y2,
+        QGraphicsItem* parent) :
+        QGraphicsLineItem{x1, y1, x2, y2, parent}, onDoubleClicked{onDoubleClicked}
+    {
+    }
+
+
+    GraphVisualizerGraphicsLineItem::~GraphVisualizerGraphicsLineItem()
+    {
+    }
+
+
+    void
+    GraphVisualizerGraphicsLineItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent*)
+    {
+        onDoubleClicked();
+    }
+
+} // namespace armarx::navigation::locgrapheditor
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/GraphScene.h b/source/armarx/navigation/gui-plugins/LocationGraphEditor/GraphScene.h
new file mode 100644
index 0000000000000000000000000000000000000000..df113b0fc859f065bed176f7287a1810a75ea844
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/GraphScene.h
@@ -0,0 +1,108 @@
+#pragma once
+
+#include <QGraphicsEllipseItem>
+#include <QGraphicsLineItem>
+#include <QGraphicsScene>
+#include <functional>
+
+#include "GuiGraph.h"
+#include <armarx/navigation/graph/Graph.h>
+
+
+namespace semrel
+{
+    struct ShapeID;
+}
+namespace armarx::navigation::locgrapheditor
+{
+
+    class GraphScene : public QGraphicsScene
+    {
+        Q_OBJECT
+
+    public:
+        using QGraphicsScene::QGraphicsScene;
+
+        QGraphicsEllipseItem* addVertex(const graph::Graph::Vertex& vertex);
+        QGraphicsLineItem* addEdge(const graph::Graph::Edge& Edge);
+
+        void updateVertex(GuiGraph::Vertex& vertex);
+        void updateEdge(GuiGraph::Edge& edge);
+
+        void removeVertex(QGraphicsEllipseItem*& item);
+        void removeEdge(QGraphicsLineItem*& item);
+
+
+    public slots:
+
+
+    signals:
+
+        void vertexSelected(const semrel::ShapeID& vertexID);
+        void edgeSelected(const semrel::ShapeID& sourceID, const semrel::ShapeID& targetID);
+
+
+    public:
+        double lineWidthDefault = 5;
+        double lineWidthSelected = 10;
+
+        double sceneScaleFactor = 2;
+        double verticesScaleFactor = 3.0 * sceneScaleFactor;
+        double lineScaleFactor = 1.0 * sceneScaleFactor;
+
+        QColor colorDefault = QColor::fromRgb(128, 128, 255);
+        QColor colorSelected = QColor::fromRgb(255, 128, 0);
+    };
+
+
+    /**
+     * @brief Required to override the double click event.
+     * This is required to toggle the select state.
+     */
+    class GraphVisualizerGraphicsEllipseItem : public QGraphicsEllipseItem
+    {
+    public:
+        GraphVisualizerGraphicsEllipseItem(std::function<void(void)> onDoubleClicked,
+                                           qreal x,
+                                           qreal y,
+                                           qreal width,
+                                           qreal height,
+                                           QGraphicsItem* parent = nullptr);
+
+        ~GraphVisualizerGraphicsEllipseItem() override;
+
+
+    protected:
+        void mouseDoubleClickEvent(QGraphicsSceneMouseEvent*) override;
+
+
+        std::function<void(void)> onDoubleClicked;
+    };
+
+
+    /**
+     * @brief Required to override the double click event.
+     * This is required to toggle the select state.
+     */
+    class GraphVisualizerGraphicsLineItem : public QGraphicsLineItem
+    {
+    public:
+        GraphVisualizerGraphicsLineItem(std::function<void(void)> onDoubleClicked,
+                                        qreal x1,
+                                        qreal y1,
+                                        qreal x2,
+                                        qreal y2,
+                                        QGraphicsItem* parent = nullptr);
+
+        ~GraphVisualizerGraphicsLineItem() override;
+
+
+    protected:
+        void mouseDoubleClickEvent(QGraphicsSceneMouseEvent*) override;
+
+
+        std::function<void(void)> onDoubleClicked;
+    };
+
+
+} // namespace armarx::navigation::locgrapheditor
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/GuiGraph.cpp b/source/armarx/navigation/gui-plugins/LocationGraphEditor/GuiGraph.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..034b0609246071b088cf973bfd9303bbb5335f4d
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/GuiGraph.cpp
@@ -0,0 +1,146 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package    MemoryX::ArmarXObjects::GraphImportExport
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#include "GuiGraph.h"
+
+#include <SimoxUtility/math/convert/mat4f_to_rpy.h>
+#include <SimoxUtility/math/convert/rad_to_deg.h>
+
+
+namespace armarx::navigation::locgrapheditor
+{
+
+    bool
+    GuiGraph::hasChanged() const
+    {
+        if (attrib().edgesChanged)
+        {
+            return true;
+        }
+        for (auto vertex : vertices())
+        {
+            if (vertex.attrib().changed)
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    std::optional<GuiGraph::Vertex>
+    GuiGraph::getVertexFromTableItem(QTableWidgetItem* item)
+    {
+        for (auto vertex : vertices())
+        {
+            if (vertex.attrib().tableWidgetItem == item)
+            {
+                return vertex;
+            }
+        }
+        return std::nullopt;
+    }
+
+
+    std::map<QTableWidgetItem*, GuiGraph::Vertex>
+    GuiGraph::getTableItemToVertexMap()
+    {
+        std::map<QTableWidgetItem*, GuiGraph::Vertex> map;
+
+        for (auto vertex : vertices())
+        {
+            if (vertex.attrib().tableWidgetItem != nullptr)
+            {
+                map[vertex.attrib().tableWidgetItem] = vertex;
+            }
+        }
+
+        return map;
+    }
+
+
+    std::map<QTableWidgetItem*, GuiGraph::Edge>
+    GuiGraph::getTableItemToEdgeMap()
+    {
+        std::map<QTableWidgetItem*, GuiGraph::Edge> map;
+
+        for (auto edge : edges())
+        {
+            if (edge.attrib().tableWidgetItem != nullptr)
+            {
+                map[edge.attrib().tableWidgetItem] = edge;
+            }
+        }
+
+        return map;
+    }
+
+} // namespace armarx::navigation::locgrapheditor
+
+namespace armarx::nav
+{
+
+    float
+    locgrapheditor::getYawAngleDegree(const Eigen::Matrix4f& pose)
+    {
+        return simox::math::rad_to_deg(simox::math::mat4f_to_rpy(pose)(2));
+    }
+
+
+    double
+    locgrapheditor::getYawAngleDegree(const Eigen::Matrix4d& pose)
+    {
+        return simox::math::rad_to_deg(simox::math::mat4f_to_rpy(pose)(2));
+    }
+
+
+    auto
+    locgrapheditor::toGuiGraph(const graph::Graph& nav) -> GuiGraph
+    {
+        GuiGraph gui;
+        for (auto v : nav.vertices())
+        {
+            gui.addVertex(v.objectID(), {v.attrib()});
+        }
+        for (auto e : nav.edges())
+        {
+            gui.addEdge(e.sourceObjectID(), e.targetObjectID(), {e.attrib()});
+        }
+        return gui;
+    }
+
+
+    navigation::graph::Graph
+    locgrapheditor::fromGuiGraph(const GuiGraph& gui)
+    {
+        graph::Graph nav;
+        for (auto v : gui.vertices())
+        {
+            nav.addVertex(v.objectID(), {v.attrib()});
+        }
+        for (auto e : gui.edges())
+        {
+            nav.addEdge(e.sourceObjectID(), e.targetObjectID(), {e.attrib()});
+        }
+        return nav;
+    }
+
+} // namespace armarx::nav
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/GuiGraph.h b/source/armarx/navigation/gui-plugins/LocationGraphEditor/GuiGraph.h
new file mode 100644
index 0000000000000000000000000000000000000000..9355d9621349e5d34d27f94406477e94ecb3f94a
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/GuiGraph.h
@@ -0,0 +1,105 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <map>
+#include <optional>
+
+#include <SemanticObjectRelations/RelationGraph/RelationGraph.h>
+#include <armarx/navigation/graph/Graph.h>
+
+
+class QGraphicsEllipseItem;
+class QGraphicsLineItem;
+class QTableWidgetItem;
+
+
+namespace armarx::navigation::locgrapheditor
+{
+
+    /**
+     * @brief The NodeData struct holds data required for the node.
+     * The name is stored in the key used in the map vertices.
+     */
+    struct VertexData : public navigation::graph::VertexAttribs
+    {
+        /// The ellipse in the scene.
+        QGraphicsEllipseItem* graphicsItem = nullptr;
+
+        /// The item in the table tableWidgetVertices.
+        QTableWidgetItem* tableWidgetItem = nullptr;
+
+        /// Whether the node is highlighted.
+        bool highlighted = false;
+
+        /// Whether the vertex was changed since loading or committing.
+        bool changed = false;
+    };
+
+
+    /**
+     * @brief The EdgeData struct holds data required for the edge.
+     * The name is stored in the key used in the map edges.
+     */
+    struct EdgeData : public navigation::graph::EdgeAttribs
+    {
+        /// The line in the scene.
+        QGraphicsLineItem* graphicsItem = nullptr;
+
+        /// The item in the table tableWidgetEdges.
+        QTableWidgetItem* tableWidgetItem = nullptr;
+
+        /// Whether the edge is highlighted.
+        bool highlighted = false;
+    };
+
+
+    struct GraphData : public navigation::graph::GraphAttribs
+    {
+        /// Whether the graph structure was changed since loading or committing.
+        bool edgesChanged = false;
+    };
+
+
+    class GuiGraph : public semrel::RelationGraph<VertexData, EdgeData, GraphData>
+    {
+    public:
+        using RelationGraph::RelationGraph;
+
+
+        bool hasChanged() const;
+
+        std::optional<Vertex> getVertexFromTableItem(QTableWidgetItem* item);
+
+        std::map<QTableWidgetItem*, Vertex> getTableItemToVertexMap();
+        std::map<QTableWidgetItem*, Edge> getTableItemToEdgeMap();
+    };
+
+
+    GuiGraph toGuiGraph(const graph::Graph& graph);
+    graph::Graph fromGuiGraph(const GuiGraph& graph);
+
+
+    float getYawAngleDegree(const Eigen::Matrix4f& pose);
+    double getYawAngleDegree(const Eigen::Matrix4d& pose);
+
+} // namespace armarx::navigation::locgrapheditor
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/LocationGraphEditorWidget.ui b/source/armarx/navigation/gui-plugins/LocationGraphEditor/LocationGraphEditorWidget.ui
new file mode 100644
index 0000000000000000000000000000000000000000..db72fa0c83009a8fa6c55cd40cf5e2ef39ae552c
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/LocationGraphEditorWidget.ui
@@ -0,0 +1,252 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>LocationGraphEditorWidget</class>
+ <widget class="QWidget" name="LocationGraphEditorWidget">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>783</width>
+    <height>684</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>LocationGraphEditorWidget</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout_2">
+   <item>
+    <widget class="QGroupBox" name="graphGroupBox">
+     <property name="title">
+      <string>Navigation Graphs from Navigation Graph Segment</string>
+     </property>
+     <layout class="QHBoxLayout" name="horizontalLayout_4">
+      <item>
+       <widget class="QPushButton" name="createGraphButton">
+        <property name="toolTip">
+         <string>Reloads the list of scenes from the memory</string>
+        </property>
+        <property name="text">
+         <string>Create New Graph</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QPushButton" name="queryGraphsButton">
+        <property name="toolTip">
+         <string>Reloads the list of scenes from the memory</string>
+        </property>
+        <property name="text">
+         <string>Query</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QComboBox" name="graphsComboBox">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QPushButton" name="loadGraphButton">
+        <property name="toolTip">
+         <string>Draws the selected scene</string>
+        </property>
+        <property name="text">
+         <string>Load</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QPushButton" name="commitGraphButton">
+        <property name="toolTip">
+         <string>Draws the selected scene</string>
+        </property>
+        <property name="text">
+         <string>Commit</string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QSplitter" name="verticalSplitter">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <widget class="QWidget" name="tables" native="true">
+      <layout class="QVBoxLayout" name="verticalLayout">
+       <property name="leftMargin">
+        <number>0</number>
+       </property>
+       <property name="topMargin">
+        <number>0</number>
+       </property>
+       <property name="rightMargin">
+        <number>0</number>
+       </property>
+       <property name="bottomMargin">
+        <number>0</number>
+       </property>
+       <item>
+        <widget class="QSplitter" name="horizontalSplitter">
+         <property name="orientation">
+          <enum>Qt::Horizontal</enum>
+         </property>
+         <widget class="QWidget" name="verticalLayout_12Widget">
+          <layout class="QVBoxLayout" name="verticalLayout_12">
+           <item>
+            <widget class="QGroupBox" name="locationDataGroupBox">
+             <property name="title">
+              <string>Location Data</string>
+             </property>
+             <layout class="QVBoxLayout" name="verticalLayout_3">
+              <property name="leftMargin">
+               <number>3</number>
+              </property>
+              <property name="topMargin">
+               <number>3</number>
+              </property>
+              <property name="rightMargin">
+               <number>3</number>
+              </property>
+              <property name="bottomMargin">
+               <number>3</number>
+              </property>
+             </layout>
+            </widget>
+           </item>
+           <item>
+            <widget class="QGroupBox" name="robotVisuGroupBox">
+             <property name="title">
+              <string>Robot State</string>
+             </property>
+             <layout class="QVBoxLayout" name="verticalLayout_5">
+              <property name="leftMargin">
+               <number>0</number>
+              </property>
+              <property name="topMargin">
+               <number>0</number>
+              </property>
+              <property name="rightMargin">
+               <number>0</number>
+              </property>
+              <property name="bottomMargin">
+               <number>0</number>
+              </property>
+             </layout>
+            </widget>
+           </item>
+          </layout>
+         </widget>
+         <widget class="QWidget" name="verticalLayout_13Widget">
+          <layout class="QVBoxLayout" name="verticalLayout_13">
+           <item>
+            <widget class="QGroupBox" name="locationsTableGroupBox">
+             <property name="title">
+              <string>Locations (right click for options)</string>
+             </property>
+             <layout class="QVBoxLayout" name="verticalLayout_7">
+              <property name="leftMargin">
+               <number>3</number>
+              </property>
+              <property name="topMargin">
+               <number>3</number>
+              </property>
+              <property name="rightMargin">
+               <number>3</number>
+              </property>
+              <property name="bottomMargin">
+               <number>3</number>
+              </property>
+             </layout>
+            </widget>
+           </item>
+          </layout>
+         </widget>
+         <widget class="QWidget" name="verticalLayout_6Widget">
+          <layout class="QVBoxLayout" name="verticalLayout_6">
+           <item>
+            <widget class="QGroupBox" name="edgesTableGroupBox">
+             <property name="title">
+              <string>Graph Edges (right click for options)</string>
+             </property>
+             <layout class="QVBoxLayout" name="verticalLayout_8">
+              <property name="leftMargin">
+               <number>3</number>
+              </property>
+              <property name="topMargin">
+               <number>3</number>
+              </property>
+              <property name="rightMargin">
+               <number>3</number>
+              </property>
+              <property name="bottomMargin">
+               <number>3</number>
+              </property>
+             </layout>
+            </widget>
+           </item>
+          </layout>
+         </widget>
+         <widget class="QSplitter" name="horizontalSplitter_2">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+         </widget>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QFrame" name="graphFrame">
+      <property name="frameShape">
+       <enum>QFrame::NoFrame</enum>
+      </property>
+      <layout class="QVBoxLayout" name="verticalLayout_4">
+       <property name="spacing">
+        <number>0</number>
+       </property>
+       <property name="leftMargin">
+        <number>0</number>
+       </property>
+       <property name="topMargin">
+        <number>0</number>
+       </property>
+       <property name="rightMargin">
+        <number>0</number>
+       </property>
+       <property name="bottomMargin">
+        <number>0</number>
+       </property>
+       <item>
+        <layout class="QVBoxLayout" name="graphSceneLayout"/>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLabel" name="statusLabel">
+     <property name="font">
+      <font>
+       <pointsize>9</pointsize>
+       <italic>true</italic>
+      </font>
+     </property>
+     <property name="text">
+      <string/>
+     </property>
+     <property name="alignment">
+      <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/LocationGraphEditorWidgetController.cpp b/source/armarx/navigation/gui-plugins/LocationGraphEditor/LocationGraphEditorWidgetController.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..0a050d5667436ad7ed073436a7edba7d78e67ae1
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/LocationGraphEditorWidgetController.cpp
@@ -0,0 +1,1128 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http:// www.gnu.org/licenses/>.
+ *
+ * \package    Navigation::gui-plugins::LocationGraphEditorWidgetController
+ * \author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * \date       2021
+ * \copyright  http:// www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#include "LocationGraphEditorWidgetController.h"
+
+#include <VirtualRobot/VirtualRobot.h>
+
+#include <ArmarXCore/core/exceptions/local/ExpressionException.h>
+#include <ArmarXCore/core/logging/Logging.h>
+
+#include <RobotAPI/components/ArViz/Client/Client.h>
+#include <RobotAPI/libraries/armem/client/MemoryNameSystem.h>
+#include <RobotAPI/libraries/armem/core/aron_conversions.h>
+#include <RobotAPI/libraries/aron/common/aron_conversions/core.h>
+
+#include "widgets/EdgeTableWidget.h"
+#include "widgets/NewEntityIdDialog.h"
+#include "widgets/RobotVisuWidget.h"
+#include "widgets/VertexDataWidget.h"
+#include "widgets/VertexTableWidget.h"
+#include "widgets/graph_scene/Scene.h"
+#include "widgets/graph_scene/Widget.h"
+#include "widgets/utils.h"
+#include <Navigation/gui-plugins/LocationGraphEditor/ui_LocationGraphEditorWidget.h>
+#include <armarx/navigation/graph/aron/Graph.aron.generated.h>
+#include <armarx/navigation/graph/constants.h>
+#include <armarx/navigation/location/aron/Location.aron.generated.h>
+#include <armarx/navigation/location/constants.h>
+
+// Qt headers
+#include <QDialog>
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QLineEdit>
+#include <QMenu>
+#include <QMessageBox>
+#include <QMouseEvent>
+#include <QObject>
+#include <QPushButton>
+#include <QSignalBlocker>
+
+// std
+#include <sstream>
+
+
+static const QString SETTING_LAST_SCENE = "lastScene";
+
+
+namespace armarx::navigation::locgrapheditor
+{
+
+    QString
+    LocationGraphEditorWidgetController::GetWidgetName()
+    {
+        return "Navigation.LocationGraphEditor";
+    }
+    QIcon
+    LocationGraphEditorWidgetController::GetWidgetIcon()
+    {
+        return QIcon{"://icons/location_graph_editor.svg"};
+    }
+
+
+    LocationGraphEditorWidgetController::LocationGraphEditorWidgetController() :
+        settings{"KIT", "LocationGraphEditorWidgetController"}
+    {
+        widget.setupUi(getWidget());
+
+        loadAutomaticSettings();
+
+        widget.loadGraphButton->setEnabled(false);
+
+        view.vertexData = new VertexDataWidget();
+        view.vertexData->setEnabled(false); // Enable on first selection of vertex.
+        widget.locationDataGroupBox->layout()->addWidget(view.vertexData);
+        if (QBoxLayout* layout = dynamic_cast<QBoxLayout*>(widget.locationDataGroupBox->layout()))
+        {
+            layout->addStretch();
+        }
+
+        view.vertexTable = new VertexTableWidget();
+        widget.locationsTableGroupBox->layout()->addWidget(view.vertexTable);
+
+        view.edgeTable = new EdgeTableWidget();
+        widget.edgesTableGroupBox->layout()->addWidget(view.edgeTable);
+
+        view.robotVisu = new RobotVisuWidget(*this);
+        widget.robotVisuGroupBox->layout()->addWidget(view.robotVisu);
+
+        view.graph = new GraphSceneWidget();
+        widget.graphSceneLayout->addWidget(view.graph);
+
+
+        connect(this, &This::connected, this, &This::queryGraphs);
+        connect(this, &This::connected, this, &This::graphChanged);
+
+        connect(widget.createGraphButton, &QPushButton::pressed, this, &This::createGraphDialog);
+
+        connect(widget.queryGraphsButton, &QPushButton::pressed, this, &This::queryGraphs);
+
+        connect(this, &This::locationMemoryChanged, this, &This::memoryChanged);
+        connect(this, &This::graphMemoryChanged, this, &This::memoryChanged);
+
+        connect(this, &This::graphMemoryChanged, this, &This::updateGraphList);
+
+        connect(widget.loadGraphButton, &QPushButton::pressed, this, &This::loadGraph);
+        connect(widget.commitGraphButton, &QPushButton::pressed, this, &This::commit);
+
+        // View updates
+        connect(this, &This::graphChanged, this, &This::updateGraphView);
+        connect(
+            view.vertexData, &VertexDataWidget::vertexDataChanged, this, &This::updateGraphView);
+        connect(view.robotVisu, &RobotVisuWidget::connected, this, &This::updateGraphView);
+        connect(view.robotVisu,
+                &RobotVisuWidget::connected,
+                this,
+                [this]() { view.vertexData->setRobotConnection(&view.robotVisu->connection()); });
+        connect(view.robotVisu, &RobotVisuWidget::settingsChanged, this, &This::updateGraphView);
+
+        // Selection
+        connect(view.vertexTable,
+                &VertexTableWidget::currentItemChanged,
+                [this](QTableWidgetItem* current, QTableWidgetItem* previous)
+                {
+                    (void)previous;
+                    this->selectVertex(current);
+                });
+
+        connect(view.vertexTable,
+                &VertexTableWidget::itemSelectionChanged,
+                this,
+                &This::updateVertexHighlighting);
+        connect(view.edgeTable,
+                &EdgeTableWidget::itemSelectionChanged,
+                this,
+                &This::updateEdgeHighlighting);
+
+        // Graph modification
+        connect(view.vertexTable,
+                &VertexTableWidget::newVertexRequested,
+                this,
+                &This::createVertexDialog);
+        connect(view.vertexTable, &VertexTableWidget::newEdgesRequested, this, &This::addEdges);
+        connect(view.vertexTable,
+                &VertexTableWidget::edgeRemovalRequested,
+                this,
+                &This::removeEdgesOfVertex);
+
+        connect(view.edgeTable, &EdgeTableWidget::edgeRemovalRequested, this, &This::removeEdges);
+    }
+
+
+    LocationGraphEditorWidgetController::~LocationGraphEditorWidgetController()
+    {
+        saveAutomaticSettings();
+    }
+
+
+    QPointer<QDialog>
+    LocationGraphEditorWidgetController::getConfigDialog(QWidget* parent)
+    {
+        if (not configDialog)
+        {
+            configDialog = new SimpleConfigDialog(parent);
+            configDialog->addProxyFinder<armem::mns::MemoryNameSystemInterfacePrx>(
+                "MemoryNameSystem", "Memory Name System", remote.memoryNameSystemName);
+        }
+        return qobject_cast<SimpleConfigDialog*>(configDialog);
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::configured()
+    {
+        remote.memoryNameSystemName = configDialog->getProxyName("MemoryNameSystem");
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::loadSettings(QSettings* settings)
+    {
+        remote.memoryNameSystemName =
+            settings
+                ->value("memoryNameSystemName", QString::fromStdString(remote.memoryNameSystemName))
+                .toString()
+                .toStdString();
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::saveSettings(QSettings* settings)
+    {
+        settings->setValue("memoryNameSystemName",
+                           QString::fromStdString(remote.memoryNameSystemName));
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::loadAutomaticSettings()
+    {
+        lastSelectedSceneName =
+            settings.value(SETTING_LAST_SCENE, lastSelectedSceneName).toString();
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::saveAutomaticSettings()
+    {
+        settings.setValue(SETTING_LAST_SCENE, lastSelectedSceneName);
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::onInitComponent()
+    {
+        usingProxy(remote.memoryNameSystemName);
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::onConnectComponent()
+    {
+        remote.connect(*this);
+        {
+            std::stringstream ss;
+            ss << "Navigation Graphs (Entities from Core Segment "
+               << navigation::graph::coreSegmentID << ")";
+            widget.graphGroupBox->setTitle(QString::fromStdString(ss.str()));
+            widget.graphGroupBox->setEnabled(true);
+        }
+
+        emit connected();
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::Remote::connect(Component& parent)
+    {
+        auto mnsProxy =
+            parent.getProxy<armem::mns::MemoryNameSystemInterfacePrx>(memoryNameSystemName);
+        memoryNameSystem = armem::client::MemoryNameSystem(mnsProxy, &parent);
+
+        locationReader = memoryNameSystem.useReader(navigation::location::coreSegmentID);
+        locationWriter = memoryNameSystem.useWriter(navigation::location::coreSegmentID);
+
+        graphReader = memoryNameSystem.useReader(navigation::graph::coreSegmentID);
+        graphWriter = memoryNameSystem.useWriter(navigation::graph::coreSegmentID);
+
+        arviz = std::make_unique<viz::Client>(viz::Client::createForGuiPlugin(parent));
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::queryMemory()
+    {
+        armem::client::QueryResult locResult = queryLocations();
+        armem::client::QueryResult graphResult = queryGraphs();
+
+        if (not(locResult.success and graphResult.success))
+        {
+            QStringList status;
+            std::stringstream ss;
+            if (not locResult.success)
+            {
+                status.append(QString::fromStdString(locResult.errorMessage));
+            }
+            if (not graphResult.success)
+            {
+                status.append(QString::fromStdString(graphResult.errorMessage));
+            }
+            widget.statusLabel->setText(status.join("\n"));
+        }
+    }
+
+
+    armem::client::QueryResult
+    LocationGraphEditorWidgetController::queryLocations()
+    {
+        armem::client::QueryResult result =
+            remote.locationReader.getLatestSnapshotsIn(navigation::location::coreSegmentID);
+        if (result.success)
+        {
+            model.locationsMemory = std::move(result.memory);
+            emit locationMemoryChanged();
+        }
+        return result;
+    }
+
+
+    armem::client::QueryResult
+    LocationGraphEditorWidgetController::queryGraphs()
+    {
+        armem::client::QueryResult result =
+            remote.graphReader.getLatestSnapshotsIn(navigation::graph::coreSegmentID);
+        if (result.success)
+        {
+            model.graphMemory = std::move(result.memory);
+            emit graphMemoryChanged();
+        }
+        return result;
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::updateGraphList()
+    {
+        QString previousText = widget.graphsComboBox->currentText();
+        widget.graphsComboBox->clear();
+
+        int i = 0;
+        int previousIndex = -1; // To keep selection.
+        model.graphMemory.forEachEntity(
+            [&](const armem::wm::Entity& entity)
+            {
+                bool hasChanged =
+                    (entity.id() == model.graphEntityID ? model.graph.hasChanged() : false);
+                QString text = getGraphDisplayName(entity.id(), hasChanged);
+                QString id = QString::fromStdString(entity.id().str());
+
+                widget.graphsComboBox->addItem(text, id);
+                if (previousIndex < 0 and text == previousText)
+                {
+                    previousIndex = i;
+                }
+                ++i;
+            });
+        if (previousIndex >= 0)
+        {
+            widget.graphsComboBox->setCurrentIndex(previousIndex);
+        }
+        widget.loadGraphButton->setEnabled(widget.graphsComboBox->count() > 0);
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::loadGraph()
+    {
+        if (model.graph.hasChanged())
+        {
+            if (not loadGraphDialog())
+            {
+                return;
+            }
+        }
+
+        const armem::MemoryID entityID = armem::MemoryID::fromString(
+            widget.graphsComboBox->currentData().toString().toStdString());
+
+        // Refresh local memory.
+        queryMemory();
+
+        const armem::wm::EntityInstance* instance = model.graphMemory.findLatestInstance(entityID);
+        if (instance)
+        {
+            widget.statusLabel->setText(QString::fromStdString(
+                "Loaded snapshot " + instance->id().getEntitySnapshotID().str()));
+        }
+        else
+        {
+            std::stringstream ss;
+            ss << "No latest instance of entity " << entityID << " in memory.";
+            widget.statusLabel->setText(QString::fromStdString(ss.str()));
+        }
+
+        navigation::graph::Graph nav;
+        {
+            navigation::graph::arondto::Graph dto;
+            ARMARX_CHECK_NOT_NULL(instance->data());
+            dto.fromAron(instance->data());
+            fromAron(dto, nav);
+        }
+        // Resolve locations and remove vertices which could not be resolved.
+        {
+            resolveLocations(nav, model.locationsMemory);
+            std::vector<semrel::ShapeID> remove;
+            for (navigation::graph::Graph::Vertex vertex : nav.vertices())
+            {
+                if (not vertex.attrib().hasPose())
+                {
+                    remove.push_back(vertex.objectID());
+                }
+            }
+            if (not remove.empty())
+            {
+                for (semrel::ShapeID vertexID : remove)
+                {
+                    nav.removeVertex(nav.vertex(vertexID));
+                }
+                std::stringstream ss;
+                ss << "Dropped " << remove.size() << " locations which could not be resolved.";
+                widget.statusLabel->setText(QString::fromStdString(ss.str()));
+            }
+        }
+
+        ARMARX_VERBOSE << "Loading graph " << nav.str();
+        setGraph(entityID, nav);
+    }
+
+
+    bool
+    LocationGraphEditorWidgetController::loadGraphDialog()
+    {
+        ARMARX_CHECK(model.graph.hasChanged());
+
+        QMessageBox msgBox;
+        msgBox.setText("The current graph and/or locations have uncommitted changes. ");
+        msgBox.setInformativeText("Do you want to discard them?");
+        QStringList detailLines;
+        if (model.graph.attrib().edgesChanged)
+        {
+            detailLines.append("Graph edges have changed.");
+        }
+        for (auto vertex : model.graph.vertices())
+        {
+            if (vertex.attrib().changed)
+            {
+                detailLines.append("Location " + QString::fromStdString(vertex.attrib().getName()) +
+                                   " has changed.");
+            }
+        }
+        msgBox.setDetailedText(detailLines.join("\n"));
+        msgBox.setStandardButtons(QMessageBox::Discard | QMessageBox::Cancel);
+        msgBox.setDefaultButton(QMessageBox::Cancel);
+
+        switch (msgBox.exec())
+        {
+            case QMessageBox::Discard:
+                return true; // Ok go.
+            case QMessageBox::Cancel:
+                return false; // Abort loading.
+            default:
+                return false; // Something went wrong.
+        }
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::commit()
+    {
+        armem::CommitResult locResults = commitLocations();
+        armem::EntityUpdateResult graphResult = commitGraph();
+
+        {
+            std::stringstream ss;
+            if (locResults.allSuccess())
+            {
+                ss << "Committed " << locResults.results.size() << " location snapshot";
+                if (locResults.results.size() != 1)
+                {
+                    ss << "s"; // plural
+                }
+                if (graphResult.success)
+                {
+                    ss << " and 1 graph snapshot " << graphResult.snapshotID;
+                }
+                else
+                {
+                    ss << " but failed to commit graph: \n" << graphResult.errorMessage;
+                }
+            }
+            else
+            {
+                int numLocs = static_cast<int>(locResults.results.size());
+                int numSuccess = 0;
+                for (const auto& r : locResults.results)
+                {
+                    numSuccess += int(r.success);
+                }
+                int numFailed = numLocs - numSuccess;
+
+                if (graphResult.success)
+                {
+                    ss << "Committed 1 graph snapshot " << graphResult.snapshotID;
+                    ss << " and " << numSuccess << " locations, but failed to commit " << numFailed
+                       << " locations: \n"
+                       << locResults.allErrorMessages();
+                }
+                else
+                {
+                    ss << "Failed to commit graph and " << numFailed << " of " << numLocs
+                       << " locations: \n";
+                    ss << graphResult.errorMessage << "\n";
+                    ss << locResults.allErrorMessages();
+                }
+            }
+
+            widget.statusLabel->setText(QString::fromStdString(ss.str()));
+        }
+
+        // `changed` flags may have changed
+        emit graphChanged();
+    }
+
+
+    armem::CommitResult
+    LocationGraphEditorWidgetController::commitLocations()
+    {
+        armem::Commit commit;
+
+        for (auto vertex : model.graph.vertices())
+        {
+            if (vertex.attrib().changed)
+            {
+                armem::EntityUpdate& update = commit.add();
+                fromAron(vertex.attrib().aron.locationID, update.entityID);
+                update.timeCreated = armem::Time::now();
+
+                navigation::location::arondto::Location dto;
+                dto.globalRobotPose = vertex.attrib().getPose();
+                update.instancesData = {dto.toAron()};
+            }
+        }
+
+        armem::CommitResult result = remote.locationWriter.commit(commit);
+        auto it = result.results.begin();
+        for (auto vertex : model.graph.vertices())
+        {
+            if (vertex.attrib().changed)
+            {
+                ARMARX_CHECK(it != result.results.end());
+                if (it->success)
+                {
+                    // Only clear dirty flag when update was successful.
+                    vertex.attrib().changed = false;
+                }
+                ++it;
+            }
+        }
+        return result;
+    }
+
+
+    armem::EntityUpdateResult
+    LocationGraphEditorWidgetController::commitGraph()
+    {
+        navigation::graph::arondto::Graph dto;
+        {
+            navigation::graph::Graph nav = fromGuiGraph(model.graph);
+            toAron(dto, nav);
+        }
+
+        armem::EntityUpdate update;
+        update.entityID = model.graphEntityID;
+        update.timeCreated = armem::Time::now();
+        update.instancesData = {dto.toAron()};
+
+        armem::EntityUpdateResult result = remote.graphWriter.commit(update);
+        if (result.success)
+        {
+            // Clear dirty flag
+            model.graph.attrib().edgesChanged = false;
+        }
+        return result;
+    }
+
+
+    template <class GraphT>
+    semrel::ShapeID
+    findNextFreeVertexID(const GraphT& graph, semrel::ShapeID vertexID)
+    {
+        while (graph.hasVertex(vertexID))
+        {
+            ++vertexID;
+        }
+        return vertexID;
+    }
+
+    void
+    LocationGraphEditorWidgetController::setGraph(const armem::MemoryID& entityID,
+                                                  const graph::Graph& nav)
+    {
+        model.graphEntityID = entityID;
+
+        // Build the gui graph (model).
+        {
+            QSignalBlocker blocker(this);
+
+            clearGraph();
+            model.graph.attrib().edgesChanged = false;
+
+            std::set<armem::MemoryID> coveredLocationIDs;
+            for (auto vertex : nav.vertices())
+            {
+                ARMARX_CHECK(vertex.attrib().hasPose());
+                addVertex(vertex.objectID(), {vertex.attrib()});
+
+                coveredLocationIDs.insert(
+                    aron::fromAron<armem::MemoryID>(vertex.attrib().aron.locationID));
+            }
+            // Add locations which have not been part of graph.
+            // ToDo: This should be an explicit step in the GUI.
+            {
+                semrel::ShapeID vertexID{0};
+                model.locationsMemory.forEachInstance(
+                    [&](const armem::wm::EntityInstance& instance)
+                    {
+                        if (coveredLocationIDs.count(instance.id().getEntityID()) == 0)
+                        {
+                            vertexID = findNextFreeVertexID(model.graph, vertexID);
+                            addVertex(vertexID, instance);
+                        }
+                    });
+            }
+
+
+            for (auto edge : nav.edges())
+            {
+                addEdge(model.graph.vertex(edge.sourceObjectID()),
+                        model.graph.vertex(edge.targetObjectID()),
+                        {edge.attrib()});
+            }
+        }
+
+        // Trigger a view update.
+        emit graphChanged();
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::setEmptyGraph(const armem::MemoryID& entityID)
+    {
+        model.graphEntityID = entityID;
+
+        {
+            QSignalBlocker blocker(this);
+            clearGraph();
+
+            semrel::ShapeID id{0};
+            queryLocations();
+            model.locationsMemory.forEachInstance(
+                [this, &id](const armem::wm::EntityInstance& instance)
+                {
+                    addVertex(id, instance);
+                    ++id;
+                });
+        }
+
+        // Mark graph as changed.
+        model.graph.attrib().edgesChanged = true;
+
+        emit graphChanged();
+    }
+
+
+    GuiGraph::Vertex
+    LocationGraphEditorWidgetController::addVertex(semrel::ShapeID vertexID,
+                                                   const VertexData& defaultAttribs)
+    {
+        ARMARX_CHECK(not model.graph.hasVertex(vertexID));
+
+        GuiGraph::Vertex vertex = model.graph.addVertex(vertexID, defaultAttribs);
+        vertex.attrib().graphicsItem = view.graph->scene()->addVertex(vertex);
+        vertex.attrib().tableWidgetItem = view.vertexTable->addVertex();
+
+        emit graphChanged();
+
+        return vertex;
+    }
+
+
+    GuiGraph::Vertex
+    LocationGraphEditorWidgetController::addVertex(
+        semrel::ShapeID vertexID,
+        const armem::wm::EntityInstance& locationInstance)
+    {
+        navigation::location::arondto::Location location;
+        location.fromAron(locationInstance.data());
+
+        VertexData attrib;
+        toAron(attrib.aron.locationID, locationInstance.id().getEntityID());
+        attrib.setPose(location.globalRobotPose);
+        return addVertex(vertexID, attrib);
+    }
+
+
+    GuiGraph::Edge
+    LocationGraphEditorWidgetController::addEdge(GuiGraph::ConstVertex source,
+                                                 GuiGraph::ConstVertex target,
+                                                 const EdgeData& defaultAttribs)
+    {
+        ARMARX_CHECK(not model.graph.hasEdge(source.objectID(), target.objectID()))
+            << "Edge must not exist before being added: '" << source.attrib().getName() << "' -> '"
+            << target.attrib().getName() << "'";
+
+        GuiGraph::Edge edge = model.graph.addEdge(source, target, defaultAttribs);
+        edge.attrib().tableWidgetItem = view.edgeTable->addEdge(edge);
+        edge.attrib().graphicsItem = view.graph->scene()->addEdge(edge);
+
+        emit graphChanged();
+
+        return edge;
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::updateGraphView()
+    {
+        for (auto vertex : model.graph.vertices())
+        {
+            updateVertexView(vertex);
+        }
+        for (auto edge : model.graph.edges())
+        {
+            updateEdgeView(edge);
+        }
+
+        int index =
+            widget.graphsComboBox->findData(QString::fromStdString(model.graphEntityID.str()));
+        if (index >= 0)
+        {
+            widget.graphsComboBox->setItemText(
+                index, getGraphDisplayName(model.graphEntityID, model.graph.hasChanged()));
+        }
+
+        updateArViz();
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::updateVertexView(GuiGraph::Vertex vertex)
+    {
+        view.graph->scene()->updateVertex(vertex);
+        view.vertexTable->updateVertex(vertex);
+
+        if (/* DISABLES CODE */ (false)) // Disable this for the moment.
+        {
+            // Highlight all edges between highlighted vertices
+            std::set<semrel::ShapeID> highlightedVertices;
+            for (auto v : model.graph.vertices())
+            {
+                if (v.attrib().highlighted)
+                {
+                    highlightedVertices.insert(v.objectID());
+                }
+            }
+
+            for (auto edge : model.graph.edges())
+            {
+                bool verticesHighlighted = highlightedVertices.count(edge.sourceObjectID()) and
+                                           highlightedVertices.count(edge.targetObjectID());
+
+                // Already highlighted but vertices not highlighted => to false, update
+                // Not highlighted but vertices highlighted => to true, update
+                if (edge.attrib().highlighted != verticesHighlighted)
+                {
+                    edge.attrib().highlighted = verticesHighlighted;
+                }
+            }
+        }
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::updateEdgeView(GuiGraph::Edge edge)
+    {
+        view.graph->scene()->updateEdge(edge);
+        view.edgeTable->updateEdge(edge);
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::updateArViz()
+    {
+        if (remote.arviz)
+        {
+            std::vector<viz::Layer> layers;
+            {
+                viz::Layer& vertices = layers.emplace_back(remote.arviz->layer("Locations"));
+                applyVisu(vertices, model.visu.vertex, model.graph.vertices());
+            }
+            {
+                viz::Layer& edges = layers.emplace_back(remote.arviz->layer("Graph Edges"));
+                applyVisu(edges, model.visu.edge, model.graph.edges());
+            }
+            {
+                viz::Layer& robot = layers.emplace_back(remote.arviz->layer("Robot"));
+                if (view.vertexData->vertex().has_value() and view.robotVisu->isEnabled())
+                {
+                    robot.add(view.robotVisu->connection().vizRobot("robot").pose(
+                        view.vertexData->vertex()->attrib().getPose()));
+                }
+            }
+            remote.arviz->commit(layers);
+        }
+    }
+
+
+    template <class T>
+    static void
+    updateElementHighlighting(QList<QTableWidgetItem*> selectedItems,
+                              std::map<QTableWidgetItem*, T>&& itemToElementMap)
+    {
+        for (QTableWidgetItem* selected : selectedItems)
+        {
+            if (auto it = itemToElementMap.find(selected); it != itemToElementMap.end())
+            {
+                it->second.attrib().highlighted = true;
+                itemToElementMap.erase(it);
+            }
+        }
+        for (auto& [_, unselected] : itemToElementMap)
+        {
+            unselected.attrib().highlighted = false;
+        }
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::updateVertexHighlighting()
+    {
+        updateElementHighlighting(view.vertexTable->selectedVertexItems(),
+                                  model.graph.getTableItemToVertexMap());
+        emit graphChanged();
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::updateEdgeHighlighting()
+    {
+        updateElementHighlighting(view.edgeTable->selectedEdgeItems(),
+                                  model.graph.getTableItemToEdgeMap());
+        emit graphChanged();
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::selectVertex(QTableWidgetItem* vertexItem)
+    {
+        if (vertexItem == nullptr)
+        {
+            view.vertexData->clearVertex();
+        }
+        if (auto vertex = model.graph.getVertexFromTableItem(vertexItem))
+        {
+            selectVertex(vertex.value());
+        }
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::selectVertex(GuiGraph::Vertex vertex)
+    {
+        view.vertexData->setVertex(vertex);
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::clearGraph()
+    {
+        {
+            QSignalBlocker blocker(this);
+            clearEdges();
+            clearVertices();
+        }
+
+        // Clear data structure
+        ARMARX_CHECK_EQUAL(model.graph.numEdges(), 0);
+        ARMARX_CHECK_EQUAL(model.graph.numVertices(), 0);
+
+        emit graphChanged();
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::clearEdges()
+    {
+        // Remove in reverse order to be more array-friendly.
+        std::vector<GuiGraph::Edge> edges{model.graph.edges().begin(), model.graph.edges().end()};
+        {
+            QSignalBlocker blocker(this);
+            for (auto it = edges.rbegin(); it != edges.rend(); ++it)
+            {
+                GuiGraph::Edge edge = *it;
+                removeEdge(edge);
+            }
+        }
+
+        ARMARX_CHECK_EQUAL(view.edgeTable->rowCount(), 0);
+        ARMARX_CHECK_EQUAL(model.graph.numEdges(), 0);
+
+        emit graphChanged();
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::clearVertices()
+    {
+        ARMARX_CHECK_EQUAL(model.graph.numEdges(), 0)
+            << "The graph may not have any edges when clearing the vertices.";
+
+        // Remove in reverse order to be more array-friendly.
+        std::vector<GuiGraph::Vertex> vertices{model.graph.vertices().begin(),
+                                               model.graph.vertices().end()};
+        {
+            QSignalBlocker blocker(this);
+            for (auto it = vertices.rbegin(); it != vertices.rend(); ++it)
+            {
+                GuiGraph::Vertex vertex = *it;
+                removeVertex(vertex);
+            }
+        }
+
+        ARMARX_CHECK_EQUAL(view.vertexTable->rowCount(), 0);
+        ARMARX_CHECK_EQUAL(model.graph.numVertices(), 0);
+
+        emit graphChanged();
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::removeEdge(GuiGraph::Edge& edge)
+    {
+        ARMARX_CHECK(model.graph.hasEdge(edge.sourceDescriptor(), edge.targetDescriptor()))
+            << "Cannot remove edge that does not exist.";
+
+        // Remove view elements
+        {
+            QSignalBlocker blocker(view.edgeTable);
+            view.edgeTable->removeEdge(edge);
+        }
+        view.graph->scene()->removeEdge(edge.attrib().graphicsItem);
+
+        // Remove from model
+        model.graph.removeEdge(edge);
+
+        ARMARX_CHECK(not model.graph.hasEdge(edge.sourceDescriptor(), edge.targetDescriptor()))
+            << edge.sourceDescriptor() << " -> " << edge.targetDescriptor();
+
+        emit graphChanged();
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::removeVertex(GuiGraph::Vertex& vertex)
+    {
+        ARMARX_CHECK_EQUAL(vertex.inDegree(), 0)
+            << "A vertex must not have any edges before being removed. "
+            << vertex.attrib().getName();
+        ARMARX_CHECK_EQUAL(vertex.outDegree(), 0)
+            << "A vertex must not have any edges before being removed. "
+            << vertex.attrib().getName();
+
+        // Remove view elements
+        {
+            QSignalBlocker blocker(view.vertexTable);
+            view.vertexTable->removeVertex(vertex);
+        }
+        view.graph->scene()->removeVertex(vertex.attrib().graphicsItem);
+        if (view.vertexData->vertex().has_value() and view.vertexData->vertex().value() == vertex)
+        {
+            view.vertexData->clearVertex();
+        }
+
+        // Remove from model
+        model.graph.removeVertex(vertex);
+
+        emit graphChanged();
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::createVertexDialog()
+    {
+        NewEntityIdDialog dialog(navigation::location::coreSegmentID, nullptr);
+        switch (dialog.exec())
+        {
+            case QDialog::Accepted:
+                // Ok go.
+                break;
+            case QDialog::Rejected:
+                // Abort creation.
+                return;
+        }
+
+        // Find free vertex ID
+        const semrel::ShapeID vertexID = findNextFreeVertexID(model.graph, semrel::ShapeID{0});
+
+        // Initiaize attributes
+        VertexData attribs;
+        toAron(attribs.aron.locationID, dialog.entityID());
+        attribs.setPose(Eigen::Matrix4f::Identity());
+        attribs.changed = true;
+
+        addVertex(vertexID, attribs);
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::addEdges(
+        QList<QPair<QTableWidgetItem*, QTableWidgetItem*>> vertexItems)
+    {
+        const auto itemToVertexMap = model.graph.getTableItemToVertexMap();
+        {
+            QSignalBlocker blocker(this);
+
+            for (const auto& [sourceItem, targetItem] : vertexItems)
+            {
+                GuiGraph::Vertex source = itemToVertexMap.at(sourceItem);
+                GuiGraph::Vertex target = itemToVertexMap.at(targetItem);
+                if (not model.graph.hasEdge(source, target))
+                {
+                    addEdge(source, target, {});
+                }
+            }
+        }
+
+        model.graph.attrib().edgesChanged = true;
+        emit graphChanged();
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::removeEdges(QList<QTableWidgetItem*> edgeItems)
+    {
+        const auto itemToEdgeMap = model.graph.getTableItemToEdgeMap();
+        {
+            QSignalBlocker blocker(this);
+
+            for (const auto& edgeItem : edgeItems)
+            {
+                GuiGraph::Edge edge = itemToEdgeMap.at(edgeItem);
+                removeEdge(edge);
+            }
+        }
+
+        model.graph.attrib().edgesChanged = true;
+        emit graphChanged();
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::removeEdgesOfVertex(QList<QTableWidgetItem*> vertexItems,
+                                                             utils::EdgeDirection direction)
+    {
+        auto removeEdges = [this](auto&& range)
+        {
+            for (auto edge : range)
+            {
+                removeEdge(edge);
+            }
+        };
+        {
+            QSignalBlocker blocker(this);
+
+            const auto itemToVertexMap = model.graph.getTableItemToVertexMap();
+            for (const auto& vertexItem : vertexItems)
+            {
+                GuiGraph::Vertex vertex = itemToVertexMap.at(vertexItem);
+                switch (direction)
+                {
+                    case utils::EdgeDirection::To:
+                        removeEdges(vertex.inEdges());
+                        break;
+
+                    case utils::EdgeDirection::From:
+                        removeEdges(vertex.outEdges());
+                        break;
+
+                    case utils::EdgeDirection::Bidirectional:
+                        removeEdges(vertex.inEdges());
+                        removeEdges(vertex.outEdges());
+                        break;
+                }
+            }
+        }
+
+        model.graph.attrib().edgesChanged = true;
+        emit graphChanged();
+    }
+
+
+    void
+    LocationGraphEditorWidgetController::createGraphDialog()
+    {
+        NewEntityIdDialog dialog(navigation::graph::coreSegmentID, nullptr);
+        switch (dialog.exec())
+        {
+            case QDialog::Accepted:
+                break;
+            case QDialog::Rejected:
+                return;
+        }
+
+        const armem::MemoryID entityID = dialog.entityID();
+
+        const bool hasChanged = true;
+        QString text = getGraphDisplayName(entityID, hasChanged);
+        QString data = QString::fromStdString(entityID.str());
+        widget.graphsComboBox->addItem(text, data);
+        widget.graphsComboBox->setCurrentIndex(widget.graphsComboBox->count() - 1);
+
+        setEmptyGraph(entityID);
+    }
+
+
+    QString
+    LocationGraphEditorWidgetController::getGraphDisplayName(const armem::MemoryID& entityID,
+                                                             bool changed) const
+    {
+        QString name =
+            QString::fromStdString(entityID.providerSegmentName + "/" + entityID.entityName);
+        if (changed)
+        {
+            name += "*";
+        }
+        return name;
+    }
+
+} // namespace armarx::navigation::locgrapheditor
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/LocationGraphEditorWidgetController.h b/source/armarx/navigation/gui-plugins/LocationGraphEditor/LocationGraphEditorWidgetController.h
new file mode 100644
index 0000000000000000000000000000000000000000..30ee5941f0d02333a8a3430b7e337dbb1a92aeaf
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/LocationGraphEditorWidgetController.h
@@ -0,0 +1,265 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package    Navigation::gui-plugins::LocationGraphEditorWidgetController
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+#pragma once
+
+#include <memory>
+#include <string>
+
+#include <ArmarXCore/core/Component.h>
+#include <ArmarXCore/core/system/ImportExportComponent.h>
+
+#include <ArmarXGui/libraries/ArmarXGuiBase/ArmarXComponentWidgetController.h>
+#include <ArmarXGui/libraries/SimpleConfigDialog/SimpleConfigDialog.h>
+
+#include <RobotAPI/libraries/armem/client/MemoryNameSystem.h>
+#include <RobotAPI/libraries/armem/client/Reader.h>
+#include <RobotAPI/libraries/armem/client/Writer.h>
+#include <RobotAPI/libraries/armem/client/forward_declarations.h>
+#include <RobotAPI/libraries/armem/core/wm/memory_definitions.h>
+
+#include "GuiGraph.h"
+#include "Visu.h"
+#include "widgets/graph_scene.h"
+#include <Navigation/gui-plugins/LocationGraphEditor/ui_LocationGraphEditorWidget.h>
+#include <armarx/navigation/graph/Graph.h>
+#include <armarx/navigation/location/aron/Location.aron.generated.h>
+
+
+class QDialog;
+
+
+namespace armarx::viz
+{
+    class Client;
+}
+namespace armarx::navigation::locgrapheditor::utils
+{
+    enum class EdgeDirection;
+}
+namespace armarx::navigation::locgrapheditor
+{
+    class EdgeTableWidget;
+    class RobotVisuWidget;
+    class VertexDataWidget;
+    class VertexTableWidget;
+
+
+    /**
+    \page Navigation-GuiPlugins-LocationGraphEditor LocationGraphEditor
+    \brief The LocationGraphEditor allows visualizing ...
+
+    \image html LocationGraphEditor.png
+    The user can
+
+    API Documentation \ref LocationGraphEditorWidgetController
+
+    \see LocationGraphEditorGuiPlugin
+    */
+
+
+    /**
+     * \class LocationGraphEditorGuiPlugin
+     * \ingroup ArmarXGuiPlugins
+     * \brief LocationGraphEditorGuiPlugin brief description
+     *
+     * Detailed description
+     */
+
+    /**
+     * \class LocationGraphEditorWidgetController
+     * \brief LocationGraphEditorWidgetController brief one line description
+     *
+     * Detailed description
+     */
+    class ARMARXCOMPONENT_IMPORT_EXPORT LocationGraphEditorWidgetController :
+        public armarx::ArmarXComponentWidgetControllerTemplate<LocationGraphEditorWidgetController>
+    {
+        Q_OBJECT
+
+        using This = LocationGraphEditorWidgetController;
+
+
+    public:
+        static QString GetWidgetName();
+        static QIcon GetWidgetIcon();
+
+
+        explicit LocationGraphEditorWidgetController();
+        virtual ~LocationGraphEditorWidgetController() override;
+
+
+        //// @see ArmarXWidgetController::loadSettings()
+        void loadSettings(QSettings* settings) override;
+        /// @see ArmarXWidgetController::saveSettings()
+        void saveSettings(QSettings* settings) override;
+
+
+        QPointer<QDialog> getConfigDialog(QWidget* parent = nullptr) override;
+        void configured() override;
+
+
+        void onInitComponent() override;
+        void onConnectComponent() override;
+
+
+        //because the above load/save functions are *obviously* for "Save/Load Gui Config." :/
+        virtual void loadAutomaticSettings();
+        virtual void saveAutomaticSettings();
+
+
+    signals:
+
+        void connected();
+
+        void locationMemoryChanged();
+        void graphMemoryChanged();
+        void memoryChanged();
+
+        void graphChanged();
+
+
+    private slots:
+
+        // Load
+        void queryMemory();
+        armem::client::QueryResult queryLocations();
+        armem::client::QueryResult queryGraphs();
+
+        void loadGraph();
+        bool loadGraphDialog();
+
+        // Commit
+        void commit();
+        armem::CommitResult commitLocations();
+        armem::EntityUpdateResult commitGraph();
+
+
+        // View & Tables
+        void updateGraphList();
+        void updateGraphView();
+        void updateVertexView(GuiGraph::Vertex vertex);
+        void updateEdgeView(GuiGraph::Edge edge);
+        void updateArViz();
+
+
+        // Selection & Highlighting
+        void selectVertex(QTableWidgetItem* vertexItem);
+        void selectVertex(GuiGraph::Vertex vertex);
+        void updateVertexHighlighting();
+        void updateEdgeHighlighting();
+
+
+        // Graph modification triggered by user interaction
+        void createVertexDialog();
+
+        void addEdges(QList<QPair<QTableWidgetItem*, QTableWidgetItem*>> vertexItems);
+        void removeEdges(QList<QTableWidgetItem*> edgeItems);
+        void removeEdgesOfVertex(QList<QTableWidgetItem*> vertexItems,
+                                 utils::EdgeDirection direction);
+
+        void createGraphDialog();
+
+
+    private:
+        void setGraph(const armem::MemoryID& entityID, const graph::Graph& nav);
+        void setEmptyGraph(const armem::MemoryID& entityID);
+
+        GuiGraph::Vertex addVertex(semrel::ShapeID vertexID, const VertexData& defaultAttribs);
+        GuiGraph::Vertex addVertex(semrel::ShapeID vertexID,
+                                   const armem::wm::EntityInstance& locationInstance);
+
+        GuiGraph::Edge addEdge(GuiGraph::ConstVertex source,
+                               GuiGraph::ConstVertex target,
+                               const EdgeData& defaultAttribs);
+
+        void removeVertex(GuiGraph::Vertex& vertex);
+        void removeEdge(GuiGraph::Edge& edge);
+
+        void clearGraph();
+        void clearEdges();
+        void clearVertices();
+
+
+        QString getGraphDisplayName(const armem::MemoryID& entityID, bool changed = false) const;
+
+
+    private:
+        /// Widget Form
+        Ui::LocationGraphEditorWidget widget;
+        QPointer<SimpleConfigDialog> configDialog;
+
+
+        struct Remote
+        {
+            std::string memoryNameSystemName = "MemoryNameSystem";
+            armem::client::MemoryNameSystem memoryNameSystem;
+
+            armem::client::Reader locationReader;
+            armem::client::Writer locationWriter;
+
+            armem::client::Reader graphReader;
+            armem::client::Writer graphWriter;
+
+            std::unique_ptr<viz::Client> arviz;
+
+            void connect(Component& parent);
+        };
+        Remote remote;
+
+
+        struct Model
+        {
+            armem::wm::Memory locationsMemory;
+
+            armem::wm::Memory graphMemory;
+            armem::MemoryID graphEntityID;
+            GuiGraph graph;
+
+            GraphVisu visu;
+        };
+        Model model;
+
+
+        struct View
+        {
+            VertexTableWidget* vertexTable = nullptr;
+            EdgeTableWidget* edgeTable = nullptr;
+            VertexDataWidget* vertexData = nullptr;
+
+            GraphSceneWidget* graph = nullptr;
+
+            RobotVisuWidget* robotVisu = nullptr;
+        };
+        View view;
+
+
+    private:
+        QSettings settings;
+        QString lastSelectedSceneName;
+    };
+
+} // namespace armarx::navigation::locgrapheditor
+
+namespace armarx
+{
+    using armarx::navigation::locgrapheditor::LocationGraphEditorWidgetController;
+}
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/Visu.cpp b/source/armarx/navigation/gui-plugins/LocationGraphEditor/Visu.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..bbfb5d6c7490e4de0b9ac48f790e06283ae66f49
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/Visu.cpp
@@ -0,0 +1,134 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#include "Visu.h"
+
+#include <SimoxUtility/color/Color.h>
+#include <SimoxUtility/math/pose.h>
+#include <VirtualRobot/VirtualRobot.h>
+
+#include <RobotAPI/components/ArViz/Client/Client.h>
+
+
+namespace armarx::navigation::locgrapheditor
+{
+
+    const simox::Color visu::defaultColorHighlighted = simox::Color::orange();
+
+
+    viz::Pose
+    VertexVisu::Pose::draw(const VertexData& attribs) const
+    {
+        viz::Pose pose = navigation::graph::VertexVisu::Pose::draw(attribs);
+        if (attribs.highlighted)
+        {
+            pose.scale(scale * scaleFactorHighlighted);
+        }
+        return pose;
+    }
+
+
+    viz::Arrow
+    VertexVisu::ForwardArrow::draw(const VertexData& attribs) const
+    {
+        viz::Arrow arrow = navigation::graph::VertexVisu::ForwardArrow::draw(attribs);
+        if (attribs.highlighted)
+        {
+            arrow.color(colorHighlighted);
+            arrow.width(width * 1.5f);
+        }
+        return arrow;
+    }
+
+
+    void
+    VertexVisu::draw(viz::Layer& layer, GuiGraph::ConstVertex vertex) const
+    {
+        draw(layer, vertex.attrib());
+    }
+
+
+    void
+    VertexVisu::draw(viz::Layer& layer, const VertexData& attribs) const
+    {
+        if (pose.has_value())
+        {
+            layer.add(pose->draw(attribs));
+        }
+        if (forwardArrow.has_value())
+        {
+            layer.add(forwardArrow->draw(attribs));
+        }
+    }
+
+
+    viz::Arrow
+    EdgeVisu::Arrow::draw(GuiGraph::ConstEdge edge) const
+    {
+        return draw(edge.attrib(), edge.source().attrib(), edge.target().attrib());
+    }
+
+
+    viz::Arrow
+    EdgeVisu::Arrow::draw(const EdgeData& edge,
+                          const VertexData& source,
+                          const VertexData& target) const
+    {
+        viz::Arrow arrow = navigation::graph::EdgeVisu::Arrow::draw(edge, source, target);
+        if (edge.highlighted)
+        {
+            arrow.color(colorHighlighted);
+            arrow.width(width * 1.5f);
+        }
+        return arrow;
+    }
+
+
+    void
+    EdgeVisu::draw(viz::Layer& layer, GuiGraph::ConstEdge edge) const
+    {
+        if (arrow.has_value())
+        {
+            layer.add(arrow->draw(edge));
+        }
+    }
+
+
+    void
+    GraphVisu::draw(viz::Layer& layer, const GuiGraph& graph) const
+    {
+        if (vertex.has_value())
+        {
+            for (GuiGraph::ConstVertex v : graph.vertices())
+            {
+                vertex->draw(layer, v);
+            }
+        }
+        if (edge.has_value())
+        {
+            for (GuiGraph::ConstEdge e : graph.edges())
+            {
+                edge->draw(layer, e);
+            }
+        }
+    }
+
+} // namespace armarx::navigation::locgrapheditor
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/Visu.h b/source/armarx/navigation/gui-plugins/LocationGraphEditor/Visu.h
new file mode 100644
index 0000000000000000000000000000000000000000..77193be2babbbfd30f4390a10b7ef565729f65c1
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/Visu.h
@@ -0,0 +1,103 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <optional>
+
+#include <SimoxUtility/color/Color.h>
+
+#include "GuiGraph.h"
+#include <armarx/navigation/graph/Visu.h>
+
+
+namespace armarx::navigation::locgrapheditor::visu
+{
+    extern const simox::Color defaultColorHighlighted;
+}
+namespace armarx::navigation::locgrapheditor
+{
+
+    struct VertexVisu
+    {
+        struct Pose : public navigation::graph::VertexVisu::Pose
+        {
+            float scaleFactorHighlighted = 1.5;
+
+            viz::Pose draw(const VertexData& attribs) const;
+        };
+        std::optional<Pose> pose = Pose{};
+
+        struct ForwardArrow : public navigation::graph::VertexVisu::ForwardArrow
+        {
+            simox::Color colorHighlighted = visu::defaultColorHighlighted;
+
+            viz::Arrow draw(const VertexData& attribs) const;
+        };
+        std::optional<ForwardArrow> forwardArrow = ForwardArrow{};
+
+
+        void draw(viz::Layer& layer, GuiGraph::ConstVertex vertex) const;
+        void draw(viz::Layer& layer, const VertexData& attribs) const;
+    };
+
+
+    struct EdgeVisu
+    {
+        struct Arrow : public navigation::graph::EdgeVisu::Arrow
+        {
+            simox::Color colorHighlighted = visu::defaultColorHighlighted;
+
+            viz::Arrow draw(GuiGraph::ConstEdge edge) const;
+            viz::Arrow
+            draw(const EdgeData& edge, const VertexData& source, const VertexData& target) const;
+        };
+        std::optional<Arrow> arrow = Arrow{};
+
+
+        void draw(viz::Layer& layer, GuiGraph::ConstEdge edge) const;
+    };
+
+
+    struct GraphVisu
+    {
+        std::optional<VertexVisu> vertex = VertexVisu{};
+        std::optional<EdgeVisu> edge = EdgeVisu{};
+
+
+        void draw(viz::Layer& layer, const GuiGraph& graph) const;
+    };
+
+
+    template <class VisuT, class RangeT>
+    void
+    applyVisu(viz::Layer& layer, const std::optional<VisuT>& visu, RangeT&& range)
+    {
+        if (visu.has_value())
+        {
+            for (auto element : range)
+            {
+                visu->draw(layer, element);
+            }
+        }
+    }
+
+} // namespace armarx::navigation::locgrapheditor
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/icons.qrc b/source/armarx/navigation/gui-plugins/LocationGraphEditor/icons.qrc
new file mode 100644
index 0000000000000000000000000000000000000000..14569cdf95896f59a34352a155e82c82f9ea7bc9
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/icons.qrc
@@ -0,0 +1,5 @@
+<RCC>
+    <qresource prefix="/">
+        <file>icons/location_graph_editor.svg</file>
+    </qresource>
+</RCC>
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/icons/location_graph_editor.svg b/source/armarx/navigation/gui-plugins/LocationGraphEditor/icons/location_graph_editor.svg
new file mode 100644
index 0000000000000000000000000000000000000000..6f27578f044c95c6c897ff3dc70d76fd4280e280
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/icons/location_graph_editor.svg
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   enable-background="new 0 0 512 512"
+   height="512px"
+   id="Layer_1"
+   version="1.1"
+   viewBox="0 0 512 512"
+   width="512px"
+   xml:space="preserve"
+   inkscape:version="0.48.4 r9939"
+   sodipodi:docname="graph_visu.svg"><metadata
+     id="metadata9"><rdf:RDF><cc:Work
+         rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+     id="defs7" /><sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1920"
+     inkscape:window-height="1028"
+     id="namedview5"
+     showgrid="false"
+     inkscape:zoom="1.3037281"
+     inkscape:cx="91.880082"
+     inkscape:cy="258.60516"
+     inkscape:window-x="1280"
+     inkscape:window-y="24"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="Layer_1" /><path
+     d="M480,0H32C14.328,0,0,14.312,0,32v352c0,17.688,14.328,32,32,32h192v32h-96c-17.672,0-32,14.312-32,32v32h320v-32  c0-17.688-14.328-32-32-32h-96v-32h192c17.672,0,32-14.312,32-32V32C512,14.312,497.672,0,480,0z M448,352H64V64h384V352z"
+     id="path3" /><path
+     sodipodi:type="arc"
+     style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     id="path3025"
+     sodipodi:cx="129.62825"
+     sodipodi:cy="159.54921"
+     sodipodi:rx="9.9714041"
+     sodipodi:ry="9.5878887"
+     d="m 139.59965,159.54921 a 9.9714041,9.5878887 0 1 1 -19.9428,0 9.9714041,9.5878887 0 1 1 19.9428,0 z"
+     transform="matrix(1.9081592,0,0,1.9844855,-85.183547,-107.15758)" /><path
+     sodipodi:type="arc"
+     style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     id="path3025-3"
+     sodipodi:cx="129.62825"
+     sodipodi:cy="159.54921"
+     sodipodi:rx="9.9714041"
+     sodipodi:ry="9.5878887"
+     d="m 139.59965,159.54921 a 9.9714041,9.5878887 0 1 1 -19.9428,0 9.9714041,9.5878887 0 1 1 19.9428,0 z"
+     transform="matrix(1.9081592,0,0,1.9844855,-37.951856,-198.87707)" /><path
+     sodipodi:type="arc"
+     style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     id="path3025-6"
+     sodipodi:cx="129.62825"
+     sodipodi:cy="159.54921"
+     sodipodi:rx="9.9714041"
+     sodipodi:ry="9.5878887"
+     d="m 139.59965,159.54921 a 9.9714041,9.5878887 0 1 1 -19.9428,0 9.9714041,9.5878887 0 1 1 19.9428,0 z"
+     transform="matrix(1.9081592,0,0,1.9844855,11.90517,-61.578511)" /><path
+     sodipodi:type="arc"
+     style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     id="path3025-7"
+     sodipodi:cx="129.62825"
+     sodipodi:cy="159.54921"
+     sodipodi:rx="9.9714041"
+     sodipodi:ry="9.5878887"
+     d="m 139.59965,159.54921 a 9.9714041,9.5878887 0 1 1 -19.9428,0 9.9714041,9.5878887 0 1 1 19.9428,0 z"
+     transform="matrix(1.9081592,0,0,1.9844855,-129.99558,-14.022576)" /><path
+     sodipodi:type="arc"
+     style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     id="path3025-5"
+     sodipodi:cx="129.62825"
+     sodipodi:cy="159.54921"
+     sodipodi:rx="9.9714041"
+     sodipodi:ry="9.5878887"
+     d="m 139.59965,159.54921 a 9.9714041,9.5878887 0 1 1 -19.9428,0 9.9714041,9.5878887 0 1 1 19.9428,0 z"
+     transform="matrix(1.9081592,0,0,1.9844855,-134.59777,-179.7013)" /><path
+     sodipodi:type="arc"
+     style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     id="path3025-35"
+     sodipodi:cx="129.62825"
+     sodipodi:cy="159.54921"
+     sodipodi:rx="9.9714041"
+     sodipodi:ry="9.5878887"
+     d="m 139.59965,159.54921 a 9.9714041,9.5878887 0 1 1 -19.9428,0 9.9714041,9.5878887 0 1 1 19.9428,0 z"
+     transform="matrix(1.9081592,0,0,1.9844855,106.24999,-80.754285)" /><path
+     sodipodi:type="arc"
+     style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     id="path3025-62"
+     sodipodi:cx="129.62825"
+     sodipodi:cy="159.54921"
+     sodipodi:rx="9.9714041"
+     sodipodi:ry="9.5878887"
+     d="m 139.59965,159.54921 a 9.9714041,9.5878887 0 1 1 -19.9428,0 9.9714041,9.5878887 0 1 1 19.9428,0 z"
+     transform="matrix(1.9081592,0,0,1.9844855,108.55109,-189.6727)" /><path
+     style="fill:none;stroke:#000000;stroke-width:11;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
+     d="M 151.45515,193.73856 123.46621,152.64875"
+     id="path3093"
+     inkscape:connector-type="polyline"
+     inkscape:connector-curvature="3"
+     inkscape:connection-start="#path3025"
+     inkscape:connection-start-point="d4"
+     inkscape:connection-end="#path3025-5"
+     inkscape:connection-end-point="d4" /><path
+     style="fill:none;stroke:#000000;stroke-width:11;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
+     d="m 125.60728,285.45098 28.30899,-58.83593"
+     id="path3095"
+     inkscape:connector-type="polyline"
+     inkscape:connector-curvature="3"
+     inkscape:connection-start="#path3025-7"
+     inkscape:connection-start-point="d4"
+     inkscape:connection-end="#path3025"
+     inkscape:connection-end-point="d4" /><path
+     style="fill:none;stroke:#000000;stroke-width:11;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
+     d="m 215.89558,135.63528 36.86483,101.52004"
+     id="path3097"
+     inkscape:connector-type="polyline"
+     inkscape:connector-curvature="3"
+     inkscape:connection-start="#path3025-3"
+     inkscape:connection-start-point="d4"
+     inkscape:connection-end="#path3025-6"
+     inkscape:connection-end-point="d4" /><path
+     style="fill:none;stroke:#000000;stroke-width:11;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
+     d="M 200.68669,134.66544 170.88058,192.5461"
+     id="path3099"
+     inkscape:connector-type="polyline"
+     inkscape:connector-curvature="3"
+     inkscape:connection-start="#path3025-3"
+     inkscape:connection-start-point="d4"
+     inkscape:connection-end="#path3025"
+     inkscape:connection-end-point="d4" /><path
+     style="fill:none;stroke:#000000;stroke-width:11;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
+     d="m 334.9519,239.65934 -57.04596,11.59471"
+     id="path3101"
+     inkscape:connector-type="polyline"
+     inkscape:connector-curvature="3"
+     inkscape:connection-start="#path3025-35"
+     inkscape:connection-start-point="d4"
+     inkscape:connection-end="#path3025-6"
+     inkscape:connection-end-point="d4" /><path
+     style="fill:none;stroke:#000000;stroke-width:11;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
+     d="m 344.44196,142.14007 -73.72498,97.71484"
+     id="path3103"
+     inkscape:connector-type="polyline"
+     inkscape:connector-curvature="3"
+     inkscape:connection-start="#path3025-62"
+     inkscape:connection-start-point="d4"
+     inkscape:connection-end="#path3025-6"
+     inkscape:connection-end-point="d4" /><path
+     style="fill:none;stroke:#000000;stroke-width:11;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
+     d="m 228.38971,118.93913 108.52249,6.81816"
+     id="path3105"
+     inkscape:connector-type="polyline"
+     inkscape:connector-curvature="3"
+     inkscape:connection-start="#path3025-3"
+     inkscape:connection-start-point="d4"
+     inkscape:connection-end="#path3025-62"
+     inkscape:connection-end-point="d4" /><path
+     style="fill:none;stroke:#000000;stroke-width:11;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
+     d="m 355.50054,145.97326 -1.49732,70.87268"
+     id="path3107"
+     inkscape:connector-type="polyline"
+     inkscape:connector-curvature="3"
+     inkscape:connection-start="#path3025-62"
+     inkscape:connection-start-point="d4"
+     inkscape:connection-end="#path3025-35"
+     inkscape:connection-end-point="d4" /><path
+     style="fill:none;stroke:#000000;stroke-width:11;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
+     d="m 135.4015,296.55275 105.80926,-35.4604"
+     id="path3109"
+     inkscape:connector-type="polyline"
+     inkscape:connector-curvature="3"
+     inkscape:connection-start="#path3025-7"
+     inkscape:connection-start-point="d4"
+     inkscape:connection-end="#path3025-6"
+     inkscape:connection-end-point="d4" /></svg>
\ No newline at end of file
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/ConnectDialog.cpp b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/ConnectDialog.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..3752fc450fd809ee04420779af0f70ceac654dc5
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/ConnectDialog.cpp
@@ -0,0 +1,28 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#include "ConnectDialog.h"
+
+
+namespace armarx::navigation::locgrapheditor
+{
+
+}
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/ConnectDialog.h b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/ConnectDialog.h
new file mode 100644
index 0000000000000000000000000000000000000000..2090fea9c5108448ffa8a945032b90c2a6a746b1
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/ConnectDialog.h
@@ -0,0 +1,82 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <QDialog>
+#include <QDialogButtonBox>
+
+#include <ArmarXCore/core/ManagedIceObject.h>
+
+#include <ArmarXGui/libraries/ArmarXGuiBase/widgets/IceProxyFinder.h>
+
+
+namespace armarx::navigation::locgrapheditor
+{
+    template <class ProxyT>
+    class ConnectDialog : public QDialog
+    {
+    public:
+        ConnectDialog(QString searchMask, ManagedIceObject& component, QWidget* parent = nullptr) :
+            QDialog(parent), component(component)
+        {
+            finder = new armarx::IceProxyFinder<ProxyT>();
+            finder->setIceManager(component.getIceManager());
+            finder->setSearchMask(searchMask);
+
+            QDialogButtonBox* buttonBox =
+                new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
+
+            setLayout(new QVBoxLayout);
+            layout()->addWidget(finder);
+            layout()->addWidget(buttonBox);
+
+            connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
+            connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
+        }
+
+        virtual ~ConnectDialog() override
+        {
+        }
+
+
+        ProxyT
+        getProxy()
+        {
+            QString name = finder->getSelectedProxyName();
+            if (name.size() > 0)
+            {
+                return component.getProxy<ProxyT>(name.toStdString());
+            }
+            else
+            {
+                return nullptr;
+            }
+        }
+
+
+    public:
+        ManagedIceObject& component;
+        armarx::IceProxyFinder<ProxyT>* finder = nullptr;
+    };
+
+
+} // namespace armarx::navigation::locgrapheditor
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/EdgeTableWidget.cpp b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/EdgeTableWidget.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..13c436fea69e956af28183e7a0893e5508b13126
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/EdgeTableWidget.cpp
@@ -0,0 +1,155 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#include "EdgeTableWidget.h"
+
+#include <QAction>
+#include <QHeaderView>
+#include <QMenu>
+
+#include "utils.h"
+
+
+namespace armarx::navigation::locgrapheditor
+{
+
+    EdgeTableWidget::EdgeTableWidget()
+    {
+        QStringList columns{"Source", "Target"};
+        setColumnCount(columns.size());
+        setHorizontalHeaderLabels(columns);
+        horizontalHeader()->setResizeMode(0, QHeaderView::Stretch);
+        horizontalHeader()->setResizeMode(1, QHeaderView::Stretch);
+        horizontalHeader()->setVisible(true);
+
+        setEditTriggers(QAbstractItemView::NoEditTriggers);
+        setSortingEnabled(true);
+
+        setAlternatingRowColors(true);
+
+        QString styleSheet = this->styleSheet();
+        styleSheet = styleSheet + "\n" + "selection-background-color: #FF8000;";
+        setStyleSheet(styleSheet);
+
+        setContextMenuPolicy(Qt::CustomContextMenu);
+        connect(this, &This::customContextMenuRequested, this, &This::makeContextMenu);
+    }
+
+
+    QTableWidgetItem*
+    EdgeTableWidget::addEdge(const graph::VertexAttribs& sourceAttrib,
+                             const graph::VertexAttribs& targetAttrib)
+    {
+        QTableWidgetItem* result = nullptr;
+
+        setSortingEnabled(false);
+        {
+            int row = rowCount();
+            setRowCount(row + 1);
+
+            setItem(row, 0, new QTableWidgetItem{QString::fromStdString(sourceAttrib.getName())});
+            setItem(row, 1, new QTableWidgetItem{QString::fromStdString(targetAttrib.getName())});
+
+            result = item(row, 0);
+        }
+        setSortingEnabled(true);
+
+        return result;
+    }
+
+
+    void
+    EdgeTableWidget::updateEdge(GuiGraph::Edge edge)
+    {
+        QColor bgColor = edge.attrib().highlighted ? bgColorSelected : bgColorDefault;
+        QFont font;
+        font.setBold(edge.attrib().highlighted);
+
+        setSortingEnabled(false);
+        {
+            int row = this->row(edge.attrib().tableWidgetItem);
+            for (int col = 0; col < 2; ++col)
+            {
+                auto* item = this->item(row, col);
+                ARMARX_CHECK_NOT_NULL(item);
+
+                item->setData(Qt::BackgroundRole, bgColor);
+                item->setFont(font);
+            }
+        }
+        setSortingEnabled(true);
+    }
+
+
+    void
+    EdgeTableWidget::removeEdge(GuiGraph::Edge& edge)
+    {
+        if (currentItem() == edge.attrib().tableWidgetItem)
+        {
+            setCurrentItem(nullptr);
+        }
+
+        removeRow(row(edge.attrib().tableWidgetItem));
+        edge.attrib().tableWidgetItem = nullptr;
+    }
+
+
+    QList<QTableWidgetItem*>
+    EdgeTableWidget::selectedEdgeItems()
+    {
+        return utils::getSelectedItemsOfColumn(this, 0);
+    }
+
+
+    void
+    EdgeTableWidget::makeContextMenu(QPoint pos)
+    {
+        QList<QTableWidgetItem*> items = selectedEdgeItems();
+
+        QMenu menu;
+        if (items.size() == 0)
+        {
+            QAction* action = menu.addAction("No edges selected");
+            action->setEnabled(false);
+        }
+        else
+        {
+            QString desc;
+            if (items.size() == 1)
+            {
+                desc = "edge '" + items[0]->text() + "' " + utils::arrowRight + " '" +
+                       item(row(items[0]), 1)->text() + "'";
+            }
+            else
+            {
+                desc = QString::number(items.size()) + " edges";
+            }
+
+            menu.addSection("Selected " + desc);
+            connect(menu.addAction("Remove " + desc),
+                    &QAction::triggered,
+                    [this, &items]() { emit edgeRemovalRequested(items); });
+        }
+
+        menu.exec(mapToGlobal(pos));
+    }
+
+} // namespace armarx::navigation::locgrapheditor
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/EdgeTableWidget.h b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/EdgeTableWidget.h
new file mode 100644
index 0000000000000000000000000000000000000000..9e1aae1579a698837047813ed34ecb68449de8ac
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/EdgeTableWidget.h
@@ -0,0 +1,81 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <QColor>
+#include <QList>
+#include <QTableWidget>
+
+#include <armarx/navigation/gui-plugins/LocationGraphEditor/GuiGraph.h>
+#include <armarx/navigation/gui-plugins/LocationGraphEditor/widgets/default_colors.h>
+
+
+namespace armarx::navigation::locgrapheditor
+{
+
+    class EdgeTableWidget : public QTableWidget
+    {
+        Q_OBJECT
+        using This = EdgeTableWidget;
+
+
+    public:
+        EdgeTableWidget();
+
+
+        template <class EdgeT>
+        QTableWidgetItem*
+        addEdge(const EdgeT& edge)
+        {
+            return addEdge(edge.source().attrib(), edge.target().attrib());
+        }
+
+        QTableWidgetItem* addEdge(const graph::VertexAttribs& sourceAttrib,
+                                  const graph::VertexAttribs& targetAttrib);
+
+
+        void updateEdge(GuiGraph::Edge edge);
+
+        void removeEdge(GuiGraph::Edge& edge);
+
+        void clearEdges();
+
+
+        QList<QTableWidgetItem*> selectedEdgeItems();
+
+
+    signals:
+
+        void edgeRemovalRequested(QList<QTableWidgetItem*> edgeItems);
+
+
+    public slots:
+
+        void makeContextMenu(QPoint pos);
+
+
+    public:
+        QColor bgColorDefault = default_colors::tableBackgroundDefault;
+        QColor bgColorSelected = default_colors::tableBackgroundSelected;
+    };
+
+} // namespace armarx::navigation::locgrapheditor
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/NewEntityIdDialog.cpp b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/NewEntityIdDialog.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..7fdac3c4640f712cd2f84d54b985c99b3f5cebac
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/NewEntityIdDialog.cpp
@@ -0,0 +1,133 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#include "NewEntityIdDialog.h"
+
+#include <QDialogButtonBox>
+#include <QGridLayout>
+#include <QLabel>
+#include <QLineEdit>
+#include <QPushButton>
+#include <QVBoxLayout>
+
+#include <ArmarXCore/core/exceptions/local/ExpressionException.h>
+
+#include <RobotAPI/libraries/armem/core/MemoryID.h>
+
+
+namespace armarx::navigation::locgrapheditor
+{
+
+    NewEntityIdDialog::NewEntityIdDialog(const armem::MemoryID& coreSegmentID, QWidget* parent) :
+        QDialog(parent), coreSegmentID(std::make_unique<armem::MemoryID>(coreSegmentID))
+    {
+        _providerSegmentName = new QLineEdit(this);
+        _entityName = new QLineEdit(this);
+
+        _buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
+
+        QLabel* instruction =
+            new QLabel("Enter provider segment name and entity name for new entity:");
+        QFont font = instruction->font();
+        font.setBold(true);
+        instruction->setFont(font);
+
+
+        QGridLayout* grid = new QGridLayout();
+        int col = 0;
+
+        grid->addWidget(new QLabel("Core Segment ID"), 0, col);
+        grid->addWidget(new QLabel(QString::fromStdString(coreSegmentID.str()) + "/"), 1, col);
+        ++col;
+
+        grid->addWidget(new QLabel("Provider Segment Name"), 0, col);
+        grid->addWidget(_providerSegmentName, 1, col);
+        ++col;
+
+        grid->addWidget(new QLabel("/"), 1, col);
+        ++col;
+
+        grid->addWidget(new QLabel("Entity Name"), 0, col);
+        grid->addWidget(_entityName, 1, col);
+        ++col;
+
+
+        QVBoxLayout* layout = new QVBoxLayout();
+        setLayout(layout);
+        layout->addWidget(instruction);
+        layout->addLayout(grid);
+        layout->addWidget(_buttonBox);
+
+
+        connect(_buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
+        connect(_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
+
+        auto enableOkIfReady = [this]()
+        {
+            _buttonBox->button(QDialogButtonBox::Ok)
+                ->setEnabled(_providerSegmentName->text().size() > 0 and
+                             _entityName->text().size() > 0);
+        };
+        connect(_providerSegmentName, &QLineEdit::textChanged, this, enableOkIfReady);
+        connect(_entityName, &QLineEdit::textChanged, this, enableOkIfReady);
+
+        enableOkIfReady();
+    }
+
+
+    NewEntityIdDialog::~NewEntityIdDialog()
+    {
+    }
+
+
+    QString
+    NewEntityIdDialog::providerSegmentName() const
+    {
+        return _providerSegmentName->text();
+    }
+
+
+    QString
+    NewEntityIdDialog::entityName() const
+    {
+        return _entityName->text();
+    }
+
+
+    armem::MemoryID
+    NewEntityIdDialog::entityIDWithoutCoreSegmentID() const
+    {
+        armem::MemoryID id;
+        id.providerSegmentName = providerSegmentName().toStdString();
+        id.entityName = entityName().toStdString();
+        return id;
+    }
+
+    armem::MemoryID
+    NewEntityIdDialog::entityID() const
+    {
+        armem::MemoryID entityID = entityIDWithoutCoreSegmentID();
+        ARMARX_CHECK_NOT_NULL(coreSegmentID);
+        entityID.setCoreSegmentID(*coreSegmentID);
+        return entityID;
+    }
+
+} // namespace armarx::navigation::locgrapheditor
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/NewEntityIdDialog.h b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/NewEntityIdDialog.h
new file mode 100644
index 0000000000000000000000000000000000000000..2a0aad3d469e7116149b9389b5a734fa5a9fc97a
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/NewEntityIdDialog.h
@@ -0,0 +1,60 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <QDialog>
+#include <memory>
+
+#include <RobotAPI/libraries/armem/core/forward_declarations.h>
+
+
+class QLineEdit;
+class QDialogButtonBox;
+
+
+namespace armarx::navigation::locgrapheditor
+{
+    class NewEntityIdDialog : public QDialog
+    {
+    public:
+        NewEntityIdDialog(const armem::MemoryID& coreSegmentID, QWidget* parent = nullptr);
+        virtual ~NewEntityIdDialog() override;
+
+        QString providerSegmentName() const;
+        QString entityName() const;
+
+
+        armem::MemoryID entityIDWithoutCoreSegmentID() const;
+        armem::MemoryID entityID() const;
+
+
+    private:
+        QLineEdit* _providerSegmentName = nullptr;
+        QLineEdit* _entityName = nullptr;
+
+        QDialogButtonBox* _buttonBox = nullptr;
+
+
+        std::unique_ptr<armem::MemoryID> coreSegmentID;
+    };
+
+} // namespace armarx::navigation::locgrapheditor
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/RobotVisuWidget.cpp b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/RobotVisuWidget.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..afe0a4a15a9e983a2b1b347e0ef22bdb3b393c69
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/RobotVisuWidget.cpp
@@ -0,0 +1,234 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#include "RobotVisuWidget.h"
+
+#include <QCheckBox>
+#include <QDialog>
+#include <QDialogButtonBox>
+#include <QPushButton>
+#include <QVBoxLayout>
+
+#include <VirtualRobot/Robot.h>
+
+#include <ArmarXCore/core/ManagedIceObject.h>
+#include <ArmarXCore/core/exceptions/local/ExpressionException.h>
+
+#include <RobotAPI/components/ArViz/Client/Elements.h>
+#include <RobotAPI/interface/core/RobotState.h>
+#include <RobotAPI/libraries/core/remoterobot/RemoteRobot.h>
+
+#include "ConnectDialog.h"
+
+
+namespace armarx::navigation::locgrapheditor
+{
+
+    robotvisu::SettingsWidget::SettingsWidget(QWidget* parent) : QWidget(parent)
+    {
+        _enabled = new QCheckBox("Enable Visu", this);
+        _enabled->setChecked(true);
+
+        _collisionModel = new QCheckBox("Use Collision Model", this);
+        _collisionModel->setChecked(false);
+
+
+        setLayout(new QVBoxLayout());
+        layout()->addWidget(_enabled);
+        layout()->addWidget(_collisionModel);
+
+
+        connect(_enabled, &QCheckBox::toggled, this, &This::changed);
+        connect(_collisionModel, &QCheckBox::toggled, this, &This::changed);
+
+        const int margin = 0;
+        setContentsMargins(margin, margin, margin, margin);
+    }
+
+
+    bool
+    robotvisu::SettingsWidget::isEnabled() const
+    {
+        return _enabled->isChecked();
+    }
+
+
+    bool
+    robotvisu::SettingsWidget::useCollisionModel() const
+    {
+        return _collisionModel->isChecked();
+    }
+
+
+    robotvisu::Connection::Connection(RobotStateComponentInterfacePrx robotStateComponent,
+                                      robotvisu::SettingsWidget* settings) :
+        _robotStateComponent(
+            std::make_unique<RobotStateComponentInterfacePrx>(robotStateComponent)),
+        _settings(settings)
+    {
+        ARMARX_CHECK_NOT_NULL(robotStateComponent);
+        _filename = robotStateComponent->getRobotFilename();
+        _robot = RemoteRobot::createLocalClone(
+            robotStateComponent, "", {}, VirtualRobot::RobotIO::RobotDescription::eStructure);
+        ARMARX_CHECK_NOT_NULL(_robot);
+    }
+
+
+    robotvisu::Connection::~Connection()
+    {
+    }
+
+
+    Eigen::Matrix4f
+    robotvisu::Connection::getGlobalPose(bool synchronize)
+    {
+        _synchronize(synchronize);
+        return _robot->getGlobalPose();
+    }
+
+
+    std::map<std::string, float>
+    robotvisu::Connection::getJointValues(bool synchronize)
+    {
+        _synchronize(synchronize);
+        return _robot->getConfig()->getRobotNodeJointValueMap();
+    }
+
+
+    viz::Robot
+    robotvisu::Connection::vizRobot(const std::string& id, bool synchronize)
+    {
+        _synchronize(synchronize);
+        viz::Robot robot = viz::Robot(id)
+                               .file("", _filename)
+                               .pose(getGlobalPose(false))
+                               .joints(getJointValues(false));
+
+        if (_settings and _settings->useCollisionModel())
+        {
+            robot.useCollisionModel();
+        }
+        return robot;
+    }
+
+
+    RobotStateComponentInterfacePrx
+    robotvisu::Connection::getRobotStateComponent() const
+    {
+        ARMARX_CHECK_NOT_NULL(_robotStateComponent);
+        return *_robotStateComponent;
+    }
+
+
+    std::string
+    robotvisu::Connection::getConnectedName() const
+    {
+        return getRobotStateComponent()->ice_getIdentity().name;
+    }
+
+
+    void
+    robotvisu::Connection::_synchronize(bool synchronize)
+    {
+        if (synchronize)
+        {
+            RemoteRobot::synchronizeLocalClone(_robot, getRobotStateComponent());
+        }
+    }
+
+
+    RobotVisuWidget::RobotVisuWidget(ManagedIceObject& component, QWidget* parent) :
+        QWidget(parent), _component(component)
+    {
+        _statusLabel = new QLabel("Not connected", this);
+        _connectButton = new QPushButton("Connect ...", this);
+
+        _settings = new robotvisu::SettingsWidget();
+        _settings->setEnabled(false);
+
+
+        QBoxLayout* layout = new QVBoxLayout(this);
+        setLayout(layout);
+
+        layout->addWidget(_statusLabel);
+        layout->addWidget(_connectButton);
+        layout->addWidget(_settings);
+
+
+        connect(_connectButton, &QPushButton::pressed, this, &This::connectToRobot);
+
+        connect(_settings, &robotvisu::SettingsWidget::changed, this, &This::settingsChanged);
+
+        connect(this,
+                &This::connected,
+                this,
+                [this]()
+                {
+                    QString name = QString::fromStdString(connection().getConnectedName());
+                    _statusLabel->setText("Connected to '" + name + "'");
+                });
+        connect(this, &This::connected, this, [this]() { _settings->setEnabled(true); });
+    }
+
+
+    bool
+    RobotVisuWidget::isEnabled() const
+    {
+        return isConnected() and _settings->isEnabled();
+    }
+
+
+    bool
+    RobotVisuWidget::isConnected() const
+    {
+        return _connection.has_value();
+    }
+
+
+    robotvisu::Connection&
+    RobotVisuWidget::connection()
+    {
+        return _connection.value();
+    }
+
+
+    void
+    RobotVisuWidget::connectToRobot()
+    {
+        ConnectDialog<RobotStateComponentInterfacePrx> dialog(
+            "*RobotStateComponent", _component, this);
+        switch (dialog.exec())
+        {
+            case QDialog::Accepted:
+                break;
+            case QDialog::Rejected:
+                return;
+        }
+
+        auto robotStateComponent = dialog.getProxy();
+        if (robotStateComponent)
+        {
+            _connection.emplace(robotStateComponent, _settings);
+            emit connected();
+        }
+    }
+
+} // namespace armarx::navigation::locgrapheditor
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/RobotVisuWidget.h b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/RobotVisuWidget.h
new file mode 100644
index 0000000000000000000000000000000000000000..1a7083dc58fd37deb1bd8d09d322b5c96dc10e59
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/RobotVisuWidget.h
@@ -0,0 +1,159 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <QWidget>
+#include <map>
+#include <memory>
+#include <optional>
+#include <string>
+
+#include <Eigen/Core>
+
+
+class QCheckBox;
+class QLabel;
+class QPushButton;
+
+
+namespace VirtualRobot
+{
+    using RobotPtr = std::shared_ptr<class Robot>;
+}
+namespace IceInternal
+{
+    template <typename T>
+    class ProxyHandle;
+}
+namespace IceProxy::armarx
+{
+    class RobotStateComponentInterface;
+}
+namespace armarx
+{
+    class ManagedIceObject;
+    using RobotStateComponentInterfacePrx =
+        ::IceInternal::ProxyHandle<::IceProxy::armarx::RobotStateComponentInterface>;
+} // namespace armarx
+namespace armarx::viz
+{
+    class Robot;
+}
+
+
+namespace armarx::navigation::locgrapheditor::robotvisu
+{
+
+    class SettingsWidget : public QWidget
+    {
+        Q_OBJECT
+        using This = SettingsWidget;
+
+    public:
+        SettingsWidget(QWidget* parent = nullptr);
+
+
+        bool isEnabled() const;
+        bool useCollisionModel() const;
+
+
+    signals:
+
+        void changed();
+
+
+    private:
+        QCheckBox* _enabled = nullptr;
+        QCheckBox* _collisionModel = nullptr;
+    };
+
+
+    class Connection
+    {
+    public:
+        Connection(RobotStateComponentInterfacePrx robotStateComponent,
+                   robotvisu::SettingsWidget* settings);
+        ~Connection();
+
+
+        RobotStateComponentInterfacePrx getRobotStateComponent() const;
+        std::string getConnectedName() const;
+
+
+        Eigen::Matrix4f getGlobalPose(bool synchronize = true);
+        std::map<std::string, float> getJointValues(bool synchronize = true);
+
+        viz::Robot vizRobot(const std::string& id, bool synchronize = true);
+
+
+    private:
+        void _synchronize(bool synchronize);
+
+        std::unique_ptr<RobotStateComponentInterfacePrx> _robotStateComponent;
+        robotvisu::SettingsWidget* _settings = nullptr;
+
+        std::string _filename;
+        VirtualRobot::RobotPtr _robot;
+    };
+
+} // namespace armarx::navigation::locgrapheditor::robotvisu
+
+namespace armarx::navigation::locgrapheditor
+{
+
+    class RobotVisuWidget : public QWidget
+    {
+        Q_OBJECT
+        using This = RobotVisuWidget;
+
+    public:
+        RobotVisuWidget(ManagedIceObject& component, QWidget* parent = nullptr);
+
+
+        /// Indicates whether the connection is established and visu is enabled.
+        bool isEnabled() const;
+
+        bool isConnected() const;
+        robotvisu::Connection& connection();
+
+
+    signals:
+
+        void connected();
+        void settingsChanged();
+
+
+    public slots:
+
+        void connectToRobot();
+
+
+    private:
+        ManagedIceObject& _component;
+        std::optional<robotvisu::Connection> _connection;
+
+        QLabel* _statusLabel = nullptr;
+        QPushButton* _connectButton = nullptr;
+        robotvisu::SettingsWidget* _settings = nullptr;
+    };
+
+} // namespace armarx::navigation::locgrapheditor
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/VertexDataWidget.cpp b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/VertexDataWidget.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f1a25de4940dc40e54b9b5fcbcead1faa92dca75
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/VertexDataWidget.cpp
@@ -0,0 +1,384 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package    MemoryX::ArmarXObjects::GraphImportExport
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#include "VertexDataWidget.h"
+
+#include <QDoubleSpinBox>
+#include <QFormLayout>
+#include <QHBoxLayout>
+#include <QLineEdit>
+#include <QList>
+#include <QPushButton>
+#include <QRadioButton>
+#include <QSignalBlocker>
+
+#include <SimoxUtility/math/convert/deg_to_rad.h>
+#include <SimoxUtility/math/convert/mat4f_to_rpy.h>
+#include <SimoxUtility/math/convert/rad_to_deg.h>
+#include <SimoxUtility/math/convert/rpy_to_mat4f.h>
+#include <SimoxUtility/math/pose/pose.h>
+
+#include <RobotAPI/libraries/armem/core/MemoryID.h>
+#include <RobotAPI/libraries/armem/core/aron_conversions.h>
+#include <RobotAPI/libraries/aron/common/aron_conversions/core.h>
+
+#include "RobotVisuWidget.h"
+
+
+namespace armarx::navigation::locgrapheditor
+{
+
+    VertexDataWidget::VertexDataWidget()
+    {
+        setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);
+
+        name = new QLineEdit(this);
+        name->setReadOnly(true);
+        locationID = new QLineEdit(this);
+        locationID->setReadOnly(true);
+
+        frame = new QLineEdit(this);
+
+        x = new QDoubleSpinBox(this);
+        y = new QDoubleSpinBox(this);
+        z = new QDoubleSpinBox(this);
+
+        roll = new QDoubleSpinBox(this);
+        pitch = new QDoubleSpinBox(this);
+        yaw = new QDoubleSpinBox(this);
+
+        angleUnitDeg = new QRadioButton("Degrees");
+        angleUnitRad = new QRadioButton("Radians");
+
+        _useCurrentRobotPose = new QPushButton("Use Current Robot Pose");
+        _useCurrentRobotPose->setEnabled(false);
+        _useCurrentRobotPose->setToolTip(
+            "Use the robot's current global pose (requires connection to robot state).");
+
+
+        for (QDoubleSpinBox* pos : _positionSpinBoxes())
+        {
+            pos->setSuffix(" mm");
+            pos->setMinimum(-1e6);
+            pos->setMaximum(+1e6);
+            pos->setSingleStep(50);
+        }
+        for (QDoubleSpinBox* spinBox : _allSpinBoxes())
+        {
+            spinBox->setValue(0);
+            spinBox->setAlignment(Qt::AlignRight);
+        }
+
+        QHBoxLayout* angleUnitLayout = new QHBoxLayout();
+        for (QRadioButton* angleUnit : {angleUnitDeg, angleUnitRad})
+        {
+            angleUnitLayout->addWidget(angleUnit);
+        }
+
+        QFormLayout* layout = new QFormLayout();
+        this->setLayout(layout);
+
+        layout->addRow("Name", name);
+        layout->addRow("Entity ID", locationID);
+        layout->addRow("Frame", frame);
+        layout->addRow("X", x);
+        layout->addRow("Y", y);
+        layout->addRow("Z", z);
+        layout->addRow("Roll", roll);
+        layout->addRow("Pitch", pitch);
+        layout->addRow("Yaw", yaw);
+        layout->addRow("Angle Units", angleUnitLayout);
+        layout->addRow(_useCurrentRobotPose);
+
+
+        // Connect
+
+        for (QRadioButton* angleUnit : {angleUnitDeg, angleUnitRad})
+        {
+            connect(angleUnit, &QRadioButton::toggled, this, &This::_updateAngleUnit);
+        }
+
+        connect(frame, &QLineEdit::editingFinished, this, &This::_updateVertexAttribs);
+        for (QDoubleSpinBox* spinBox : _allSpinBoxes())
+        {
+            connect(spinBox,
+                    QOverload<qreal>::of(&QDoubleSpinBox::valueChanged),
+                    this,
+                    &This::_updateVertexAttribs);
+        }
+
+        connect(_useCurrentRobotPose, &QPushButton::pressed, this, &This::_setFromCurrentRobotPose);
+
+
+        angleUnitDeg->click();
+    }
+
+
+    std::optional<GuiGraph::Vertex>
+    VertexDataWidget::vertex()
+    {
+        return _vertex;
+    }
+
+
+    void
+    VertexDataWidget::setVertex(GuiGraph::Vertex vertex)
+    {
+        QSignalBlocker blocker(this);
+
+        _vertex = vertex;
+        _setFromVertex(vertex);
+        setEnabled(true);
+    }
+
+
+    void
+    VertexDataWidget::clearVertex()
+    {
+        _vertex = std::nullopt;
+        setEnabled(false);
+    }
+
+
+    Eigen::Vector3d
+    VertexDataWidget::xyz() const
+    {
+        return {x->value(), y->value(), z->value()};
+    }
+
+
+    Eigen::Vector3d
+    VertexDataWidget::rpyDeg() const
+    {
+        Eigen::Vector3d raw = _rpyRaw();
+        if (angleUnitRad->isChecked())
+        {
+            return {}; // return simox::math::rad_to_deg(raw);
+        }
+        else
+        {
+            return raw;
+        }
+    }
+
+
+    Eigen::Vector3d
+    VertexDataWidget::rpyRad() const
+    {
+        Eigen::Vector3d raw = _rpyRaw();
+        if (angleUnitDeg->isChecked())
+        {
+            return simox::math::deg_to_rad(raw);
+        }
+        else
+        {
+            return raw;
+        }
+    }
+
+
+    void
+    VertexDataWidget::setRobotConnection(robotvisu::Connection* connection)
+    {
+        this->_robotConnection = connection;
+        _useCurrentRobotPose->setEnabled(_robotConnection != nullptr);
+    }
+
+
+    void
+    VertexDataWidget::_setFromVertex(const GuiGraph::Vertex& vertex)
+    {
+        const VertexData& attrib = vertex.attrib();
+
+        name->setText(QString::fromStdString(attrib.getName()));
+        locationID->setText(
+            QString::fromStdString(aron::fromAron<armem::MemoryID>(attrib.aron.locationID).str()));
+        frame->setText("<WIP>");
+        _setPose(attrib.getPose().cast<qreal>());
+    }
+
+
+    void
+    VertexDataWidget::_getToVertex(GuiGraph::Vertex& vertex)
+    {
+        VertexData& attrib = vertex.attrib();
+
+        // name is read-only
+        // locationID is read-only
+        // frame->setText("<WIP>");  // WIP
+
+        Eigen::Matrix4d pose = simox::math::pose(xyz(), simox::math::rpy_to_mat3f(rpyRad()));
+        attrib.setPose(pose.cast<float>());
+    }
+
+
+    void
+    VertexDataWidget::_updateAngleUnit()
+    {
+        std::function<double(double)> convertValue;
+        QString suffix = " \u00b0";
+        double min = -360;
+        double max = +360;
+        double step = 5.;
+        int decimals = 2;
+
+        if (angleUnitRad->isChecked())
+        {
+            convertValue = [](double deg) { return simox::math::deg_to_rad(deg); };
+            suffix = " rad";
+            min = simox::math::deg_to_rad(min);
+            max = simox::math::deg_to_rad(max);
+            step = simox::math::deg_to_rad(step);
+            decimals = 3;
+        }
+        else
+        {
+            ARMARX_CHECK(angleUnitDeg->isChecked());
+            convertValue = [](double rad) { return simox::math::rad_to_deg(rad); };
+        }
+
+        std::vector<QSignalBlocker> blockers;
+        for (QDoubleSpinBox* angle : _angleSpinBoxes())
+        {
+            blockers.emplace_back(angle);
+
+            angle->setSuffix(suffix);
+            angle->setMinimum(min);
+            angle->setMaximum(max);
+            angle->setSingleStep(step);
+            angle->setDecimals(decimals);
+        }
+        if (_vertex.has_value())
+        {
+            _setRpyRad(simox::math::mat4f_to_rpy(_vertex->attrib().getPose().cast<qreal>()));
+        }
+
+        // blockers will disable blocking for all spin boxes on destruction.
+    }
+
+
+    void
+    VertexDataWidget::_updateVertexAttribs()
+    {
+        if (not signalsBlocked() and _vertex.has_value())
+        {
+            _getToVertex(_vertex.value());
+            _vertex->attrib().changed = true;
+            emit vertexDataChanged();
+        }
+    }
+
+
+    std::vector<QDoubleSpinBox*>
+    VertexDataWidget::_positionSpinBoxes()
+    {
+        return {x, y, z};
+    }
+
+
+    std::vector<QDoubleSpinBox*>
+    VertexDataWidget::_angleSpinBoxes()
+    {
+        return {roll, pitch, yaw};
+    }
+
+
+    std::vector<QDoubleSpinBox*>
+    VertexDataWidget::_allSpinBoxes()
+    {
+        return {x, y, z, roll, pitch, yaw};
+    }
+
+
+    void
+    VertexDataWidget::_setPose(const Eigen::Matrix4d& pose)
+    {
+        _setXyz(simox::math::position(pose));
+        _setRpyRad(simox::math::mat4f_to_rpy(pose));
+    }
+
+
+    void
+    VertexDataWidget::_setXyz(const Eigen::Vector3d& xyz)
+    {
+        x->setValue(xyz.x());
+        y->setValue(xyz.y());
+        z->setValue(xyz.z());
+    }
+
+
+    Eigen::Vector3d
+    VertexDataWidget::_rpyRaw() const
+    {
+        return {roll->value(), pitch->value(), yaw->value()};
+    }
+
+
+    void
+    VertexDataWidget::_setRpyRaw(const Eigen::Vector3d& rpy) const
+    {
+        roll->setValue(rpy(0));
+        pitch->setValue(rpy(1));
+        yaw->setValue(rpy(2));
+    }
+
+
+    void
+    VertexDataWidget::_setRpyDeg(const Eigen::Vector3d& rpyDeg) const
+    {
+        if (angleUnitDeg->isChecked())
+        {
+            _setRpyRaw(rpyDeg);
+        }
+        else if (angleUnitRad->isChecked())
+        {
+            _setRpyRaw(simox::math::deg_to_rad(rpyDeg));
+        }
+    }
+
+
+    void
+    VertexDataWidget::_setRpyRad(const Eigen::Vector3d& rpyRad) const
+    {
+        if (angleUnitDeg->isChecked())
+        {
+            _setRpyRaw(simox::math::rad_to_deg(rpyRad));
+        }
+        else if (angleUnitRad->isChecked())
+        {
+            _setRpyRaw(rpyRad);
+        }
+    }
+
+
+    void
+    VertexDataWidget::_setFromCurrentRobotPose()
+    {
+        ARMARX_CHECK_NOT_NULL(_robotConnection);
+        {
+            QSignalBlocker blocker(this);
+            _setPose(_robotConnection->getGlobalPose().cast<qreal>());
+        }
+        _updateVertexAttribs();
+    }
+
+} // namespace armarx::navigation::locgrapheditor
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/VertexDataWidget.h b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/VertexDataWidget.h
new file mode 100644
index 0000000000000000000000000000000000000000..6b5c2587f0ce7dd233310a27cde2f0cb159fd085
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/VertexDataWidget.h
@@ -0,0 +1,120 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <QWidget>
+
+#include <Eigen/Core>
+
+#include <armarx/navigation/gui-plugins/LocationGraphEditor/GuiGraph.h>
+
+
+class QLineEdit;
+class QDoubleSpinBox;
+class QPushButton;
+class QRadioButton;
+
+
+namespace armarx::navigation::locgrapheditor::robotvisu
+{
+    class Connection;
+}
+namespace armarx::navigation::locgrapheditor
+{
+
+    class VertexDataWidget : public QWidget
+    {
+        Q_OBJECT
+        using This = VertexDataWidget;
+
+
+    public:
+        VertexDataWidget();
+
+
+        std::optional<GuiGraph::Vertex> vertex();
+        void setVertex(GuiGraph::Vertex vertex);
+        void clearVertex();
+
+        Eigen::Vector3d xyz() const;
+        Eigen::Vector3d rpyDeg() const;
+        Eigen::Vector3d rpyRad() const;
+
+
+        void setRobotConnection(robotvisu::Connection* connection);
+
+
+    signals:
+
+        void vertexDataChanged();
+
+
+    private slots:
+
+        void _updateAngleUnit();
+        void _updateVertexAttribs();
+
+
+    private:
+        void _setFromVertex(const GuiGraph::Vertex& vertex);
+        void _getToVertex(GuiGraph::Vertex& vertex);
+
+        std::vector<QDoubleSpinBox*> _positionSpinBoxes();
+        std::vector<QDoubleSpinBox*> _angleSpinBoxes();
+        std::vector<QDoubleSpinBox*> _allSpinBoxes();
+
+        void _setPose(const Eigen::Matrix4d& pose);
+        void _setXyz(const Eigen::Vector3d& xyz);
+
+        Eigen::Vector3d _rpyRaw() const;
+        void _setRpyRaw(const Eigen::Vector3d& rpy) const;
+        void _setRpyDeg(const Eigen::Vector3d& rpyDeg) const;
+        void _setRpyRad(const Eigen::Vector3d& rpyRad) const;
+
+        void _setFromCurrentRobotPose();
+
+
+    private:
+        std::optional<GuiGraph::Vertex> _vertex;
+
+
+        QLineEdit* name = nullptr;
+        QLineEdit* locationID = nullptr;
+        QLineEdit* frame = nullptr;
+
+        QDoubleSpinBox* x = nullptr;
+        QDoubleSpinBox* y = nullptr;
+        QDoubleSpinBox* z = nullptr;
+
+        QDoubleSpinBox* roll = nullptr;
+        QDoubleSpinBox* pitch = nullptr;
+        QDoubleSpinBox* yaw = nullptr;
+
+        QRadioButton* angleUnitDeg = nullptr;
+        QRadioButton* angleUnitRad = nullptr;
+
+
+        robotvisu::Connection* _robotConnection = nullptr;
+        QPushButton* _useCurrentRobotPose = nullptr;
+    };
+
+} // namespace armarx::navigation::locgrapheditor
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/VertexTableWidget.cpp b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/VertexTableWidget.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..ecaaca3c7c11a4a690d7e7258de8d82fa767097b
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/VertexTableWidget.cpp
@@ -0,0 +1,308 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#include "VertexTableWidget.h"
+
+#include <QAction>
+#include <QHeaderView>
+#include <QMenu>
+
+#include <ArmarXCore/core/exceptions/local/ExpressionException.h>
+#include <ArmarXCore/core/logging/Logging.h>
+
+#include "utils.h"
+
+
+namespace armarx::navigation::locgrapheditor
+{
+
+    VertexTableWidget::VertexTableWidget()
+    {
+        QStringList columns{"Name", "X [mm]", "Y [mm]", "Yaw [\u00b0]"};
+        setColumnCount(columns.size());
+        setHorizontalHeaderLabels(columns);
+        horizontalHeader()->setResizeMode(0, QHeaderView::Stretch);
+        horizontalHeader()->setVisible(true);
+
+        setEditTriggers(QAbstractItemView::NoEditTriggers);
+        setSortingEnabled(true);
+
+        setAlternatingRowColors(true);
+
+        QString styleSheet = this->styleSheet();
+        styleSheet = styleSheet + "\n" + "selection-background-color: #FF8000;";
+        setStyleSheet(styleSheet);
+
+        setContextMenuPolicy(Qt::CustomContextMenu);
+        connect(this, &This::customContextMenuRequested, this, &This::makeContextMenu);
+    }
+
+
+    QTableWidgetItem*
+    VertexTableWidget::addVertex()
+    {
+        // We need to disable sorting to prevent the new row from being moved away.
+        setSortingEnabled(false);
+
+        QTableWidgetItem* result = nullptr;
+        {
+            int row = rowCount();
+            setRowCount(row + 1);
+
+            for (int col = 0; col < 4; ++col)
+            {
+                // Just fill with vanilla items, they will get values in the update.
+                QTableWidgetItem* item = new QTableWidgetItem{};
+                setItem(row, col, item);
+
+                if (col >= 1)
+                {
+                    item->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
+                }
+                if (col == 0)
+                {
+                    result = item;
+                }
+            }
+        }
+
+        // Enable sorting - `row` could now be moved away.
+        setSortingEnabled(true);
+
+        ARMARX_CHECK_NOT_NULL(result);
+        return result;
+    }
+
+
+    void
+    VertexTableWidget::updateVertex(GuiGraph::Vertex vertex)
+    {
+        const Eigen::Matrix4d pose = vertex.attrib().getPose().cast<qreal>();
+        char format = 'f';
+        const int precision = 2;
+
+        // Changing the values may trigger a re-sort.
+        setSortingEnabled(false);
+
+
+        int row = this->row(vertex.attrib().tableWidgetItem);
+
+        QString displayName = QString::fromStdString(vertex.attrib().getName());
+        if (vertex.attrib().changed)
+        {
+            displayName += "*";
+        }
+        item(row, 0)->setText(displayName);
+        item(row, 0)->setData(Qt::UserRole, QString::fromStdString(vertex.attrib().getName()));
+        item(row, 1)->setText(QString::number(pose(0, 3), format, precision));
+        item(row, 2)->setText(QString::number(pose(1, 3), format, precision));
+        item(row, 3)->setText(QString::number(getYawAngleDegree(pose), format, precision));
+
+        setSortingEnabled(true);
+
+        QColor bgColor = vertex.attrib().highlighted ? bgColorSelected : bgColorDefault;
+        QFont font;
+        font.setBold(vertex.attrib().highlighted);
+        for (int col = 0; col < 4; ++col)
+        {
+            auto* item = this->item(row, col);
+            ARMARX_CHECK_NOT_NULL(item);
+
+            item->setData(Qt::BackgroundRole, bgColor);
+            item->setFont(font);
+        }
+    }
+
+
+    void
+    VertexTableWidget::removeVertex(GuiGraph::Vertex& vertex)
+    {
+        if (currentItem() == vertex.attrib().tableWidgetItem)
+        {
+            setCurrentItem(nullptr);
+        }
+
+        removeRow(row(vertex.attrib().tableWidgetItem));
+        vertex.attrib().tableWidgetItem = nullptr;
+    }
+
+
+    QList<QTableWidgetItem*>
+    VertexTableWidget::selectedVertexItems()
+    {
+        return utils::getSelectedItemsOfColumn(this, 0);
+    }
+
+
+    QString
+    VertexTableWidget::_nameOf(QTableWidgetItem* item)
+    {
+        return item->data(Qt::UserRole).toString();
+    }
+
+
+    void
+    VertexTableWidget::makeContextMenu(QPoint pos)
+    {
+        QList<QTableWidgetItem*> items = selectedVertexItems();
+
+        QMenu menu;
+        if (items.size() == 0)
+        {
+            QAction* action = menu.addSection("No locations selected");
+            action->setEnabled(false);
+        }
+
+        // Partners selected
+        if (items.size() == 2)
+        {
+            menu.addSection("Selected pair of locations");
+
+            // Generate actions for connecting these two nodes.
+            std::sort(items.begin(),
+                      items.end(),
+                      [](QTableWidgetItem* first, QTableWidgetItem* second)
+                      { return _nameOf(first) < _nameOf(second); });
+            QTableWidgetItem* first = items[0];
+            QTableWidgetItem* second = items[1];
+
+            connect(menu.addAction("Add edge '" + _nameOf(first) + "' " + utils::arrowRight + " '" +
+                                   _nameOf(second) + "'"),
+                    &QAction::triggered,
+                    [this, first, second]() {
+                        emit newEdgesRequested({{first, second}});
+                    });
+            connect(menu.addAction("Add edge '" + _nameOf(first) + "' " + utils::arrowLeft + " '" +
+                                   _nameOf(second) + "'"),
+                    &QAction::triggered,
+                    [this, first, second]() {
+                        emit newEdgesRequested({{second, first}});
+                    });
+            connect(menu.addAction("Add edges '" + _nameOf(first) + "' " + utils::arrowBoth + " '" +
+                                   _nameOf(second) + "'"),
+                    &QAction::triggered,
+                    [this, first, second]() {
+                        emit newEdgesRequested({{first, second}, {second, first}});
+                    });
+        }
+
+        // Partners via context menu
+        if (items.size() > 0)
+        {
+            QString edges = items.size() == 1 ? "edge" : "edges";
+            QString desc = items.size() == 1 ? "'" + _nameOf(items[0]) + "'"
+                                             : QString::number(items.size()) + " locations";
+
+            if (items.size() == 1)
+            {
+                // QAction* deleteAction = menu.addAction("Delete location '" + );
+                menu.addSection("Selected single location " + desc);
+            }
+            else
+            {
+                menu.addSection("Selected bulk of " + desc);
+            }
+
+            using Item = QTableWidgetItem;
+            using ListOfEdges = QList<QPair<Item*, Item*>>;
+            using AppendFunc =
+                std::function<void(ListOfEdges & edges, Item * selected, Item * action)>;
+
+            auto addBulkAddEdgeActions = [this, &items](QMenu* submenu, AppendFunc appendFunc)
+            {
+                if (items.size() == rowCount())
+                {
+                    QAction* a = submenu->addAction("No other locations");
+                    a->setDisabled(true);
+                }
+                for (int row = 0; row < rowCount(); ++row)
+                {
+                    QTableWidgetItem* action = this->item(row, 0);
+                    if (items.count(action) == 0) // Do no generate self-edges
+                    {
+                        QAction* a = submenu->addAction(_nameOf(action));
+                        connect(a,
+                                &QAction::triggered,
+                                this,
+                                [this, items, action, appendFunc]()
+                                {
+                                    QList<QPair<QTableWidgetItem*, QTableWidgetItem*>> edges;
+                                    for (auto* selected : items)
+                                    {
+                                        appendFunc(edges, selected, action);
+                                    }
+                                    emit newEdgesRequested(edges);
+                                });
+                    }
+                }
+            };
+
+            addBulkAddEdgeActions(
+                menu.addMenu("Add " + edges + " " + desc + " " + utils::arrowRight + " ..."),
+                [](ListOfEdges& edges, Item* selected, Item* action) {
+                    edges.append({selected, action});
+                });
+            addBulkAddEdgeActions(
+                menu.addMenu("Add " + edges + " " + desc + " " + utils::arrowLeft + " ..."),
+                [](ListOfEdges& edges, Item* selected, Item* action) {
+                    edges.append({action, selected});
+                });
+            addBulkAddEdgeActions(
+                menu.addMenu("Add " + edges + " " + desc + " " + utils::arrowBoth + " ..."),
+                [](ListOfEdges& edges, Item* selected, Item* action)
+                {
+                    edges.append({selected, action});
+                    edges.append({action, selected});
+                });
+
+
+            auto connectBulkRemoveEdgeAction =
+                [this, &items](QAction* action, utils::EdgeDirection edgeDirection)
+            {
+                connect(action,
+                        &QAction::triggered,
+                        this,
+                        [this, items, edgeDirection]()
+                        { emit edgeRemovalRequested(items, edgeDirection); });
+            };
+
+            connectBulkRemoveEdgeAction(
+                menu.addAction("Remove all edges " + utils::arrowRight + " " + desc),
+                utils::EdgeDirection::To);
+            connectBulkRemoveEdgeAction(
+                menu.addAction("Remove all edges " + utils::arrowLeft + " " + desc),
+                utils::EdgeDirection::From);
+            connectBulkRemoveEdgeAction(
+                menu.addAction("Remove all edges " + utils::arrowBoth + " " + desc),
+                utils::EdgeDirection::Bidirectional);
+        }
+
+
+        menu.addSection("Manage Locations");
+        connect(menu.addAction("Create new location ..."),
+                &QAction::triggered,
+                this,
+                [this]() { emit newVertexRequested(); });
+
+        menu.exec(mapToGlobal(pos));
+    }
+
+} // namespace armarx::navigation::locgrapheditor
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/VertexTableWidget.h b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/VertexTableWidget.h
new file mode 100644
index 0000000000000000000000000000000000000000..0ce3c98da3cf8fcabdc31672faf426d9b69dd174
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/VertexTableWidget.h
@@ -0,0 +1,82 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <QColor>
+#include <QList>
+#include <QPair>
+#include <QTableWidget>
+
+#include <armarx/navigation/gui-plugins/LocationGraphEditor/GuiGraph.h>
+#include <armarx/navigation/gui-plugins/LocationGraphEditor/widgets/default_colors.h>
+
+
+namespace armarx::navigation::locgrapheditor::utils
+{
+    enum class EdgeDirection;
+}
+namespace armarx::navigation::locgrapheditor
+{
+    class VertexTableWidget : public QTableWidget
+    {
+        Q_OBJECT
+        using This = VertexTableWidget;
+
+
+    public:
+        VertexTableWidget();
+
+
+        QTableWidgetItem* addVertex();
+
+        void updateVertex(GuiGraph::Vertex vertex);
+
+        void removeVertex(GuiGraph::Vertex& vertex);
+
+
+        QList<QTableWidgetItem*> selectedVertexItems();
+
+
+    signals:
+
+        void newVertexRequested();
+
+        void newEdgesRequested(QList<QPair<QTableWidgetItem*, QTableWidgetItem*>> edges);
+        void edgeRemovalRequested(QList<QTableWidgetItem*> vertexItems,
+                                  utils::EdgeDirection direction);
+
+
+    public slots:
+
+        void makeContextMenu(QPoint pos);
+
+
+    private:
+        static QString _nameOf(QTableWidgetItem* item);
+
+
+    public:
+        QColor bgColorDefault = default_colors::tableBackgroundDefault;
+        QColor bgColorSelected = default_colors::tableBackgroundSelected;
+    };
+
+} // namespace armarx::navigation::locgrapheditor
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/default_colors.cpp b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/default_colors.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..130485d76bfa3acb10bb04688e1a0fdf4112b224
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/default_colors.cpp
@@ -0,0 +1,33 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#include "default_colors.h"
+
+#include <QColor>
+
+
+namespace armarx::navigation::locgrapheditor
+{
+
+    const QColor default_colors::tableBackgroundDefault = QColor::fromRgb(255, 255, 255);
+    const QColor default_colors::tableBackgroundSelected = QColor::fromRgb(255, 210, 160);
+
+} // namespace armarx::navigation::locgrapheditor
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/default_colors.h b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/default_colors.h
new file mode 100644
index 0000000000000000000000000000000000000000..e8067a33da5a8a260dcc57e93a379a9b5023dfb1
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/default_colors.h
@@ -0,0 +1,33 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+class QColor;
+
+
+namespace armarx::navigation::locgrapheditor::default_colors
+{
+
+    extern const QColor tableBackgroundDefault;
+    extern const QColor tableBackgroundSelected;
+
+} // namespace armarx::navigation::locgrapheditor::default_colors
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/graph_scene.h b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/graph_scene.h
new file mode 100644
index 0000000000000000000000000000000000000000..5baadfdea87ef21728bbd291e7aba2c155b3f030
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/graph_scene.h
@@ -0,0 +1,16 @@
+#pragma once
+
+
+namespace armarx::navigation::locgrapheditor::graph_scene
+{
+    class Scene;
+    class Widget;
+} // namespace armarx::navigation::locgrapheditor::graph_scene
+
+namespace armarx::navigation::locgrapheditor
+{
+
+    using GraphScene = graph_scene::Scene;
+    using GraphSceneWidget = graph_scene::Widget;
+
+} // namespace armarx::navigation::locgrapheditor
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/graph_scene/ControlWidget.cpp b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/graph_scene/ControlWidget.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..ee81158baca7ac20a370ac91d487c97a92d5653f
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/graph_scene/ControlWidget.cpp
@@ -0,0 +1,113 @@
+#include "ControlWidget.h"
+
+// #include <ArmarXCore/core/logging/Logging.h>
+
+#include <QDoubleSpinBox>
+#include <QFrame>
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QPushButton>
+#include <cmath>
+
+
+namespace armarx::navigation::locgrapheditor::graph_scene
+{
+
+    ControlWidget::ControlWidget()
+    {
+        _angle.turnClockwise = new QPushButton("\u21bb"); // https://unicode-table.com/de/21BB/
+        _angle.turnCounterClockwise =
+            new QPushButton("\u21ba"); // https://unicode-table.com/de/21BA/
+        std::vector<QPushButton*> turnButtons;
+        for (QPushButton* button : turnButtons)
+        {
+            button->setMinimumWidth(button->minimumHeight());
+            button->setMaximumWidth(button->maximumHeight());
+            button->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed);
+        }
+
+        _angle.value = new QDoubleSpinBox();
+        _angle.value->setMinimum(-360);
+        _angle.value->setMaximum(360);
+        _angle.value->setSingleStep(15);
+        _angle.value->setValue(0);
+
+        _zoom.value = new QDoubleSpinBox();
+        _zoom.value->setDecimals(5);
+        _zoom.value->setMinimum(1e-6);
+        _zoom.value->setSingleStep(0.01);
+        _zoom.value->setValue(0.1);
+        _zoom.audoAdjust = new QPushButton("Auto");
+
+
+        QHBoxLayout* layout = new QHBoxLayout();
+        this->setLayout(layout);
+
+        layout->addWidget(new QLabel("Angle: "));
+        layout->addWidget(_angle.turnClockwise);
+        layout->addWidget(_angle.value);
+        layout->addWidget(_angle.turnCounterClockwise);
+
+        // Vertical separator line.
+        QFrame* line = new QFrame();
+        line->setFrameShape(QFrame::VLine);
+        line->setFrameShadow(QFrame::Sunken);
+        layout->addWidget(line);
+
+        layout->addWidget(new QLabel("Zoom: "));
+        layout->addWidget(_zoom.value);
+        layout->addWidget(_zoom.audoAdjust);
+
+
+        // Connect
+
+        connect(_angle.value,
+                QOverload<double>::of(&QDoubleSpinBox::valueChanged),
+                this,
+                &This::angleChanged);
+
+        connect(_angle.turnClockwise,
+                &QPushButton::pressed,
+                [this]()
+                {
+                    qreal newangle = std::fmod(angle() + _angle.rotateStepSize, 360.0);
+                    setAngle(newangle);
+                });
+        connect(_angle.turnCounterClockwise,
+                &QPushButton::pressed,
+                [this]() { setAngle(std::fmod(angle() - _angle.rotateStepSize, 360.0)); });
+
+        connect(_zoom.value,
+                QOverload<double>::of(&QDoubleSpinBox::valueChanged),
+                this,
+                &This::angleChanged);
+        connect(_zoom.audoAdjust, &QPushButton::pressed, this, &This::zoomAutoAdjustRequested);
+    }
+
+
+    qreal
+    ControlWidget::angle()
+    {
+        return _angle.value->value();
+    }
+
+    void
+    ControlWidget::setAngle(qreal angle)
+    {
+        _angle.value->setValue(angle);
+    }
+
+
+    qreal
+    ControlWidget::zoom()
+    {
+        return _zoom.value->value();
+    }
+
+    void
+    ControlWidget::setZoom(qreal zoom)
+    {
+        _zoom.value->setValue(zoom);
+    }
+
+} // namespace armarx::navigation::locgrapheditor::graph_scene
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/graph_scene/ControlWidget.h b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/graph_scene/ControlWidget.h
new file mode 100644
index 0000000000000000000000000000000000000000..7a2af02426b15691a2f5f2a1f91c8dc2262908a2
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/graph_scene/ControlWidget.h
@@ -0,0 +1,55 @@
+#pragma once
+
+#include <QWidget>
+
+class QDoubleSpinBox;
+class QHBoxLayout;
+class QPushButton;
+
+
+namespace armarx::navigation::locgrapheditor::graph_scene
+{
+
+    class ControlWidget : public QWidget
+    {
+        Q_OBJECT
+        using This = ControlWidget;
+
+
+    public:
+        ControlWidget();
+
+        qreal angle();
+        void setAngle(qreal angle);
+        qreal zoom();
+        void setZoom(qreal zoom);
+
+
+    signals:
+
+        void angleChanged(qreal value);
+
+        void zoomChanged(qreal value);
+        void zoomAutoAdjustRequested();
+
+
+    public:
+        struct Angle
+        {
+            QPushButton* turnClockwise = nullptr;
+            QDoubleSpinBox* value = nullptr;
+            QPushButton* turnCounterClockwise = nullptr;
+
+            qreal rotateStepSize = 45;
+        };
+        Angle _angle;
+
+        struct Zoom
+        {
+            QDoubleSpinBox* value = nullptr;
+            QPushButton* audoAdjust = nullptr;
+        };
+        Zoom _zoom;
+    };
+
+} // namespace armarx::navigation::locgrapheditor::graph_scene
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/graph_scene/Scene.cpp b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/graph_scene/Scene.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..dcd520687591d6e87cbabf9607aeeb940f23124f
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/graph_scene/Scene.cpp
@@ -0,0 +1,163 @@
+#include "Scene.h"
+
+#include <Eigen/Core>
+
+#include <ArmarXCore/core/exceptions/local/ExpressionException.h>
+
+#include <SemanticObjectRelations/Shapes/Shape.h>
+
+
+namespace armarx::navigation::locgrapheditor::graph_scene
+{
+
+    QGraphicsEllipseItem*
+    Scene::addVertex(semrel::ShapeID vertexID, const graph::VertexAttribs& attrib)
+    {
+        const Eigen::Matrix4d pose = attrib.getPose().cast<qreal>();
+
+        GraphVisualizerGraphicsEllipseItem* item = new GraphVisualizerGraphicsEllipseItem{
+            [this, vertexID]() { emit vertexSelected(vertexID); }, pose(0, 3), -pose(1, 3), 2, 2};
+        addItem(item);
+
+        // setToolTip on graphicsItem does not work
+        item->setZValue(std::numeric_limits<qreal>::max());
+        // dynamic_cast<QGraphicsItem*>(item)->setToolTip(QString::fromStdString("Vertex '" + name + "'"));
+        item->setToolTip(QString::fromStdString("Vertex '" + attrib.getName() + "'"));
+
+        return item;
+    }
+
+
+    QGraphicsLineItem*
+    Scene::addEdge(semrel::ShapeID sourceID,
+                   const graph::VertexAttribs& sourceAttrib,
+                   semrel::ShapeID targetID,
+                   const graph::VertexAttribs& targetAttrib)
+    {
+        Eigen::Matrix4d sourcePose = sourceAttrib.getPose().cast<qreal>();
+        Eigen::Matrix4d targetPose = targetAttrib.getPose().cast<qreal>();
+
+        GraphVisualizerGraphicsLineItem* item = new GraphVisualizerGraphicsLineItem{
+            [this, sourceID, targetID]() { emit edgeSelected(sourceID, targetID); },
+            sourcePose(0, 3),
+            -sourcePose(1, 3),
+            targetPose(0, 3),
+            -targetPose(1, 3)};
+        addItem(item);
+
+        // setToolTip on item does not work
+        std::stringstream toolTip;
+        toolTip << "Edge '" << sourceAttrib.getName() << "' -> '" << targetAttrib.getName();
+        item->setToolTip(QString::fromStdString(toolTip.str()));
+
+        return item;
+    }
+
+
+    void
+    Scene::updateVertex(GuiGraph::Vertex& vertex)
+    {
+        QGraphicsEllipseItem* item = vertex.attrib().graphicsItem;
+        ARMARX_CHECK_NOT_NULL(item);
+
+        const Eigen::Matrix4d pose = vertex.attrib().getPose().cast<qreal>();
+
+        QColor color = vertex.attrib().highlighted ? colorSelected : colorDefault;
+        double lineWidth = vertex.attrib().highlighted ? lineWidthSelected : lineWidthDefault;
+
+        item->setPen(QPen{color});
+        item->setBrush(QBrush{color});
+        item->setRect(pose(0, 3) - lineWidth * verticesScaleFactor / 2,
+                      -pose(1, 3) - lineWidth * verticesScaleFactor / 2,
+                      lineWidth * verticesScaleFactor,
+                      lineWidth * verticesScaleFactor);
+    }
+
+
+    void
+    Scene::updateEdge(GuiGraph::Edge& edge)
+    {
+        QGraphicsLineItem* item = edge.attrib().graphicsItem;
+        ARMARX_CHECK_NOT_NULL(item)
+            << edge.source().attrib().getName() << " -> " << edge.target().attrib().getName();
+
+        Eigen::Matrix4d sourcePose = edge.source().attrib().getPose().cast<qreal>();
+        Eigen::Matrix4d targetPose = edge.target().attrib().getPose().cast<qreal>();
+
+        QColor color = edge.attrib().highlighted ? colorSelected : colorDefault;
+        double lineWidth = edge.attrib().highlighted ? lineWidthSelected : lineWidthDefault;
+
+        QPen pen{color};
+        pen.setWidthF(lineWidth * lineScaleFactor);
+
+        item->setPen(pen);
+        item->setLine(sourcePose(0, 3), -sourcePose(1, 3), targetPose(0, 3), -targetPose(1, 3));
+    }
+
+
+    void
+    Scene::removeVertex(QGraphicsEllipseItem*& item)
+    {
+        removeItem(item);
+        delete item;
+        item = nullptr;
+    }
+
+
+    void
+    Scene::removeEdge(QGraphicsLineItem*& item)
+    {
+        removeItem(item);
+        delete item;
+        item = nullptr;
+    }
+
+
+    GraphVisualizerGraphicsEllipseItem::GraphVisualizerGraphicsEllipseItem(
+        std::function<void(void)> onDoubleClicked,
+        qreal x,
+        qreal y,
+        qreal width,
+        qreal height,
+        QGraphicsItem* parent) :
+        QGraphicsEllipseItem{x, y, width, height, parent}, onDoubleClicked{onDoubleClicked}
+    {
+    }
+
+
+    GraphVisualizerGraphicsEllipseItem::~GraphVisualizerGraphicsEllipseItem()
+    {
+    }
+
+
+    void
+    GraphVisualizerGraphicsEllipseItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent*)
+    {
+        onDoubleClicked();
+    }
+
+
+    GraphVisualizerGraphicsLineItem::GraphVisualizerGraphicsLineItem(
+        std::function<void(void)> onDoubleClicked,
+        qreal x1,
+        qreal y1,
+        qreal x2,
+        qreal y2,
+        QGraphicsItem* parent) :
+        QGraphicsLineItem{x1, y1, x2, y2, parent}, onDoubleClicked{onDoubleClicked}
+    {
+    }
+
+
+    GraphVisualizerGraphicsLineItem::~GraphVisualizerGraphicsLineItem()
+    {
+    }
+
+
+    void
+    GraphVisualizerGraphicsLineItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent*)
+    {
+        onDoubleClicked();
+    }
+
+} // namespace armarx::navigation::locgrapheditor::graph_scene
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/graph_scene/Scene.h b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/graph_scene/Scene.h
new file mode 100644
index 0000000000000000000000000000000000000000..390c105291bb52c1a6b242ece61fe2a231c33e69
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/graph_scene/Scene.h
@@ -0,0 +1,129 @@
+#pragma once
+
+#include <QGraphicsEllipseItem>
+#include <QGraphicsLineItem>
+#include <QGraphicsScene>
+#include <functional>
+
+#include <armarx/navigation/graph/Graph.h>
+#include <armarx/navigation/gui-plugins/LocationGraphEditor/GuiGraph.h>
+
+
+namespace semrel
+{
+    struct ShapeID;
+}
+namespace armarx::navigation::locgrapheditor::graph_scene
+{
+    class Scene : public QGraphicsScene
+    {
+        Q_OBJECT
+
+    public:
+        using QGraphicsScene::QGraphicsScene;
+
+        template <class VertexT>
+        QGraphicsEllipseItem*
+        addVertex(const VertexT& vertex)
+        {
+            return addVertex(vertex.objectID(), vertex.attrib());
+        }
+        QGraphicsEllipseItem* addVertex(semrel::ShapeID vertexID,
+                                        const graph::VertexAttribs& attribs);
+
+
+        template <class EdgeT>
+        QGraphicsLineItem*
+        addEdge(const EdgeT& edge)
+        {
+            return addEdge(edge.sourceObjectID(),
+                           edge.source().attrib(),
+                           edge.targetObjectID(),
+                           edge.target().attrib());
+        }
+        QGraphicsLineItem* addEdge(semrel::ShapeID sourceID,
+                                   const graph::VertexAttribs& sourceAttrib,
+                                   semrel::ShapeID targetID,
+                                   const graph::VertexAttribs& targetAttrib);
+
+
+        void updateVertex(GuiGraph::Vertex& vertex);
+        void updateEdge(GuiGraph::Edge& edge);
+
+        void removeVertex(QGraphicsEllipseItem*& item);
+        void removeEdge(QGraphicsLineItem*& item);
+
+
+    public slots:
+
+
+    signals:
+
+        void vertexSelected(const semrel::ShapeID& vertexID);
+        void edgeSelected(const semrel::ShapeID& sourceID, const semrel::ShapeID& targetID);
+
+
+    public:
+        double lineWidthDefault = 5;
+        double lineWidthSelected = 10;
+
+        double sceneScaleFactor = 2;
+        double verticesScaleFactor = 3.0 * sceneScaleFactor;
+        double lineScaleFactor = 1.0 * sceneScaleFactor;
+
+        QColor colorDefault = QColor::fromRgb(128, 128, 255);
+        QColor colorSelected = QColor::fromRgb(255, 128, 0);
+    };
+
+
+    /**
+     * @brief Required to override the double click event.
+     * This is required to toggle the select state.
+     */
+    class GraphVisualizerGraphicsEllipseItem : public QGraphicsEllipseItem
+    {
+    public:
+        GraphVisualizerGraphicsEllipseItem(std::function<void(void)> onDoubleClicked,
+                                           qreal x,
+                                           qreal y,
+                                           qreal width,
+                                           qreal height,
+                                           QGraphicsItem* parent = nullptr);
+
+        ~GraphVisualizerGraphicsEllipseItem() override;
+
+
+    protected:
+        void mouseDoubleClickEvent(QGraphicsSceneMouseEvent*) override;
+
+
+        std::function<void(void)> onDoubleClicked;
+    };
+
+
+    /**
+     * @brief Required to override the double click event.
+     * This is required to toggle the select state.
+     */
+    class GraphVisualizerGraphicsLineItem : public QGraphicsLineItem
+    {
+    public:
+        GraphVisualizerGraphicsLineItem(std::function<void(void)> onDoubleClicked,
+                                        qreal x1,
+                                        qreal y1,
+                                        qreal x2,
+                                        qreal y2,
+                                        QGraphicsItem* parent = nullptr);
+
+        ~GraphVisualizerGraphicsLineItem() override;
+
+
+    protected:
+        void mouseDoubleClickEvent(QGraphicsSceneMouseEvent*) override;
+
+
+        std::function<void(void)> onDoubleClicked;
+    };
+
+
+} // namespace armarx::navigation::locgrapheditor::graph_scene
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/graph_scene/Widget.cpp b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/graph_scene/Widget.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..cfcdc4d6b0123021831d4a81cfcb09b55206d011
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/graph_scene/Widget.cpp
@@ -0,0 +1,129 @@
+#include "Widget.h"
+
+#include <QGraphicsView>
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QVBoxLayout>
+
+#include "ControlWidget.h"
+#include "Scene.h"
+
+
+namespace armarx::navigation::locgrapheditor::graph_scene
+{
+
+    Widget::Widget()
+    {
+        _control = new ControlWidget();
+
+        _scene = new Scene();
+
+        _view = new QGraphicsView();
+        _view->setScene(_scene);
+        _view->setRenderHint(QPainter::Antialiasing, true);
+
+        int margin = 0;
+        this->setContentsMargins(margin, margin, margin, margin);
+        this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+
+        QHBoxLayout* header = new QHBoxLayout();
+        header->setSpacing(0);
+        header->addWidget(new QLabel("Graph"));
+        header->addStretch();
+        header->addWidget(_control);
+
+        QVBoxLayout* layout = new QVBoxLayout();
+        this->setLayout(layout);
+
+        layout->addLayout(header);
+        layout->addWidget(_view);
+
+
+        connect(_control, &ControlWidget::angleChanged, this, &This::transform);
+        connect(_control, &ControlWidget::zoomChanged, this, &This::transform);
+        connect(_control, &ControlWidget::zoomAutoAdjustRequested, this, &This::autoAdjustZoom);
+
+
+        // Initial transform.
+        transform();
+
+#if 0
+        auto eventFilter = [this](QObject* obj, QEvent* event) -> bool
+        {
+            if (obj == this->view.view && event->type() == QEvent::MouseButtonPress)
+            {
+                QMouseEvent* me = static_cast<QMouseEvent*>(event);
+                if (me->button() == Qt::LeftButton)
+                {
+                    QPointF scenePoint = this->view.view->mapToScene(me->pos());
+                    scenePoint.setY(- scenePoint.y());  // not sure why
+
+                    float minDist = std::numeric_limits<float>::max();
+                    auto bestIt = this->vertices.cend();
+
+                    for (auto it = this->vertices.cbegin(); it != this->vertices.cend(); ++it)
+                    {
+                        float deltaX = it->second.pose->position->x - scenePoint.x();
+                        float deltaY = it->second.pose->position->y - scenePoint.y();
+                        float dist = std::sqrt(deltaX * deltaX + deltaY * deltaY);
+
+                        if (dist < minDist)
+                        {
+                            minDist = dist;
+                            bestIt = it;
+                        }
+                    }
+
+                    if (bestIt != this->vertices.cend())
+                    {
+                        this->highlightVertex(bestIt->first);
+                    }
+                }
+            }
+            else if (event->type() == QEvent::Resize)
+            {
+                this->adjustView();
+            }
+            else
+            {
+                return false;
+            }
+        };
+        _view->installEventFilter(new simox::gui::FunctionalEventFilter(eventFilter));
+#endif
+    }
+
+
+    QGraphicsView*
+    Widget::view()
+    {
+        return _view;
+    }
+
+
+    Scene*
+    Widget::scene()
+    {
+        return _scene;
+    }
+
+
+    void
+    Widget::transform()
+    {
+        double angle = _control->angle();
+        double zoom = _control->zoom();
+        _view->setTransform(QTransform::fromScale(zoom, zoom).rotate(angle));
+    }
+
+
+    void
+    Widget::autoAdjustZoom()
+    {
+        int margin = 3;
+        QRectF rect = _scene->sceneRect() + QMargins(margin, margin, margin, margin);
+        _view->fitInView(rect, Qt::AspectRatioMode::KeepAspectRatio);
+        _control->setZoom(_view->transform().rotate(-_control->angle()).m11());
+    }
+
+} // namespace armarx::navigation::locgrapheditor::graph_scene
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/graph_scene/Widget.h b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/graph_scene/Widget.h
new file mode 100644
index 0000000000000000000000000000000000000000..149f7cf471cd3e4b313819d86bdf6813efdd7742
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/graph_scene/Widget.h
@@ -0,0 +1,54 @@
+#pragma once
+
+#include <QWidget>
+
+class QGraphicsView;
+
+
+namespace armarx::navigation::locgrapheditor::graph_scene
+{
+    class ControlWidget;
+    class Scene;
+
+
+    class Widget : public QWidget
+    {
+        Q_OBJECT
+        using This = Widget;
+
+
+    public:
+        Widget();
+
+        QGraphicsView* view();
+        Scene* scene();
+
+
+    public slots:
+
+        void transform();
+        void autoAdjustZoom();
+
+
+    private:
+        /// Some buttons and input fields.
+        ControlWidget* _control = nullptr;
+
+        /// The "canvas".
+        QGraphicsView* _view = nullptr;
+
+        /**
+         * @brief The scene displayed in the widget.
+         *
+         * For y coordinates (-y) is used to mirror the scene on the y axis.
+         * If (+y) would be used the graph displayed in the scene would not
+         * match the graph drawn to the debug layer.
+         */
+        Scene* _scene;
+
+
+        /// The view's rotation angle.
+        qreal _angle = 0;
+    };
+
+} // namespace armarx::navigation::locgrapheditor::graph_scene
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/utils.cpp b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/utils.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f39dcaa019661566cfca159c9b71823131ccbed3
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/utils.cpp
@@ -0,0 +1,60 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#include "utils.h"
+
+#include <QTableWidget>
+#include <set>
+
+
+namespace armarx::navigation::locgrapheditor
+{
+
+    // <- https://unicode-table.com/de/2190/
+    const QString utils::arrowLeft = QStringLiteral("\u2190");
+    // -> https://unicode-table.com/de/2192/
+    const QString utils::arrowRight = QStringLiteral("\u2192");
+    // <-> https://unicode-table.com/de/21C4/
+    const QString utils::arrowBoth = QStringLiteral("\u21C4");
+
+
+    QList<QTableWidgetItem*>
+    utils::getSelectedItemsOfColumn(QTableWidget* widget, int column)
+    {
+        std::set<QTableWidgetItem*> set;
+        for (QTableWidgetItem* selected : widget->selectedItems())
+        {
+            if (widget->column(selected) != column)
+            {
+                selected = widget->item(widget->row(selected), 0);
+            }
+            set.insert(selected);
+        }
+
+        QList<QTableWidgetItem*> list;
+        for (auto i : set)
+        {
+            list.append(i);
+        }
+        return list;
+    }
+
+} // namespace armarx::navigation::locgrapheditor
diff --git a/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/utils.h b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/utils.h
new file mode 100644
index 0000000000000000000000000000000000000000..1a879dd97044df42c61d5bd02321858dcfd9f7ff
--- /dev/null
+++ b/source/armarx/navigation/gui-plugins/LocationGraphEditor/widgets/utils.h
@@ -0,0 +1,52 @@
+/*
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <QList>
+#include <QString>
+
+class QTableWidget;
+class QTableWidgetItem;
+
+
+namespace armarx::navigation::locgrapheditor::utils
+{
+
+    /// <-
+    extern const QString arrowLeft;
+    /// ->
+    extern const QString arrowRight;
+    /// <->
+    extern const QString arrowBoth;
+
+
+    enum class EdgeDirection
+    {
+        From,
+        To,
+        Bidirectional
+    };
+
+
+    QList<QTableWidgetItem*> getSelectedItemsOfColumn(QTableWidget* widget, int column);
+
+} // namespace armarx::navigation::locgrapheditor::utils
diff --git a/source/armarx/navigation/location/CMakeLists.txt b/source/armarx/navigation/location/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..45c89d57565840faccda75b923d16624ac70157b
--- /dev/null
+++ b/source/armarx/navigation/location/CMakeLists.txt
@@ -0,0 +1,21 @@
+
+
+armarx_add_aron_library(location_aron
+    ARON_FILES
+        aron/Location.xml
+)
+
+
+armarx_add_library(location
+    DEPENDENCIES
+        ArmarXCoreInterfaces
+        ArmarXCore
+
+        armem
+
+        armarx_navigation::location_aron
+    SOURCES
+        constants.cpp
+    HEADERS
+        constants.h
+)
diff --git a/source/armarx/navigation/location/aron/Location.xml b/source/armarx/navigation/location/aron/Location.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9a0539d5d40a3b3b91dc32ad2034077a815437ff
--- /dev/null
+++ b/source/armarx/navigation/location/aron/Location.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<AronTypeDefinition>
+    <CodeIncludes>
+    </CodeIncludes>
+    <AronIncludes>
+        <Include include="<RobotAPI/libraries/armem/aron/MemoryID.xml>" autoinclude="true"/>
+    </AronIncludes>
+
+    <GenerateTypes>
+
+        <!--
+        ToDo: Model regions. Ideas:
+        - Polygon (convex, non-convex)
+        -
+        -->
+
+
+        <Object name='armarx::navigation::location::arondto::ObjectRelativeLocation'>
+
+            <ObjectChild key='objectInstanceID'>
+                <armarx::armem::arondto::MemoryID />
+            </ObjectChild>
+
+            <ObjectChild key='relativeRobotPose'>
+                <Pose />
+            </ObjectChild>
+
+        </Object>
+
+
+        <Object name='armarx::navigation::location::arondto::Location'>
+
+            <ObjectChild key='globalRobotPose'>
+                <Pose />
+            </ObjectChild>
+
+            <ObjectChild key='relativeToObject'>
+                <armarx::navigation::location::arondto::ObjectRelativeLocation optional="true" />
+            </ObjectChild>
+
+        </Object>
+
+    </GenerateTypes>
+</AronTypeDefinition>
diff --git a/source/armarx/navigation/location/constants.cpp b/source/armarx/navigation/location/constants.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..5c06c99d049bb83c68045f48af71c1f304fd3df8
--- /dev/null
+++ b/source/armarx/navigation/location/constants.cpp
@@ -0,0 +1,11 @@
+#include "constants.h"
+
+#include <RobotAPI/libraries/armem/core/MemoryID.h>
+
+
+namespace armarx::navigation
+{
+
+    const armem::MemoryID location::coreSegmentID{"Navigation", "Location"};
+
+}
diff --git a/source/armarx/navigation/location/constants.h b/source/armarx/navigation/location/constants.h
new file mode 100644
index 0000000000000000000000000000000000000000..57a99e76c0ce7969c6bced71ac7cbf0028a754dd
--- /dev/null
+++ b/source/armarx/navigation/location/constants.h
@@ -0,0 +1,32 @@
+/**
+ * This file is part of ArmarX.
+ *
+ * ArmarX is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * ArmarX is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author     Rainer Kartmann ( rainer dot kartmann at kit dot edu )
+ * @date       2021
+ * @copyright  http://www.gnu.org/licenses/gpl-2.0.txt
+ *             GNU General Public License
+ */
+
+#pragma once
+
+#include <RobotAPI/libraries/armem/core/forward_declarations.h>
+
+
+namespace armarx::navigation::location
+{
+
+    extern const armem::MemoryID coreSegmentID;
+
+}
diff --git a/source/armarx/navigation/memory/CMakeLists.txt b/source/armarx/navigation/memory/CMakeLists.txt
index 18b8f00913336123bdf078a7877395b04985fae8..e6e05d83ba509797a784cdb3d8266a0f6fe8f151 100644
--- a/source/armarx/navigation/memory/CMakeLists.txt
+++ b/source/armarx/navigation/memory/CMakeLists.txt
@@ -2,15 +2,17 @@ armarx_add_library(memory
     SOURCES
         #./memory.cpp
         ./client/stack_result/Writer.cpp
-        ./client/parameterization/Writer.cpp
         ./client/parameterization/Reader.cpp
+        ./client/parameterization/Writer.cpp
         ./client/events/Writer.cpp
+        # ./client/events/Reader.cpp
     HEADERS
         #./memory.h
         ./client/stack_result/Writer.h
-        ./client/parameterization/Writer.h
         ./client/parameterization/Reader.h
+        ./client/parameterization/Writer.h
         ./client/events/Writer.h
+        # ./client/events/Reader.h
     DEPENDENCIES
         ArmarXCoreInterfaces
         ArmarXCore
diff --git a/source/armarx/navigation/memory/client/parameterization/Writer.cpp b/source/armarx/navigation/memory/client/parameterization/Writer.cpp
index 677df950880389729ceb5e7aecd762a4763b7e86..64fff8641baa9a2c664f684be3a25d4246f4aa28 100644
--- a/source/armarx/navigation/memory/client/parameterization/Writer.cpp
+++ b/source/armarx/navigation/memory/client/parameterization/Writer.cpp
@@ -1,16 +1,17 @@
 #include "Writer.h"
 
-#include <RobotAPI/libraries/aron/core/navigator/data/container/Dict.h>
+#include <RobotAPI/libraries/aron/core/navigator/data/AllNavigators.h>
 
 #include <armarx/navigation/core/constants.h>
 
+
 namespace armarx::navigation::mem::client::param
 {
     bool
-    Writer::store(const std::unordered_map<core::StackLayer,
-                                           aron::datanavigator::DictNavigator::PointerType>& stack,
-                  const std::string& clientID,
-                  const core::TimestampUs& timestamp)
+    Writer::store(
+        const std::unordered_map<core::StackLayer, aron::datanavigator::DictNavigatorPtr>& stack,
+        const std::string& clientID,
+        const core::TimestampUs& timestamp)
     {
         ARMARX_CHECK(not stack.empty());
 
@@ -64,4 +65,5 @@ namespace armarx::navigation::mem::client::param
             .providerName = "" // clientId
         };
     }
+
 } // namespace armarx::navigation::mem::client::param
diff --git a/source/armarx/navigation/memory/client/parameterization/Writer.h b/source/armarx/navigation/memory/client/parameterization/Writer.h
index fb2982abc8464a09286a33b78223d6921543fd94..aac8fea7640efc921937b639cdb518b339272591 100644
--- a/source/armarx/navigation/memory/client/parameterization/Writer.h
+++ b/source/armarx/navigation/memory/client/parameterization/Writer.h
@@ -22,12 +22,11 @@
 #pragma once
 
 #include <RobotAPI/libraries/armem/client/util/SimpleWriterBase.h>
-#include <RobotAPI/libraries/aron/core/navigator/data/container/Dict.h>
+#include <RobotAPI/libraries/aron/core/navigator/data/forward_declarations.h>
 
 #include <armarx/navigation/core/constants.h>
 #include <armarx/navigation/core/types.h>
 
-
 namespace armarx::navigation::mem::client::param
 {
 
@@ -38,7 +37,7 @@ namespace armarx::navigation::mem::client::param
 
 
         bool store(const std::unordered_map<core::StackLayer,
-                                            aron::datanavigator::DictNavigator::PointerType>& stack,
+                                            aron::datanavigator::DictNavigatorPtr>& stack,
                    const std::string& clientID,
                    const core::TimestampUs& timestamp);