From 5608f897b5efcaad2781a638b6efbd7f837bdb13 Mon Sep 17 00:00:00 2001
From: Fabian Paus <fabian.paus@kit.edu>
Date: Fri, 15 Nov 2019 09:30:44 +0100
Subject: [PATCH] ArViz: GUI for starting, stopping and inspecting recordings

---
 .../components/ArViz/ArVizStorage.cpp         |  88 +++++++-
 .../RobotAPI/components/ArViz/ArVizStorage.h  |   2 +-
 .../RobotAPI/gui-plugins/ArViz/ArVizWidget.ui | 192 ++++++++++++++++--
 .../ArViz/ArVizWidgetController.cpp           |  76 ++++++-
 .../gui-plugins/ArViz/ArVizWidgetController.h |   5 +
 source/RobotAPI/interface/ArViz/Component.ice |   2 +-
 6 files changed, 342 insertions(+), 23 deletions(-)

diff --git a/source/RobotAPI/components/ArViz/ArVizStorage.cpp b/source/RobotAPI/components/ArViz/ArVizStorage.cpp
index c1f59cc39..70313c24d 100644
--- a/source/RobotAPI/components/ArViz/ArVizStorage.cpp
+++ b/source/RobotAPI/components/ArViz/ArVizStorage.cpp
@@ -28,6 +28,8 @@
 
 #include <VirtualRobot/Util/json/json.hpp>
 
+#include <iomanip>
+
 
 using namespace armarx;
 
@@ -202,6 +204,15 @@ namespace armarx::viz
         j["lastTimestampInMicroSeconds"] = batch.lastTimestampInMicroSeconds;
     }
 
+    void from_json(nlohmann::json const& j, armarx::viz::RecordingBatchHeader& batch)
+    {
+        batch.index = j["index"] ;
+        batch.firstRevision = j["firstRevision"];
+        batch.lastRevision = j["lastRevision"];
+        batch.firstTimestampInMicroSeconds = j["firstTimestampInMicroSeconds"];
+        batch.lastTimestampInMicroSeconds = j["lastTimestampInMicroSeconds"];
+    }
+
     void to_json(nlohmann::json& j, armarx::viz::Recording const& recording)
     {
         j["id"] = recording.id;
@@ -212,6 +223,16 @@ namespace armarx::viz
         j["batchHeaders"] = recording.batchHeaders;
     }
 
