{"action":"created","comment":{"url":"https://api.github.com/repos/OpenSpace/OpenSpace/pulls/comments/229939915","pull_request_review_id":170557464,"id":229939915,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDIyOTkzOTkxNQ==","diff_hunk":"@@ -0,0 +1,1207 @@\n+/*****************************************************************************************\n+ *                                                                                       *\n+ * OpenSpace                                                                             *\n+ *                                                                                       *\n+ * Copyright (c) 2014-2018                                                               *\n+ *                                                                                       *\n+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this  *\n+ * software and associated documentation files (the \"Software\"), to deal in the Software *\n+ * without restriction, including without limitation the rights to use, copy, modify,    *\n+ * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to    *\n+ * permit persons to whom the Software is furnished to do so, subject to the following   *\n+ * conditions:                                                                           *\n+ *                                                                                       *\n+ * The above copyright notice and this permission notice shall be included in all copies *\n+ * or substantial portions of the Software.                                              *\n+ *                                                                                       *\n+ * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,   *\n+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A         *\n+ * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT    *\n+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF  *\n+ * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE  *\n+ * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                                         *\n+ ****************************************************************************************/\n+\n+#include <openspace/interaction/sessionrecording.h>\n+#include <openspace/interaction/externinteraction.h>\n+\n+#include <openspace/openspace.h>\n+#include <openspace/engine/globals.h>\n+#include <openspace/engine/openspaceengine.h>\n+#include <openspace/engine/windowdelegate.h>\n+#include <openspace/interaction/navigationhandler.h>\n+#include <openspace/interaction/orbitalnavigator.h>\n+#include <openspace/interaction/keyframenavigator.h>\n+#include <openspace/scripting/scriptscheduler.h>\n+#include <openspace/scripting/scriptengine.h>\n+#include <openspace/scene/scenegraphnode.h>\n+#include <openspace/scene/scene.h>\n+#include <openspace/util/camera.h>\n+#include <openspace/util/time.h>\n+#include <openspace/util/timemanager.h>\n+#include <openspace/util/spicemanager.h>\n+\n+#include <ghoul/logging/logmanager.h>\n+\n+#include<openspace/rendering/renderengine.h>\n+\n+#include <iterator>\n+\n+namespace {\n+    const char* _loggerCat = \"SessionRecording\";\n+}\n+\n+#include \"sessionrecording_lua.inl\"\n+\n+\n+namespace openspace::interaction {\n+\n+SessionRecording::SessionRecording()\n+    : properties::PropertyOwner({ \"SessionRecording\" }) {\n+}\n+\n+SessionRecording::~SessionRecording() {} // NOLINT\n+\n+void SessionRecording::deinitialize() {\n+    stopRecording();\n+    stopPlayback();\n+}\n+\n+void SessionRecording::setRecordDataFormat(recordDataMode dataMode) {\n+    _recordingDataMode = dataMode;\n+}\n+\n+bool SessionRecording::startRecording(std::string filename) {\n+    if (_state == sessionState::playback) {\n+        _playbackFile.close();\n+    }\n+    _state = sessionState::recording;\n+    _playbackActive_camera = false;\n+    _playbackActive_time = false;\n+    _playbackActive_script = false;\n+    if (isDataModeBinary())\n+        _recordFile.open(filename, std::ios::binary);\n+    else\n+        _recordFile.open(filename);\n+\n+    if (!_recordFile.is_open() || !_recordFile.good()) {\n+        LERROR(fmt::format(\"Unable to open file {} for keyframe recording\", filename.c_str()));\n+        return false;\n+    }\n+    _recordFile << _fileHeaderTitle;\n+    _recordFile.write(_fileHeaderVersion, _fileHeaderVersionLength);\n+    if (isDataModeBinary())\n+        _recordFile << dataFormatBinaryTag;\n+    else\n+        _recordFile << dataFormatAsciiTag;\n+    _recordFile << '\\n';\n+\n+    LINFO(\"Session recording started\");\n+    _timestampRecordStarted = global::windowDelegate.applicationTime();\n+    return true;\n+}\n+\n+void SessionRecording::stopRecording() {\n+    if (_state == sessionState::recording) {\n+        _state = sessionState::idle;\n+        LINFO(\"Session recording stopped\");\n+    }\n+    //Close the recording file\n+    _recordFile.close();\n+}\n+\n+bool SessionRecording::startPlayback(std::string filename, KeyframeTimeRef timeMode,\n+                                     bool forceSimTimeAtStart)\n+{\n+    if (_state == sessionState::recording) {\n+        LERROR(\"Unable to start playback while in session recording mode\");\n+        return false;\n+    }\n+    else if (_state == sessionState::playback) {\n+        if (_playbackFilename.compare(filename) == 0) {\n+            LERROR(fmt::format(\n+                \"Unable to start playback on file {} since it is already in playback\",\n+                filename)\n+            );\n+            return false;\n+        }\n+    }\n+\n+    if (!doesFileExist(filename)) {\n+        LERROR(\"Cannot find the specified playback file.\");\n+        cleanUpPlayback();\n+        return false;\n+    }\n+\n+    _playbackLineNum = 1;\n+    _playbackFilename = filename;\n+\n+    //Open in ASCII first\n+    _playbackFile.open(_playbackFilename, std::ifstream::in);\n+    //Read header\n+    std::string readBackHeaderString = readHeaderElement(_fileHeaderTitle.length());\n+    if (readBackHeaderString.compare(_fileHeaderTitle) != 0) {\n+        LERROR(\"Specified playback file does not contain expected header.\");\n+        cleanUpPlayback();\n+        return false;\n+    }\n+    readHeaderElement(_fileHeaderVersionLength);\n+    std::string readDataMode = readHeaderElement(1);\n+    if (readDataMode[0] == dataFormatAsciiTag) {\n+        _recordingDataMode = recordDataMode::ascii;\n+    }\n+    else if (readDataMode[0] == dataFormatBinaryTag) {\n+        _recordingDataMode = recordDataMode::binary;\n+    }\n+    else {\n+        LERROR(\"Unknown data type in header (should be Ascii or Binary)\");\n+        cleanUpPlayback();\n+    }\n+    std::string throwawayNewlineChar = readHeaderElement(1);\n+    \n+    if (isDataModeBinary()) {\n+        //Close & re-open the file, starting from the beginning, and do dummy read\n+        // past the header, version, and data type\n+        _playbackFile.close();\n+        _playbackFile.open(_playbackFilename, std::ifstream::in | std::ios::binary);\n+        size_t throwAwayHeaderReadSize = _fileHeaderTitle.length() +\n+                                               _fileHeaderVersionLength +\n+                                               sizeof(dataFormatBinaryTag) +\n+                                               sizeof('\\n');\n+        _playbackFile.read(reinterpret_cast<char*>(&_keyframeBuffer),\n+                           throwAwayHeaderReadSize);\n+    }\n+\n+    if (!_playbackFile.is_open() || !_playbackFile.good())\n+    {\n+        LERROR(fmt::format(\"Unable to open file {} for keyframe playback\",\n+               filename.c_str()));\n+        stopPlayback();\n+        cleanUpPlayback();\n+        return false;\n+    }\n+    //Set time reference mode\n+    double now = global::windowDelegate.applicationTime();\n+    _timestampPlaybackStarted_application = now;\n+    _timestampPlaybackStarted_simulation = global::timeManager.time().j2000Seconds();\n+    _timestampApplicationStarted_simulation = _timestampPlaybackStarted_simulation - now;\n+    _playbackTimeReferenceMode = timeMode;\n+\n+    //Set playback flags to true for all modes\n+    _playbackActive_camera = true;\n+    _playbackActive_script = true;\n+    if (_usingTimeKeyframes)\n+        _playbackActive_time = true;\n+\n+    global::navigationHandler.keyframeNavigator().setTimeReferenceMode(timeMode, now);\n+    global::scriptScheduler.setTimeReferenceMode(timeMode);\n+\n+    _setSimulationTimeWithNextCameraKeyframe = forceSimTimeAtStart;\n+    if (!playbackAddEntriesToTimeline()) {\n+        cleanUpPlayback();\n+        return false;\n+    }\n+\n+    _hasHitEndOfCameraKeyframes = false;\n+    findFirstCameraKeyframeInTimeline();\n+\n+    LINFO(fmt::format(\"Playback session started @ ({:8.3f},0.0,{:13.3f}) with {}/{}/{} entries, forceTime={}\",\n+        now, _timestampPlaybackStarted_simulation, _keyframesCamera.size(),\n+        _keyframesTime.size(), _keyframesScript.size(), (forceSimTimeAtStart ? 1 : 0)));\n+\n+    global::navigationHandler.triggerPlaybackStart();\n+    global::scriptScheduler.triggerPlaybackStart();\n+    global::timeManager.triggerPlaybackStart();\n+    _state = sessionState::playback;\n+\n+    return true;\n+}\n+\n+void SessionRecording::findFirstCameraKeyframeInTimeline() {\n+    bool foundCameraKeyframe = false;\n+    for (unsigned int i = 0; i < _timeline.size(); i++) {\n+        if (doesTimelineEntryContainCamera(i)) {\n+            _idxTimeline_cameraPtrPrev = i;\n+            _idxTimeline_cameraPtrNext = i;\n+            foundCameraKeyframe = true;\n+            break;\n+        }\n+    }\n+\n+    if (!foundCameraKeyframe)\n+        signalPlaybackFinishedForComponent(recordedType::camera);\n+}\n+\n+std::string SessionRecording::readHeaderElement(size_t readLen_chars) {\n+    std::vector<char> readTemp(readLen_chars);\n+    _playbackFile.read(&readTemp[0], readLen_chars);\n+    return std::string(readTemp.begin(), readTemp.end());\n+    //return std::string(&readTemp[0], &readTemp[readLen_chars - 1]);\n+}\n+\n+void SessionRecording::signalPlaybackFinishedForComponent(recordedType type) {\n+    if (type == recordedType::camera) {\n+        _playbackActive_camera = false;\n+        LINFO(\"Playback finished signal: camera\");\n+    }\n+    else if (type == recordedType::time) {\n+        _playbackActive_time = false;\n+        LINFO(\"Playback finished signal: time\");\n+    }\n+    else if (type == recordedType::script) {\n+        _playbackActive_script = false;\n+        LINFO(\"Playback finished signal: script\");\n+    }\n+\n+    if (!_playbackActive_camera && !_playbackActive_time && !_playbackActive_script ) {\n+        _state = sessionState::idle;\n+        _cleanupNeeded = true;\n+        LINFO(\"Playback session finished\");\n+    }\n+}\n+\n+void SessionRecording::stopPlayback() {\n+    if (_state == sessionState::playback) {\n+        _state = sessionState::idle;\n+        _cleanupNeeded = true;\n+        LINFO(\"Session playback stopped\");\n+    }\n+}\n+\n+void SessionRecording::cleanUpPlayback() {\n+    global::navigationHandler.stopPlayback();\n+\n+    Camera * camera = global::navigationHandler.camera();\n+    ghoul_assert(camera != nullptr, \"Camera must not be nullptr\");\n+    Scene * scene = camera->parent()->scene();\n+    if (_timeline.size() != 0) {\n+        unsigned int prevIdx = _timeline[_idxTimeline_cameraPtrPrev].idxIntoKeyframeTypeArray;\n+        global::navigationHandler.setFocusNode(scene->sceneGraphNode(_keyframesCamera[prevIdx].focusNode));\n+    }\n+    global::scriptScheduler.stopPlayback();\n+\n+    _playbackFile.close();\n+\n+    //Clear all timelines and keyframes\n+    _timeline.clear();\n+    _keyframesCamera.clear();\n+    _keyframesTime.clear();\n+    _keyframesScript.clear();\n+    _idxTimeline_nonCamera = 0;\n+    _idxTime = 0;\n+    _idxScript = 0;\n+    _idxTimeline_cameraPtrNext = 0;\n+    _idxTimeline_cameraPtrPrev = 0;\n+    _hasHitEndOfCameraKeyframes = false;\n+\n+    _cleanupNeeded = false;\n+}\n+\n+bool SessionRecording::isDataModeBinary() {\n+    if (_recordingDataMode == recordDataMode::binary)\n+        return true;\n+    else\n+        return false;\n+}\n+\n+void SessionRecording::writeToFileBuffer(const double& src) {\n+    const size_t writeSize_bytes = sizeof(double);\n+    unsigned char const *p = reinterpret_cast<unsigned char const*>(&src);\n+    memcpy((_keyframeBuffer + _bufferIndex), p, writeSize_bytes);\n+    _bufferIndex += writeSize_bytes;\n+}\n+\n+void SessionRecording::writeToFileBuffer(std::vector<char>& cvec) {\n+    const size_t writeSize_bytes = cvec.size() * sizeof(char);\n+    memcpy((_keyframeBuffer + _bufferIndex), &cvec[0], writeSize_bytes);\n+    _bufferIndex += writeSize_bytes;\n+}\n+\n+void SessionRecording::writeToFileBuffer(const unsigned char c) {\n+    const size_t writeSize_bytes = sizeof(char);\n+    _keyframeBuffer[_bufferIndex] = c;\n+    _bufferIndex += writeSize_bytes;\n+}\n+\n+void SessionRecording::writeToFileBuffer(const bool b) {\n+    const size_t writeSize_bytes = sizeof(char);\n+    if( b )\n+        _keyframeBuffer[_bufferIndex] = 1;\n+    else\n+        _keyframeBuffer[_bufferIndex] = 0;\n+    _bufferIndex += writeSize_bytes;\n+}\n+\n+void SessionRecording::saveStringToFile(const std::string s) {\n+    size_t strLen = s.size();\n+    size_t writeSize_bytes = sizeof(size_t);\n+\n+    _bufferIndex = 0;\n+    unsigned char const *p = reinterpret_cast<unsigned char const*>(&strLen);\n+    memcpy((_keyframeBuffer + _bufferIndex), p, writeSize_bytes);\n+    _bufferIndex += (unsigned int)writeSize_bytes;\n+    saveKeyframeToFileBinary(_keyframeBuffer, _bufferIndex);\n+\n+    _recordFile.write(s.c_str(), s.size());\n+}\n+\n+void SessionRecording::readFromPlayback(unsigned char& result) {\n+    _playbackFile.read(reinterpret_cast<char*>(&result), sizeof(unsigned char));\n+}\n+\n+void SessionRecording::readFromPlayback(double& result) {\n+    _playbackFile.read(reinterpret_cast<char*>(&result), sizeof(double));\n+}\n+\n+void SessionRecording::readFromPlayback(float& result) {\n+    _playbackFile.read(reinterpret_cast<char*>(&result), sizeof(float));\n+}\n+\n+void SessionRecording::readFromPlayback(size_t& result) {\n+    _playbackFile.read(reinterpret_cast<char*>(&result), sizeof(size_t));\n+}\n+\n+void SessionRecording::readFromPlayback(bool& result) {\n+    unsigned char b;\n+    _playbackFile.read(reinterpret_cast<char*>(&b), sizeof(unsigned char));\n+    if (b == 0)\n+        result = false;\n+    else if (b == 1)\n+        result = true;\n+    else\n+        LERROR(fmt::format(\"Invalid bool read value at {}\", _playbackLineNum - 1));\n+}\n+\n+void SessionRecording::readFromPlayback(std::string& result) {\n+    result.erase();\n+    size_t strLen;\n+    //Read string length from file\n+    _playbackFile.read(reinterpret_cast<char*>(&strLen), sizeof(strLen));\n+    //Read back full string\n+    char* temp = new char[strLen + 1];\n+    _playbackFile.read(temp, strLen);\n+    temp[strLen] = '\\0';\n+    result = temp;\n+    delete[] temp;\n+}\n+\n+bool SessionRecording::hasCameraChangedFromPrev(datamessagestructures::CameraKeyframe kfNew) {\n+    const double threshold = 1e-2;\n+    bool hasChanged = false;\n+\n+    glm::dvec3 positionDiff = kfNew._position - _prevRecordedCameraKeyframe._position;\n+    if (glm::length(positionDiff) > threshold)\n+        hasChanged = true;\n+\n+    double rotationDiff = dot(kfNew._rotation, _prevRecordedCameraKeyframe._rotation);\n+    if (std::abs(rotationDiff - 1.0) > threshold)\n+        hasChanged = true;\n+\n+    _prevRecordedCameraKeyframe = kfNew;\n+    return hasChanged;\n+}\n+\n+void SessionRecording::saveCameraKeyframe() {\n+    if (_state != sessionState::recording)\n+        return;\n+\n+    SceneGraphNode* focusNode = global::navigationHandler.focusNode();\n+    if (!focusNode) {\n+        return;\n+    }\n+\n+    //Create a camera keyframe, then call to populate it with current position\n+    // & orientation of camera\n+    datamessagestructures::CameraKeyframe kf;\n+    _externInteract.generateCameraKeyframe(kf);\n+\n+    if (/*hasCameraChangedFromPrev(kf)*/true) {\n+        if (isDataModeBinary()) {\n+            _bufferIndex = 0;\n+            _keyframeBuffer[_bufferIndex++] = 'c';\n+\n+            //Writing to internal buffer, and then to file, for performance reasons\n+            writeToFileBuffer(kf._timestamp);\n+            writeToFileBuffer(kf._timestamp - _timestampRecordStarted);\n+            writeToFileBuffer(global::timeManager.time().j2000Seconds());\n+            std::vector<char> kfBuffer;\n+            kf.serialize(kfBuffer);\n+            writeToFileBuffer(kfBuffer);\n+\n+            saveKeyframeToFileBinary(_keyframeBuffer, _bufferIndex);\n+        } else {\n+            std::stringstream keyframeLine = std::stringstream();\n+            //Add simulation timestamp, timestamp relative, simulation time to recording start\n+            keyframeLine << \"camera \";\n+            keyframeLine << kf._timestamp << \" \";\n+            keyframeLine << (kf._timestamp - _timestampRecordStarted) << \" \";\n+            keyframeLine << std::fixed << std::setprecision(3) << global::timeManager.time().j2000Seconds();\n+            keyframeLine << \" \";\n+            //Add camera position\n+            keyframeLine << std::fixed << std::setprecision(7) << kf._position.x << \" \"\n+                << std::fixed << std::setprecision(7) << kf._position.y << \" \"\n+                << std::fixed << std::setprecision(7) << kf._position.z << \" \";\n+            //Add camera rotation\n+            keyframeLine << std::fixed << std::setprecision(7) << kf._rotation.x << \" \"\n+                << std::fixed << std::setprecision(7) << kf._rotation.y << \" \"\n+                << std::fixed << std::setprecision(7) << kf._rotation.z << \" \"\n+                << std::fixed << std::setprecision(7) << kf._rotation.w << \" \";\n+            keyframeLine << std::fixed << std::setprecision(7) << kf._scale << \" \";\n+            if (kf._followNodeRotation)\n+                keyframeLine << \"F \";\n+            else\n+                keyframeLine << \"- \";\n+            keyframeLine << kf._focusNode;\n+\n+            saveKeyframeToFile(keyframeLine.str());\n+        }\n+    }\n+}\n+\n+void SessionRecording::saveTimeKeyframe() {\n+    if (_state != sessionState::recording)\n+        return;\n+\n+    //Create a time keyframe, then call to populate it with current time props\n+    datamessagestructures::TimeKeyframe kf;\n+    _externInteract.generateTimeKeyframe(kf);\n+\n+    if (isDataModeBinary()) {\n+        _bufferIndex = 0;\n+        _keyframeBuffer[_bufferIndex++] = 't';\n+        writeToFileBuffer(kf._timestamp);\n+        writeToFileBuffer(kf._timestamp - _timestampRecordStarted);\n+        writeToFileBuffer(kf._time);\n+        writeToFileBuffer(kf._dt);\n+        writeToFileBuffer(kf._paused);\n+        writeToFileBuffer(kf._requiresTimeJump);\n+\n+        saveKeyframeToFileBinary(_keyframeBuffer, _bufferIndex);\n+    } else {\n+        std::stringstream keyframeLine = std::stringstream();\n+        //Add simulation timestamp, timestamp relative, simulation time to recording start\n+        keyframeLine << \"time \";\n+        keyframeLine << kf._timestamp << \" \";\n+        keyframeLine << (kf._timestamp - _timestampRecordStarted) << \" \";\n+\n+        //keyframeLine << std::fixed << std::setprecision(3) << global::timeManager.time().j2000Seconds();\n+        keyframeLine << std::fixed << std::setprecision(3) << kf._time;\n+\n+        keyframeLine << \" \" << kf._dt;\n+        if (kf._paused)\n+            keyframeLine << \" P\";\n+        else\n+            keyframeLine << \" R\";\n+\n+        if (kf._requiresTimeJump)\n+            keyframeLine << \" J\";\n+        else\n+            keyframeLine << \" -\";\n+\n+        saveKeyframeToFile(keyframeLine.str());\n+    }\n+}\n+\n+void SessionRecording::saveScriptKeyframe(std::string scriptToSave) {\n+    if (_state != sessionState::recording)\n+        return;\n+\n+    datamessagestructures::ScriptMessage sm;\n+    _externInteract.generateScriptMessage(sm, scriptToSave);\n+    if (isDataModeBinary()) {\n+        _bufferIndex = 0;\n+        _keyframeBuffer[_bufferIndex++] = 's';\n+        writeToFileBuffer(sm._timestamp);\n+        writeToFileBuffer(sm._timestamp - _timestampRecordStarted);\n+        writeToFileBuffer(global::timeManager.time().j2000Seconds());\n+        //Write header to file\n+        saveKeyframeToFileBinary(_keyframeBuffer, _bufferIndex);\n+\n+        saveStringToFile(scriptToSave);\n+    } else {\n+        unsigned int numLinesInScript = static_cast<unsigned int>(std::count(scriptToSave.begin(),\n+            scriptToSave.end(), '\\n'));\n+        std::stringstream keyframeLine = std::stringstream();\n+        //Add simulation timestamp, timestamp relative, simulation time to recording start\n+        keyframeLine << \"script \";\n+        keyframeLine << sm._timestamp << \" \";\n+        keyframeLine << (sm._timestamp - _timestampRecordStarted) << \" \";\n+        keyframeLine << std::fixed << std::setprecision(3) << global::timeManager.time().j2000Seconds();\n+        keyframeLine << \" \";\n+        keyframeLine << (numLinesInScript + 1) << \" \";\n+        keyframeLine << scriptToSave;\n+\n+        saveKeyframeToFile(keyframeLine.str());\n+    }\n+}\n+\n+void SessionRecording::preSynchronization() {\n+    if( _state == sessionState::recording ) {","path":"src/interaction/sessionrecording.cpp","position":539,"original_position":539,"commit_id":"42ef1331b775c0ddbd2f6e852f6315e59fa5dedf","original_commit_id":"42ef1331b775c0ddbd2f6e852f6315e59fa5dedf","user":{"login":"GPayne","id":3966290,"node_id":"MDQ6VXNlcjM5NjYyOTA=","avatar_url":"https://avatars0.githubusercontent.com/u/3966290?v=4","gravatar_id":"","url":"https://api.github.com/users/GPayne","html_url":"https://github.com/GPayne","followers_url":"https://api.github.com/users/GPayne/followers","following_url":"https://api.github.com/users/GPayne/following{/other_user}","gists_url":"https://api.github.com/users/GPayne/gists{/gist_id}","starred_url":"https://api.github.com/users/GPayne/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/GPayne/subscriptions","organizations_url":"https://api.github.com/users/GPayne/orgs","repos_url":"https://api.github.com/users/GPayne/repos","events_url":"https://api.github.com/users/GPayne/events{/privacy}","received_events_url":"https://api.github.com/users/GPayne/received_events","type":"User","site_admin":false},"body":"Done","created_at":"2018-11-01T04:59:01Z","updated_at":"2018-11-01T04:59:01Z","html_url":"https://github.com/OpenSpace/OpenSpace/pull/737#discussion_r229939915","pull_request_url":"https://api.github.com/repos/OpenSpace/OpenSpace/pulls/737","author_association":"CONTRIBUTOR","_links":{"self":{"href":"https://api.github.com/repos/OpenSpace/OpenSpace/pulls/comments/229939915"},"html":{"href":"https://github.com/OpenSpace/OpenSpace/pull/737#discussion_r229939915"},"pull_request":{"href":"https://api.github.com/repos/OpenSpace/OpenSpace/pulls/737"}},"in_reply_to_id":229505154},"pull_request":{"url":"https://api.github.com/repos/OpenSpace/OpenSpace/pulls/737","id":224174853,"node_id":"MDExOlB1bGxSZXF1ZXN0MjI0MTc0ODUz","html_url":"https://github.com/OpenSpace/OpenSpace/pull/737","diff_url":"https://github.com/OpenSpace/OpenSpace/pull/737.diff","patch_url":"https://github.com/OpenSpace/OpenSpace/pull/737.patch","issue_url":"https://api.github.com/repos/OpenSpace/OpenSpace/issues/737","number":737,"state":"open","locked":false,"title":"Feature/session recording","user":{"login":"GPayne","id":3966290,"node_id":"MDQ6VXNlcjM5NjYyOTA=","avatar_url":"https://avatars0.githubusercontent.com/u/3966290?v=4","gravatar_id":"","url":"https://api.github.com/users/GPayne","html_url":"https://github.com/GPayne","followers_url":"https://api.github.com/users/GPayne/followers","following_url":"https://api.github.com/users/GPayne/following{/other_user}","gists_url":"https://api.github.com/users/GPayne/gists{/gist_id}","starred_url":"https://api.github.com/users/GPayne/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/GPayne/subscriptions","organizations_url":"https://api.github.com/users/GPayne/orgs","repos_url":"https://api.github.com/users/GPayne/repos","events_url":"https://api.github.com/users/GPayne/events{/privacy}","received_events_url":"https://api.github.com/users/GPayne/received_events","type":"User","site_admin":false},"body":"Session Recording and Playback in its current state. Usable, but needs more testing, and needs improved time interpolation to handle the nonlinear behavior where time accelerates/decelerates to adjust to a new simulated time rate.\r\nRequesting inclusion of this feature in an \"un-advertised\" state.","created_at":"2018-10-19T05:29:19Z","updated_at":"2018-11-01T04:59:01Z","closed_at":null,"merged_at":null,"merge_commit_sha":"b00176482b9eaa2e22c46990a819a01b007ba4a3","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/OpenSpace/OpenSpace/pulls/737/commits","review_comments_url":"https://api.github.com/repos/OpenSpace/OpenSpace/pulls/737/comments","review_comment_url":"https://api.github.com/repos/OpenSpace/OpenSpace/pulls/comments{/number}","comments_url":"https://api.github.com/repos/OpenSpace/OpenSpace/issues/737/comments","statuses_url":"https://api.github.com/repos/OpenSpace/OpenSpace/statuses/42ef1331b775c0ddbd2f6e852f6315e59fa5dedf","head":{"label":"OpenSpace:feature/sessionRecording","ref":"feature/sessionRecording","sha":"42ef1331b775c0ddbd2f6e852f6315e59fa5dedf","user":{"login":"OpenSpace","id":9393286,"node_id":"MDEyOk9yZ2FuaXphdGlvbjkzOTMyODY=","avatar_url":"https://avatars1.githubusercontent.com/u/9393286?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenSpace","html_url":"https://github.com/OpenSpace","followers_url":"https://api.github.com/users/OpenSpace/followers","following_url":"https://api.github.com/users/OpenSpace/following{/other_user}","gists_url":"https://api.github.com/users/OpenSpace/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenSpace/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenSpace/subscriptions","organizations_url":"https://api.github.com/users/OpenSpace/orgs","repos_url":"https://api.github.com/users/OpenSpace/repos","events_url":"https://api.github.com/users/OpenSpace/events{/privacy}","received_events_url":"https://api.github.com/users/OpenSpace/received_events","type":"Organization","site_admin":false},"repo":{"id":38380543,"node_id":"MDEwOlJlcG9zaXRvcnkzODM4MDU0Mw==","name":"OpenSpace","full_name":"OpenSpace/OpenSpace","private":false,"owner":{"login":"OpenSpace","id":9393286,"node_id":"MDEyOk9yZ2FuaXphdGlvbjkzOTMyODY=","avatar_url":"https://avatars1.githubusercontent.com/u/9393286?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenSpace","html_url":"https://github.com/OpenSpace","followers_url":"https://api.github.com/users/OpenSpace/followers","following_url":"https://api.github.com/users/OpenSpace/following{/other_user}","gists_url":"https://api.github.com/users/OpenSpace/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenSpace/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenSpace/subscriptions","organizations_url":"https://api.github.com/users/OpenSpace/orgs","repos_url":"https://api.github.com/users/OpenSpace/repos","events_url":"https://api.github.com/users/OpenSpace/events{/privacy}","received_events_url":"https://api.github.com/users/OpenSpace/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/OpenSpace/OpenSpace","description":"This is the official GitHub repository for OpenSpace: an open source astrovisualization project. For instructions on how to build and run OpenSpace, see the Getting Started Guides on the wiki page.","fork":false,"url":"https://api.github.com/repos/OpenSpace/OpenSpace","forks_url":"https://api.github.com/repos/OpenSpace/OpenSpace/forks","keys_url":"https://api.github.com/repos/OpenSpace/OpenSpace/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenSpace/OpenSpace/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenSpace/OpenSpace/teams","hooks_url":"https://api.github.com/repos/OpenSpace/OpenSpace/hooks","issue_events_url":"https://api.github.com/repos/OpenSpace/OpenSpace/issues/events{/number}","events_url":"https://api.github.com/repos/OpenSpace/OpenSpace/events","assignees_url":"https://api.github.com/repos/OpenSpace/OpenSpace/assignees{/user}","branches_url":"https://api.github.com/repos/OpenSpace/OpenSpace/branches{/branch}","tags_url":"https://api.github.com/repos/OpenSpace/OpenSpace/tags","blobs_url":"https://api.github.com/repos/OpenSpace/OpenSpace/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenSpace/OpenSpace/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenSpace/OpenSpace/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenSpace/OpenSpace/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenSpace/OpenSpace/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenSpace/OpenSpace/languages","stargazers_url":"https://api.github.com/repos/OpenSpace/OpenSpace/stargazers","contributors_url":"https://api.github.com/repos/OpenSpace/OpenSpace/contributors","subscribers_url":"https://api.github.com/repos/OpenSpace/OpenSpace/subscribers","subscription_url":"https://api.github.com/repos/OpenSpace/OpenSpace/subscription","commits_url":"https://api.github.com/repos/OpenSpace/OpenSpace/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenSpace/OpenSpace/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenSpace/OpenSpace/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenSpace/OpenSpace/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenSpace/OpenSpace/contents/{+path}","compare_url":"https://api.github.com/repos/OpenSpace/OpenSpace/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenSpace/OpenSpace/merges","archive_url":"https://api.github.com/repos/OpenSpace/OpenSpace/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenSpace/OpenSpace/downloads","issues_url":"https://api.github.com/repos/OpenSpace/OpenSpace/issues{/number}","pulls_url":"https://api.github.com/repos/OpenSpace/OpenSpace/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenSpace/OpenSpace/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenSpace/OpenSpace/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenSpace/OpenSpace/labels{/name}","releases_url":"https://api.github.com/repos/OpenSpace/OpenSpace/releases{/id}","deployments_url":"https://api.github.com/repos/OpenSpace/OpenSpace/deployments","created_at":"2015-07-01T15:59:54Z","updated_at":"2018-10-31T22:26:36Z","pushed_at":"2018-11-01T00:35:42Z","git_url":"git://github.com/OpenSpace/OpenSpace.git","ssh_url":"git@github.com:OpenSpace/OpenSpace.git","clone_url":"https://github.com/OpenSpace/OpenSpace.git","svn_url":"https://github.com/OpenSpace/OpenSpace","homepage":"http://openspaceproject.com","size":292943,"stargazers_count":125,"watchers_count":125,"language":"C++","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":24,"mirror_url":null,"archived":false,"open_issues_count":203,"license":{"key":"mit","name":"MIT License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"forks":24,"open_issues":203,"watchers":125,"default_branch":"master"}},"base":{"label":"OpenSpace:master","ref":"master","sha":"700fd60295c142ee2c1d01e3e53d6fdc1a44486b","user":{"login":"OpenSpace","id":9393286,"node_id":"MDEyOk9yZ2FuaXphdGlvbjkzOTMyODY=","avatar_url":"https://avatars1.githubusercontent.com/u/9393286?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenSpace","html_url":"https://github.com/OpenSpace","followers_url":"https://api.github.com/users/OpenSpace/followers","following_url":"https://api.github.com/users/OpenSpace/following{/other_user}","gists_url":"https://api.github.com/users/OpenSpace/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenSpace/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenSpace/subscriptions","organizations_url":"https://api.github.com/users/OpenSpace/orgs","repos_url":"https://api.github.com/users/OpenSpace/repos","events_url":"https://api.github.com/users/OpenSpace/events{/privacy}","received_events_url":"https://api.github.com/users/OpenSpace/received_events","type":"Organization","site_admin":false},"repo":{"id":38380543,"node_id":"MDEwOlJlcG9zaXRvcnkzODM4MDU0Mw==","name":"OpenSpace","full_name":"OpenSpace/OpenSpace","private":false,"owner":{"login":"OpenSpace","id":9393286,"node_id":"MDEyOk9yZ2FuaXphdGlvbjkzOTMyODY=","avatar_url":"https://avatars1.githubusercontent.com/u/9393286?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenSpace","html_url":"https://github.com/OpenSpace","followers_url":"https://api.github.com/users/OpenSpace/followers","following_url":"https://api.github.com/users/OpenSpace/following{/other_user}","gists_url":"https://api.github.com/users/OpenSpace/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenSpace/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenSpace/subscriptions","organizations_url":"https://api.github.com/users/OpenSpace/orgs","repos_url":"https://api.github.com/users/OpenSpace/repos","events_url":"https://api.github.com/users/OpenSpace/events{/privacy}","received_events_url":"https://api.github.com/users/OpenSpace/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/OpenSpace/OpenSpace","description":"This is the official GitHub repository for OpenSpace: an open source astrovisualization project. For instructions on how to build and run OpenSpace, see the Getting Started Guides on the wiki page.","fork":false,"url":"https://api.github.com/repos/OpenSpace/OpenSpace","forks_url":"https://api.github.com/repos/OpenSpace/OpenSpace/forks","keys_url":"https://api.github.com/repos/OpenSpace/OpenSpace/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenSpace/OpenSpace/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenSpace/OpenSpace/teams","hooks_url":"https://api.github.com/repos/OpenSpace/OpenSpace/hooks","issue_events_url":"https://api.github.com/repos/OpenSpace/OpenSpace/issues/events{/number}","events_url":"https://api.github.com/repos/OpenSpace/OpenSpace/events","assignees_url":"https://api.github.com/repos/OpenSpace/OpenSpace/assignees{/user}","branches_url":"https://api.github.com/repos/OpenSpace/OpenSpace/branches{/branch}","tags_url":"https://api.github.com/repos/OpenSpace/OpenSpace/tags","blobs_url":"https://api.github.com/repos/OpenSpace/OpenSpace/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenSpace/OpenSpace/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenSpace/OpenSpace/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenSpace/OpenSpace/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenSpace/OpenSpace/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenSpace/OpenSpace/languages","stargazers_url":"https://api.github.com/repos/OpenSpace/OpenSpace/stargazers","contributors_url":"https://api.github.com/repos/OpenSpace/OpenSpace/contributors","subscribers_url":"https://api.github.com/repos/OpenSpace/OpenSpace/subscribers","subscription_url":"https://api.github.com/repos/OpenSpace/OpenSpace/subscription","commits_url":"https://api.github.com/repos/OpenSpace/OpenSpace/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenSpace/OpenSpace/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenSpace/OpenSpace/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenSpace/OpenSpace/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenSpace/OpenSpace/contents/{+path}","compare_url":"https://api.github.com/repos/OpenSpace/OpenSpace/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenSpace/OpenSpace/merges","archive_url":"https://api.github.com/repos/OpenSpace/OpenSpace/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenSpace/OpenSpace/downloads","issues_url":"https://api.github.com/repos/OpenSpace/OpenSpace/issues{/number}","pulls_url":"https://api.github.com/repos/OpenSpace/OpenSpace/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenSpace/OpenSpace/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenSpace/OpenSpace/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenSpace/OpenSpace/labels{/name}","releases_url":"https://api.github.com/repos/OpenSpace/OpenSpace/releases{/id}","deployments_url":"https://api.github.com/repos/OpenSpace/OpenSpace/deployments","created_at":"2015-07-01T15:59:54Z","updated_at":"2018-10-31T22:26:36Z","pushed_at":"2018-11-01T00:35:42Z","git_url":"git://github.com/OpenSpace/OpenSpace.git","ssh_url":"git@github.com:OpenSpace/OpenSpace.git","clone_url":"https://github.com/OpenSpace/OpenSpace.git","svn_url":"https://github.com/OpenSpace/OpenSpace","homepage":"http://openspaceproject.com","size":292943,"stargazers_count":125,"watchers_count":125,"language":"C++","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":24,"mirror_url":null,"archived":false,"open_issues_count":203,"license":{"key":"mit","name":"MIT License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"forks":24,"open_issues":203,"watchers":125,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/OpenSpace/OpenSpace/pulls/737"},"html":{"href":"https://github.com/OpenSpace/OpenSpace/pull/737"},"issue":{"href":"https://api.github.com/repos/OpenSpace/OpenSpace/issues/737"},"comments":{"href":"https://api.github.com/repos/OpenSpace/OpenSpace/issues/737/comments"},"review_comments":{"href":"https://api.github.com/repos/OpenSpace/OpenSpace/pulls/737/comments"},"review_comment":{"href":"https://api.github.com/repos/OpenSpace/OpenSpace/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/OpenSpace/OpenSpace/pulls/737/commits"},"statuses":{"href":"https://api.github.com/repos/OpenSpace/OpenSpace/statuses/42ef1331b775c0ddbd2f6e852f6315e59fa5dedf"}},"author_association":"CONTRIBUTOR"}}
{"actor":{"display_login":"GPayne"}}