+    void from_json(nlohmann::json const& j, armarx::viz::Recording& recording)
+    {
+        recording.id = j["id"];
+        recording.firstRevision = j["firstRevision"];
+        recording.lastRevision = j["lastRevision"];
+        recording.firstTimestampInMicroSeconds = j["firstTimestampInMicroSeconds"];
+        recording.lastTimestampInMicroSeconds = j["lastTimestampInMicroSeconds"];
+        j["batchHeaders"].get_to(recording.batchHeaders);
+    }
+
 }
 
 static bool writeCompleteFile(std::string const& filename,
@@ -231,6 +252,21 @@ static bool writeCompleteFile(std::string const& filename,
     return true;
 }
 
+static std::string readCompleteFile(std::filesystem::path const& path)
+{
+    FILE* f = fopen(path.string().c_str(), "rb");
+    fseek(f, 0, SEEK_END);
+    long fsize = ftell(f);
+    fseek(f, 0, SEEK_SET);  /* same as rewind(f); */
+
+    std::string result(fsize, '\0');
+    std::size_t read = fread(result.data(), 1, fsize, f);
+    result.resize(read);
+    fclose(f);
+
+    return result;
+}
+
 void ArVizStorage::recordBatch(std::vector<viz::TimestampedLayerUpdate> const& batch)
 {
     if (batch.empty())
@@ -291,18 +327,26 @@ void ArVizStorage::recordBatch(std::vector<viz::TimestampedLayerUpdate> const& b
 }
 
 
-std::string armarx::ArVizStorage::startRecording(std::string const& newRecordingID, const Ice::Current&)
+std::string armarx::ArVizStorage::startRecording(std::string const& newRecordingPrefix, const Ice::Current&)
 {
     {
         std::unique_lock<std::mutex> lock(recordingMutex);
         if (recordingMetaData.id.size() > 0)
         {
-            ARMARX_WARNING << "Could not start recording with ID " << newRecordingID
+            ARMARX_WARNING << "Could not start recording with prefix " << newRecordingPrefix
                            << "\nbecause there is already a recording running for the recording ID: "
                            << recordingMetaData.id;
             return recordingMetaData.id;
         }
 
+        auto t = std::time(nullptr);
+        auto tm = *std::localtime(&t);
+        std::ostringstream id;
+        id << newRecordingPrefix
+           << '_'
+           << std::put_time(&tm, "%Y-%m-%d_%H-%M-%S");
+        std::string newRecordingID = id.str();
+
         recordingPath = historyPath / newRecordingID;
         if (!std::filesystem::exists(recordingPath))
         {
@@ -331,7 +375,7 @@ std::string armarx::ArVizStorage::startRecording(std::string const& newRecording
     recordingTask = new RunningTask<ArVizStorage>(this, &ArVizStorage::record);
     recordingTask->start();
 
-    return recordingMetaData.id;
+    return "";
 }
 
 void armarx::ArVizStorage::stopRecording(const Ice::Current&)
@@ -349,11 +393,49 @@ void armarx::ArVizStorage::stopRecording(const Ice::Current&)
     recordBatch(history);
 
     recordingMetaData.id = "";
+    recordingMetaData.firstRevision = -1;
+    recordingMetaData.firstTimestampInMicroSeconds = -1;
 }
 
 viz::RecordingSeq armarx::ArVizStorage::getAllRecordings(const Ice::Current&)
 {
     viz::RecordingSeq result;
+
+    for (std::filesystem::directory_entry const& entry : std::filesystem::directory_iterator(historyPath))
+    {
+        ARMARX_INFO << "Checking: " << entry.path();
+
+        if (!entry.is_directory())
+        {
+            continue;
+        }
+
+        std::filesystem::path recordingFilePath = entry.path() / "recording.json";
+        if (!std::filesystem::exists(recordingFilePath))
+        {
+            ARMARX_INFO << "No recording.json found in directory: " << entry.path();
+            continue;
+        }
+
+        try
+        {
+            std::string recordingString = readCompleteFile(recordingFilePath);
+            nlohmann::json recordingJson = nlohmann::json::parse(recordingString);
+
+            viz::Recording recording;
+            recordingJson.get_to(recording);
+
+            result.push_back(std::move(recording));
+        }
+        catch (std::exception const& ex)
+        {
+            ARMARX_WARNING << "Could not parse JSON file: " << recordingFilePath
+                           << "\nReason: " << ex.what();
+            continue;
+        }
+    }
+
+
     return result;
 }
 
diff --git a/source/RobotAPI/components/ArViz/ArVizStorage.h b/source/RobotAPI/components/ArViz/ArVizStorage.h
index 88570480d..56ec77649 100644
--- a/source/RobotAPI/components/ArViz/ArVizStorage.h
+++ b/source/RobotAPI/components/ArViz/ArVizStorage.h
@@ -109,7 +109,7 @@ namespace armarx
 
         // StorageInterface interface
         viz::LayerUpdates pullUpdatesSince(Ice::Long revision, const Ice::Current&) override;
-        std::string startRecording(const std::string&, const Ice::Current&) override;
+        std::string startRecording(std::string const& prefix, const Ice::Current&) override;
         void stopRecording(const Ice::Current&) override;
         viz::RecordingSeq getAllRecordings(const Ice::Current&) override;
         viz::RecordingBatch getRecordingBatch(const std::string&, Ice::Long, const Ice::Current&) override;
diff --git a/source/RobotAPI/gui-plugins/ArViz/ArVizWidget.ui b/source/RobotAPI/gui-plugins/ArViz/ArVizWidget.ui
index 53178c762..6400791a9 100644
--- a/source/RobotAPI/gui-plugins/ArViz/ArVizWidget.ui
+++ b/source/RobotAPI/gui-plugins/ArViz/ArVizWidget.ui
@@ -7,7 +7,7 @@
     <x>0</x>
     <y>0</y>
     <width>631</width>
-    <height>429</height>
+    <height>488</height>
    </rect>
   </property>
   <property name="windowTitle">
@@ -126,7 +126,7 @@
             <item>
              <widget class="QLabel" name="label">
               <property name="text">
-               <string>ID:</string>
+               <string>Prefix:</string>
               </property>
              </widget>
             </item>
@@ -137,30 +137,188 @@
               </property>
              </widget>
             </item>
+            <item>
+             <widget class="QPushButton" name="startRecordingButton">
+              <property name="text">
+               <string>Start</string>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="QPushButton" name="stopRecordingButton">
+              <property name="text">
+               <string>Stop</string>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <spacer name="horizontalSpacer_2">
+              <property name="orientation">
+               <enum>Qt::Horizontal</enum>
+              </property>
+              <property name="sizeHint" stdset="0">
+               <size>
+                <width>40</width>
+                <height>20</height>
+               </size>
+              </property>
+             </spacer>
+            </item>
            </layout>
           </item>
-          <item>
-           <widget class="QPushButton" name="startRecordingButton">
-            <property name="text">
-             <string>Start</string>
-            </property>
-           </widget>
-          </item>
-          <item>
-           <widget class="QPushButton" name="stopRecordingButton">
-            <property name="text">
-             <string>Stop</string>
-            </property>
-           </widget>
-          </item>
          </layout>
         </widget>
        </item>
        <item>
         <widget class="QGroupBox" name="groupBox_3">
          <property name="title">
-          <string>Replay</string>
+          <string>Replay Selection</string>
          </property>
+         <layout class="QVBoxLayout" name="verticalLayout_4">
+          <item>
+           <layout class="QHBoxLayout" name="horizontalLayout_7">
+            <item>
+             <widget class="QLabel" name="label_4">
+              <property name="text">
+               <string>All Recordings</string>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="QPushButton" name="refreshRecordingsButton">
+              <property name="text">
+               <string>Refresh</string>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <spacer name="horizontalSpacer">
+              <property name="orientation">
+               <enum>Qt::Horizontal</enum>
+              </property>
+              <property name="sizeHint" stdset="0">
+               <size>
+                <width>40</width>
+                <height>20</height>
+               </size>
+              </property>
+             </spacer>
+            </item>
+           </layout>
+          </item>
+          <item>
+           <layout class="QHBoxLayout" name="horizontalLayout_8">
+            <item>
+             <widget class="QListWidget" name="recordingList">
+              <property name="minimumSize">
+               <size>
+                <width>220</width>
+                <height>0</height>
+               </size>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="QGroupBox" name="groupBox_4">
+              <property name="title">
+               <string>Recording Information</string>
+              </property>
+              <layout class="QFormLayout" name="formLayout">
+               <item row="0" column="0">
+                <widget class="QLabel" name="label_3">
+                 <property name="text">
+                  <string>Recording ID:</string>
+                 </property>
+                </widget>
+               </item>
+               <item row="0" column="1">
+                <widget class="QLabel" name="recordingIdLabel">
+                 <property name="text">
+                  <string>-----</string>
+                 </property>
+                </widget>
+               </item>
+               <item row="1" column="0">
+                <widget class="QLabel" name="label_5">
+                 <property name="text">
+                  <string>Revision:</string>
+                 </property>
+                </widget>
+               </item>
+               <item row="1" column="1">
+                <widget class="QLabel" name="recordingRevisionLabel">
+                 <property name="text">
+                  <string>-----</string>
+                 </property>
+                </widget>
+               </item>
+               <item row="2" column="0">
+                <widget class="QLabel" name="label_7">
+                 <property name="text">
+                  <string>Timestamp:</string>
+                 </property>
+                </widget>
+               </item>
+               <item row="2" column="1">
+                <widget class="QLabel" name="recordingTimestampLabel">
+                 <property name="minimumSize">
+                  <size>
+                   <width>290</width>
+                   <height>0</height>
+                  </size>
+                 </property>
+                 <property name="text">
+                  <string>01.01.1970 01:00:00 - 01.01.1970 01:00:00</string>
+                 </property>
+                </widget>
+               </item>
+               <item row="3" column="0">
+                <widget class="QLabel" name="label_9">
+                 <property name="text">
+                  <string>Duration:</string>
+                 </property>
+                </widget>
+               </item>
+               <item row="3" column="1">
+                <widget class="QLabel" name="recordingDurationLabel">
+                 <property name="text">
+                  <string>-----</string>
+                 </property>
+                </widget>
+               </item>
+               <item row="4" column="0">
+                <widget class="QLabel" name="label_11">
+                 <property name="text">
+                  <string>Batches:</string>
+                 </property>
+                </widget>
+               </item>
+               <item row="4" column="1">
+                <widget class="QLabel" name="recordingBatchesLabel">
+                 <property name="text">
+                  <string>-----</string>
+                 </property>
+                </widget>
+               </item>
+              </layout>
+             </widget>
+            </item>
+            <item>
+             <spacer name="horizontalSpacer_3">
+              <property name="orientation">
+               <enum>Qt::Horizontal</enum>
+              </property>
+              <property name="sizeHint" stdset="0">
+               <size>
+                <width>40</width>
+                <height>20</height>
+               </size>
+              </property>
+             </spacer>
+            </item>
+           </layout>
+          </item>
+         </layout>
         </widget>
        </item>
       </layout>
diff --git a/source/RobotAPI/gui-plugins/ArViz/ArVizWidgetController.cpp b/source/RobotAPI/gui-plugins/ArViz/ArVizWidgetController.cpp
index 03da4e048..ec24b3917 100644
--- a/source/RobotAPI/gui-plugins/ArViz/ArVizWidgetController.cpp
+++ b/source/RobotAPI/gui-plugins/ArViz/ArVizWidgetController.cpp
@@ -47,6 +47,9 @@ ArVizWidgetController::ArVizWidgetController()
     connect(widget.startRecordingButton, &QPushButton::clicked, this, &ArVizWidgetController::onStartRecording);
     connect(widget.stopRecordingButton, &QPushButton::clicked, this, &ArVizWidgetController::onStopRecording);
 
+    connect(widget.refreshRecordingsButton, &QPushButton::clicked, this, &ArVizWidgetController::onRefreshRecordings);
+    connect(widget.recordingList, &QListWidget::currentItemChanged, this, &ArVizWidgetController::onRecordingSelectionChanged);
+
 
     // We need a callback from the visualizer, when the layers have changed
     // So we can update the tree accordingly
@@ -312,7 +315,7 @@ void ArVizWidgetController::onStartRecording()
 
     std::string runningID = storage->startRecording(recordingID);
 
-    if (runningID != recordingID)
+    if (runningID.size() > 0)
     {
         std::string message = "There is already a recording running with ID '"
                               + runningID + "'. \n"
@@ -329,6 +332,77 @@ void ArVizWidgetController::onStopRecording()
     storage->stopRecording();
 }
 
+void ArVizWidgetController::onRefreshRecordings()
+{
+    allRecordings = storage->getAllRecordings();
+
+    std::string currentText;
+    QListWidgetItem* currentItem = widget.recordingList->currentItem();
+    if (currentItem)
+    {
+        currentText = currentItem->text().toStdString();
+    }
+
+    // TODO: Remember old selected entry and try to restore
+    widget.recordingList->clear();
+    int currentTextIndex = -1;
+    int index = 0;
+    for (auto& recording : allRecordings)
+    {
+        if (recording.id == currentText)
+        {
+            currentTextIndex = index;
+        }
+        widget.recordingList->addItem(QString::fromStdString(recording.id));
+        ++index;
+    }
+
+    if (currentTextIndex > 0)
+    {
+        widget.recordingList->setCurrentRow(currentTextIndex);
+    }
+}
+
+void ArVizWidgetController::onRecordingSelectionChanged(QListWidgetItem* current, QListWidgetItem* previous)
+{
+    std::string selectedID = current->text().toStdString();
+    for (auto& recording : allRecordings)
+    {
+        if (recording.id == selectedID)
+        {
+            widget.recordingIdLabel->setText(QString::fromStdString(recording.id));
+
+            widget.recordingRevisionLabel->setText(QString::fromStdString(
+                    std::to_string(recording.firstRevision) +
+                    " - " +
+                    std::to_string(recording.lastRevision)));
+
+            IceUtil::Time firstTime = IceUtil::Time::microSeconds(recording.firstTimestampInMicroSeconds);
+            IceUtil::Time lastTime = IceUtil::Time::microSeconds(recording.lastTimestampInMicroSeconds);
+            std::string firstTimeString = firstTime.toDateTime();
+            std::string lastTimeString = lastTime.toDateTime();
+            firstTimeString = firstTimeString.substr(0, firstTimeString.size() - 4);
+            lastTimeString = lastTimeString.substr(0, lastTimeString.size() - 4);
+
+            widget.recordingTimestampLabel->setText(QString::fromStdString(
+                    firstTimeString +
+                    " - " +
+                    lastTimeString));
+
+            IceUtil::Time duration = lastTime - firstTime;
+            int durationSeconds = duration.toSeconds();
+            widget.recordingDurationLabel->setText(QString::fromStdString(
+                    std::to_string(durationSeconds) + " s"));
+
+            widget.recordingBatchesLabel->setText(QString::fromStdString(
+                    std::to_string(recording.batchHeaders.size())
+                                                  ));
+
+            break;
+        }
+    }
+}
+
 SoNode* ArVizWidgetController::getScene()
 {
     return visualizer.root;
diff --git a/source/RobotAPI/gui-plugins/ArViz/ArVizWidgetController.h b/source/RobotAPI/gui-plugins/ArViz/ArVizWidgetController.h
index 9c1ea5554..3a7bb09b5 100644
--- a/source/RobotAPI/gui-plugins/ArViz/ArVizWidgetController.h
+++ b/source/RobotAPI/gui-plugins/ArViz/ArVizWidgetController.h
@@ -124,6 +124,9 @@ namespace armarx
         void onStartRecording();
         void onStopRecording();
 
+        void onRefreshRecordings();
+        void onRecordingSelectionChanged(QListWidgetItem* current, QListWidgetItem* previous);
+
     private:
         Ui::ArVizWidget widget;
 
@@ -133,6 +136,8 @@ namespace armarx
         armarx::viz::StorageInterfacePrx storage;
 
         armarx::viz::CoinVisualizer visualizer;
+
+        viz::RecordingSeq allRecordings;
     };
 }
 
diff --git a/source/RobotAPI/interface/ArViz/Component.ice b/source/RobotAPI/interface/ArViz/Component.ice
index 01b5d75e1..198068966 100644
--- a/source/RobotAPI/interface/ArViz/Component.ice
+++ b/source/RobotAPI/interface/ArViz/Component.ice
@@ -84,7 +84,7 @@ interface StorageInterface
 {
     LayerUpdates pullUpdatesSince(long revision);
 
-    string startRecording(string id);
+    string startRecording(string prefix);
 
     void stopRecording();
 
-- 
GitLab