diff --git a/modules/softwareintegration/CMakeLists.txt b/modules/softwareintegration/CMakeLists.txt index c557b3a213..abdb75772a 100644 --- a/modules/softwareintegration/CMakeLists.txt +++ b/modules/softwareintegration/CMakeLists.txt @@ -26,23 +26,25 @@ include(${OPENSPACE_CMAKE_EXT_DIR}/module_definition.cmake) set(HEADER_FILES ${CMAKE_CURRENT_SOURCE_DIR}/pointdatamessagehandler.h - ${CMAKE_CURRENT_SOURCE_DIR}/simp.h ${CMAKE_CURRENT_SOURCE_DIR}/softwareintegrationmodule.h - ${CMAKE_CURRENT_SOURCE_DIR}/syncabledatastorage.h ${CMAKE_CURRENT_SOURCE_DIR}/network/softwareconnection.h ${CMAKE_CURRENT_SOURCE_DIR}/network/networkengine.h ${CMAKE_CURRENT_SOURCE_DIR}/rendering/renderablepointscloud.h + ${CMAKE_CURRENT_SOURCE_DIR}/simp.h + ${CMAKE_CURRENT_SOURCE_DIR}/syncablefloatdatastorage.h + ${CMAKE_CURRENT_SOURCE_DIR}/interruptibleconcurrentqueue.h + ${CMAKE_CURRENT_SOURCE_DIR}/interruptibleconcurrentqueue.inl ) source_group("Header Files" FILES ${HEADER_FILES}) set(SOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/pointdatamessagehandler.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/simp.cpp ${CMAKE_CURRENT_SOURCE_DIR}/softwareintegrationmodule.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/syncabledatastorage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/network/softwareconnection.cpp ${CMAKE_CURRENT_SOURCE_DIR}/network/networkengine.cpp ${CMAKE_CURRENT_SOURCE_DIR}/rendering/renderablepointscloud.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/simp.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/syncablefloatdatastorage.cpp ) source_group("Source Files" FILES ${SOURCE_FILES}) diff --git a/modules/softwareintegration/interruptibleconcurrentqueue.h b/modules/softwareintegration/interruptibleconcurrentqueue.h new file mode 100644 index 0000000000..cc014fd662 --- /dev/null +++ b/modules/softwareintegration/interruptibleconcurrentqueue.h @@ -0,0 +1,54 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2022 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#ifndef __OPENSPACE_MODULE_SOFTWAREINTEGRATION___INTERRUPTABLECONCURRENTQUEUE___H__ +#define __OPENSPACE_MODULE_SOFTWAREINTEGRATION___INTERRUPTABLECONCURRENTQUEUE___H__ + +#include +#include +#include + +namespace openspace { + +template +class InterruptibleConcurrentQueue { +public: + T pop(); + void interrupt(); + void push(const T& item); + void push(T&& item); + +private: + std::atomic_bool _interrupted{ false }; + std::queue _queue; + mutable std::mutex _mutex; + mutable std::condition_variable _notifyForPop; +}; + + +} // namespace openspace + +#include "interruptibleconcurrentqueue.inl" + +#endif // __OPENSPACE_MODULE_SOFTWAREINTEGRATION___INTERRUPTABLECONCURRENTQUEUE___H__ diff --git a/modules/softwareintegration/interruptibleconcurrentqueue.inl b/modules/softwareintegration/interruptibleconcurrentqueue.inl new file mode 100644 index 0000000000..c3b50c1462 --- /dev/null +++ b/modules/softwareintegration/interruptibleconcurrentqueue.inl @@ -0,0 +1,73 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2022 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +namespace openspace { + +template +T InterruptibleConcurrentQueue::pop() { + auto isInterruptedOrNotEmpty = [this]() { + if (_interrupted) return true; + return !_queue.empty(); + }; + + std::unique_lock lock(_mutex); + + // Block execution until interrupted or queue not empty + if (!isInterruptedOrNotEmpty()) { + _notifyForPop.wait(lock, isInterruptedOrNotEmpty); + } + + if (_interrupted) throw ghoul::RuntimeError{""}; + + auto item = _queue.front(); + _queue.pop(); + + return item; +} + +template +void InterruptibleConcurrentQueue::interrupt() { + _interrupted = true; + _notifyForPop.notify_all(); +} + +template +void InterruptibleConcurrentQueue::push(const T& item) { + if (_interrupted) return; + std::unique_lock mlock(_mutex); + _queue.push(item); + mlock.unlock(); + _notifyForPop.notify_one(); +} + +template +void InterruptibleConcurrentQueue::push(T&& item) { + if (_interrupted) return; + std::unique_lock mlock(_mutex); + _queue.push(std::move(item)); + mlock.unlock(); + _notifyForPop.notify_one(); +} + +} // namespace openspace diff --git a/modules/softwareintegration/network/networkengine.cpp b/modules/softwareintegration/network/networkengine.cpp index 4f9b2f918b..abbe20e563 100644 --- a/modules/softwareintegration/network/networkengine.cpp +++ b/modules/softwareintegration/network/networkengine.cpp @@ -24,16 +24,11 @@ #include -#include #include #include -#include #include #include -// #include -// #include #include -// #include namespace { @@ -41,241 +36,196 @@ namespace { } // namespace namespace openspace { + NetworkEngine::NetworkEngine(const int port) : _port{port} {} +NetworkEngine::~NetworkEngine() { + stop(); +} + void NetworkEngine::start() { _socketServer.listen(_port); - _serverThread = std::thread([this]() { handleNewPeers(); }); + _serverThread = std::thread([this]() { handleNewSoftwareConnections(); }); _eventLoopThread = std::thread([this]() { eventLoop(); }); } void NetworkEngine::stop() { - for (auto [id, peer] : _peers) { - disconnect(peer); + _shouldStopServerThread = true; + + { + std::lock_guard guardSoftwareConnections(_softwareConnectionsMutex); + for (auto& [id, connectionPtr] : _softwareConnections) { + SoftwareConnection::NetworkEngineFriends::stopThread(connectionPtr); + } } - - _shouldStop = true; + + _incomingMessages.interrupt(); + + _shouldStopEventThread = true; _socketServer.close(); + _softwareConnections.clear(); if (_serverThread.joinable()) { _serverThread.join(); - } + } if (_eventLoopThread.joinable()) { _eventLoopThread.join(); } } -void NetworkEngine::update() { - _pointDataMessageHandler.preSyncUpdate(); +void NetworkEngine::postSync() { + _pointDataMessageHandler.postSync(); } -bool NetworkEngine::isConnected(const std::shared_ptr peer) const { - return peer->status == Peer::Status::Connected; -} - -std::shared_ptr NetworkEngine::peer(size_t id) { - // std::lock_guard lock(_peerListMutex); - auto it = _peers.find(id); - if (it == _peers.end()) return nullptr; - return it->second; -} - -void NetworkEngine::disconnect(std::shared_ptr peer) { - if (peer == nullptr) return; - if (!isConnected(peer)) return; // Already disconnected - - --_nConnections; - peer->status = Peer::Status::Disconnected; - peer->connection.disconnect(); - - // auto sgnIterator = peer->sceneGraphNodes.begin(); - // while (sgnIterator != peer->sceneGraphNodes.end()) { - // LDEBUG(fmt::format("t{}: disconnect() - removing SGN '{}'", std::this_thread::get_id(), *sgnIterator)); - // removeSceneGraphNode(*sgnIterator); - // peer->sceneGraphNodes.erase(sgnIterator); - // ++sgnIterator; - // } - - if (peer->thread.joinable()) peer->thread.join(); - _peers.erase(peer->id); -} - -void NetworkEngine::eventLoop() { - while (!_shouldStop) { - if (!_incomingMessages.empty()) { - auto pm = _incomingMessages.pop(); - handlePeerMessage(std::move(pm)); - } - } -} - -void NetworkEngine::handleNewPeers() { - while (!_shouldStop) { - std::unique_ptr socket = - _socketServer.awaitPendingTcpSocket(); +void NetworkEngine::handleNewSoftwareConnections() { + while (!_shouldStopServerThread) { + std::unique_ptr socket = _socketServer.awaitPendingTcpSocket(); if (!socket) return; socket->startStreams(); - std::shared_ptr p = std::make_shared(Peer{ - _nextConnectionId++, - "", - {}, - SoftwareConnection(std::move(socket)), - {}, - Peer::Status::Connected - }); - auto [it, peerInserted] = _peers.emplace(p->id, p); - if (peerInserted){ - // The thread 'it.first->second->thread' will run 'peerEventLoop()' as fast as it can until stopped - it->second->thread = std::thread( - [this, &p] () { - peerEventLoop(p->id); - }); + auto p = std::make_shared(SoftwareConnection{ std::move(socket) }); + std::lock_guard guard(_softwareConnectionsMutex); + auto [it, peerInserted] = _softwareConnections.emplace(p->id(), p); + if (peerInserted) { + auto& connectionPtr = it->second; + auto thread = std::thread{ + [this, &connectionPtr] { + peerEventLoop(connectionPtr->id()); + } + }; + connectionPtr->setThread(thread); } } } -void NetworkEngine::peerEventLoop(size_t id) { +void NetworkEngine::peerEventLoop(size_t connection_id) { using namespace std::literals::chrono_literals; + auto connectionPtr = getSoftwareConnection(connection_id); - while (!_shouldStop) { - std::shared_ptr p = peer(id); - - if (!p || !p->connection.isConnectedOrConnecting() - || p->status == Peer::Status::Disconnected) { - break; - } - + while (!connectionPtr->shouldStopThread()) { try { - SoftwareConnection::Message m = p->connection.receiveMessageFromSoftware(); - - _incomingMessages.push({ id, m }); + SoftwareConnection::Message m = connectionPtr->receiveMessageFromSoftware(); + _incomingMessages.push({ connection_id, m }); } catch (const SoftwareConnection::SoftwareConnectionLostError& err) { - if (p->status == Peer::Status::Connected) { - LERROR(fmt::format("Connection lost to {}: {}", p->id, err.message)); - disconnect(p); + if (connectionPtr->shouldStopThread()) break; + + if (connectionPtr && (!connectionPtr->shouldStopThread() || !connectionPtr->isConnectedOrConnecting())) { + LDEBUG(fmt::format("Connection lost to {}: {}", connection_id, err.message)); + _incomingMessages.push({ + connection_id, + SoftwareConnection::Message{ simp::MessageType::Disconnection } + }); } break; } } } -void NetworkEngine::handlePeerMessage(PeerMessage peerMessage) { - const size_t peerId = peerMessage.peerId; - std::shared_ptr peerPtr = peer(peerId); - if(peerPtr == nullptr) { - LERROR(fmt::format("No peer with peerId {} could be found", peerId)); +void NetworkEngine::eventLoop() { + while (!_shouldStopEventThread) { + // The call to "pop" below will block execution + // on this thread until interrupt is called + try { + auto pm = _incomingMessages.pop(); + handleIncomingMessage(pm); + } + catch (const ghoul::RuntimeError&) { + break; + } + } +} + +std::shared_ptr NetworkEngine::getSoftwareConnection(size_t id) { + std::lock_guard guard(_softwareConnectionsMutex); + auto it = _softwareConnections.find(id); + if (it == _softwareConnections.end()) return nullptr; + return it->second; +} + +void NetworkEngine::handleIncomingMessage(IncomingMessage incomingMessage) { + auto connectionPtr = getSoftwareConnection(incomingMessage.connection_id); + + if(!connectionPtr) { + LDEBUG(fmt::format("Trying to handle message from disconnected peer. Aborting.")); return; } - const simp::MessageType messageType = peerMessage.message.type; - std::vector& message = peerMessage.message.content; + const simp::MessageType messageType = incomingMessage.message.type; + std::vector& message = incomingMessage.message.content; switch (messageType) { - case simp::MessageType::Connection: - { - const std::string software{ message.begin(), message.end() }; - LINFO(fmt::format("OpenSpace has connected with {} through socket", software)); - // Send back message to software to complete handshake - peerPtr->connection.sendMessage(simp::formatConnectionMessage(software)); - break; - } - case simp::MessageType::ReadPointData: { - LDEBUG("Message recieved.. Point Data"); - _pointDataMessageHandler.handlePointDataMessage(message, peerPtr->connection, peerPtr->sceneGraphNodes); - break; - } - case simp::MessageType::RemoveSceneGraphNode: { - const std::string identifier(message.begin(), message.end()); - LDEBUG(fmt::format("Message recieved.. Delete SGN: {}", identifier)); - removeSceneGraphNode(identifier); + case simp::MessageType::Connection: { + size_t offset = 0; + const std::string software = simp::readString(message, offset); - auto sgnIterator = peerPtr->sceneGraphNodes.begin(); - while (sgnIterator != peerPtr->sceneGraphNodes.end()) { - if (*sgnIterator == identifier) { - peerPtr->sceneGraphNodes.erase(sgnIterator); - break; - } - ++sgnIterator; + // Send back message to software to complete handshake + connectionPtr->sendMessage(simp::formatConnectionMessage(software)); + LINFO(fmt::format("OpenSpace has connected with {} through socket", software)); + break; + } + case simp::MessageType::PointData: { + LDEBUG("Message recieved.. Point Data"); + _pointDataMessageHandler.handlePointDataMessage(message, connectionPtr); + break; + } + case simp::MessageType::RemoveSceneGraphNode: { + LDEBUG(fmt::format("Message recieved.. Remove SGN")); + _pointDataMessageHandler.handleRemoveSGNMessage(message, connectionPtr); + break; + } + case simp::MessageType::Color: { + LDEBUG(fmt::format("Message recieved.. New Color")); + _pointDataMessageHandler.handleFixedColorMessage(message, connectionPtr); + break; + } + case simp::MessageType::Colormap: { + LDEBUG(fmt::format("Message recieved.. New Colormap")); + _pointDataMessageHandler.handleColormapMessage(message, connectionPtr); + break; + } + case simp::MessageType::AttributeData: { + LDEBUG(fmt::format("Message recieved.. New Colormap scalar values")); + _pointDataMessageHandler.handleAttributeDataMessage(message, connectionPtr); + break; + } + case simp::MessageType::Opacity: { + LDEBUG(fmt::format("Message recieved.. New Opacity")); + _pointDataMessageHandler.handleOpacityMessage(message, connectionPtr); + break; + } + case simp::MessageType::Size: { + LDEBUG(fmt::format("Message recieved.. New Size")); + _pointDataMessageHandler.handleFixedPointSizeMessage(message, connectionPtr); + break; + } + case simp::MessageType::Visibility: { + LDEBUG(fmt::format("Message recieved.. New Visibility")); + _pointDataMessageHandler.handleVisiblityMessage(message, connectionPtr); + break; + } + case simp::MessageType::Disconnection: { + LDEBUG(fmt::format("Message recieved.. Disconnect software connection: {}", incomingMessage.connection_id)); + std::lock_guard guard(_softwareConnectionsMutex); + if (_softwareConnections.count(incomingMessage.connection_id)) { + _softwareConnections.erase(incomingMessage.connection_id); + } + SoftwareConnection::NetworkEngineFriends::stopThread(connectionPtr); + break; + } + default: { + LERROR(fmt::format( + "Unsupported message type: {}", incomingMessage.message.rawMessageType + )); + break; } - break; } - case simp::MessageType::Color: { - const std::string colorMessage(message.begin(), message.end()); - LDEBUG(fmt::format("Message recieved.. New Color: {}", colorMessage)); - - _pointDataMessageHandler.handleColorMessage(message); - break; - } - case simp::MessageType::ColorMap: { - const std::string colorMapMessage(message.begin(), message.end()); - LDEBUG(fmt::format("Message recieved.. New ColorMap")); - - _pointDataMessageHandler.handleColorMapMessage(message); - break; - } - case simp::MessageType::Opacity: { - const std::string opacityMessage(message.begin(), message.end()); - LDEBUG(fmt::format("Message recieved.. New Opacity: {}", opacityMessage)); - - _pointDataMessageHandler.handleOpacityMessage(message); - break; - } - case simp::MessageType::Size: { - const std::string sizeMessage(message.begin(), message.end()); - LDEBUG(fmt::format("Message recieved.. New Size: {}", sizeMessage)); - - _pointDataMessageHandler.handlePointSizeMessage(message); - break; - } - case simp::MessageType::Visibility: { - const std::string visibilityMessage(message.begin(), message.end()); - LDEBUG(fmt::format("Message recieved.. New Visibility: {}", visibilityMessage)); - - _pointDataMessageHandler.handleVisiblityMessage(message); - break; - } - case simp::MessageType::Disconnection: { - LDEBUG(fmt::format("Message recieved.. Disconnect peer: {}", peerPtr->id)); - disconnect(peerPtr); - break; - } - default: - LERROR(fmt::format( - "Unsupported message type: {}", static_cast(messageType) - )); - break; - } -} - -void NetworkEngine::removeSceneGraphNode(const std::string &identifier) { - const std::string currentAnchor = - global::navigationHandler->orbitalNavigator().anchorNode()->identifier(); - - if (currentAnchor == identifier) { - // If the deleted node is the current anchor, first change focus to the Sun - openspace::global::scriptEngine->queueScript( - "openspace.setPropertyValueSingle('NavigationHandler.OrbitalNavigator.Anchor', 'Sun')" - "openspace.setPropertyValueSingle('NavigationHandler.OrbitalNavigator.Aim', '')", - scripting::ScriptEngine::RemoteScripting::Yes - ); - } - openspace::global::scriptEngine->queueScript( - "openspace.removeSceneGraphNode('" + identifier + "');", - scripting::ScriptEngine::RemoteScripting::Yes - ); - - // TODO: remove from sceneGraphNodes - // peer->sceneGraphNodes.erase(sgnIterator); - - LDEBUG(fmt::format("Scene graph node '{}' removed.", identifier)); } } // namespace openspace diff --git a/modules/softwareintegration/network/networkengine.h b/modules/softwareintegration/network/networkengine.h index dfd23093e7..b8d4751670 100644 --- a/modules/softwareintegration/network/networkengine.h +++ b/modules/softwareintegration/network/networkengine.h @@ -27,7 +27,8 @@ #include #include -#include +#include +#include #include namespace openspace { @@ -35,61 +36,42 @@ namespace openspace { class NetworkEngine { public: NetworkEngine(const int port = 4700); + ~NetworkEngine(); - struct Peer { - enum class Status : uint32_t { - Disconnected = 0, - Connected - }; - - size_t id; - std::string name; - std::thread thread; - - SoftwareConnection connection; - std::list sceneGraphNodes; // TODO: Change to std::unordered_set? - Status status; - }; - - struct PeerMessage { - size_t peerId; - SoftwareConnection::Message message; + struct IncomingMessage { + size_t connection_id{1}; + SoftwareConnection::Message message{ simp::MessageType::Unknown }; }; void start(); void stop(); - void update(); + void postSync(); private: - void disconnect(std::shared_ptr peer); - void handleNewPeers(); - void peerEventLoop(size_t id); - void handlePeerMessage(PeerMessage peerMessage); + void handleNewSoftwareConnections(); + void handleIncomingMessage(IncomingMessage incomingMessage); + void peerEventLoop(size_t connection_id); void eventLoop(); - bool isConnected(const std::shared_ptr peer) const; + // The destuction of the object a shared_ptr is pointing to, occurs when the pointer no longer has any owners + std::shared_ptr getSoftwareConnection(size_t id); - void removeSceneGraphNode(const std::string &identifier); - - std::shared_ptr peer(size_t id); - - std::unordered_map> _peers; - mutable std::mutex _peerListMutex; + std::unordered_map> _softwareConnections; + std::mutex _softwareConnectionsMutex; ghoul::io::TcpSocketServer _socketServer; - size_t _nextConnectionId = 1; - std::atomic_size_t _nConnections = 0; std::thread _serverThread; + std::atomic_bool _shouldStopServerThread = false; std::thread _eventLoopThread; + std::atomic_bool _shouldStopEventThread = false; - std::atomic_bool _shouldStop = false; const int _port; // Message handlers PointDataMessageHandler _pointDataMessageHandler; - ConcurrentQueue _incomingMessages; + InterruptibleConcurrentQueue _incomingMessages; }; } // namespace openspace diff --git a/modules/softwareintegration/network/softwareconnection.cpp b/modules/softwareintegration/network/softwareconnection.cpp index 135781e681..5b42dfac3c 100644 --- a/modules/softwareintegration/network/softwareconnection.cpp +++ b/modules/softwareintegration/network/softwareconnection.cpp @@ -24,11 +24,12 @@ #include -#include #include #include #include #include +#include +#include namespace { constexpr const char* _loggerCat = "SoftwareConnection"; @@ -36,52 +37,172 @@ namespace { namespace openspace { -SoftwareConnection::Message::Message(simp::MessageType type, std::vector content) - : type{ type }, content{ std::move(content) } -{} +std::atomic_size_t SoftwareConnection::_nextConnectionId = 1; SoftwareConnection::SoftwareConnectionLostError::SoftwareConnectionLostError(const std::string& msg) : ghoul::RuntimeError(fmt::format("{}{}", "Software connection lost", msg), "SoftwareConnection") {} SoftwareConnection::SoftwareConnection(std::unique_ptr socket) - : _socket(std::move(socket)) + : _id{ _nextConnectionId++ }, _socket{ std::move(socket) }, _sceneGraphNodes{}, + _thread{}, _shouldStopThread{ false } {} -bool SoftwareConnection::isConnected() const { - return _socket->isConnected(); -} +SoftwareConnection::SoftwareConnection(SoftwareConnection&& sc) + : _id{ std::move(sc._id) }, _socket{ std::move(sc._socket) }, + _isConnected{ sc._isConnected }, _sceneGraphNodes{ std::move(sc._sceneGraphNodes) }, + _thread{}, _shouldStopThread{ false } +{} -bool SoftwareConnection::isConnectedOrConnecting() const { - return _socket->isConnected() || _socket->isConnecting(); -} +SoftwareConnection::~SoftwareConnection() { + LINFO(fmt::format("Remove software connection {}", _id)); -bool SoftwareConnection::sendMessage(std::string message) { - LDEBUG(fmt::format("In SoftwareConnection::sendMessage()", 0)); + if (!_isConnected) return; + _isConnected = false; - if (_isListening) { - if (!_socket->put(message.data(), message.size())) { - return false; - } - LDEBUG(fmt::format("Message sent: {}", message)); - } - else { - _isListening = true; - return false; + for (auto& identifier : _sceneGraphNodes) { + removePropertySubscriptions(identifier); } - return true; -} - -void SoftwareConnection::disconnect() { if (_socket) { - sendMessage(simp::formatDisconnectionMessage()); _socket->disconnect(); } } -ghoul::io::TcpSocket* SoftwareConnection::socket() { - return _socket.get(); +void SoftwareConnection::addPropertySubscription( + const std::string propertyName, + const std::string& identifier, + std::function newHandler +) { + // Get renderable + auto r = renderable(identifier); + if (!r) { + LWARNING(fmt::format( + "Couldn't add property subscription. Renderable \"{}\" doesn't exist", + identifier + )); + return; + } + + if (!r->hasProperty(propertyName)) { + LWARNING(fmt::format( + "Couldn't add property subscription. Property \"{}\" doesn't exist on \"{}\"", + propertyName, identifier + )); + return; + } + + auto property = r->property(propertyName); + OnChangeHandle onChangeHandle = property->onChange(newHandler); + + auto propertySubscriptions = _subscribedProperties.find(identifier); + if (propertySubscriptions != _subscribedProperties.end()) { + // At least one property have been subscribed to on this SGN + auto propertySubscription = propertySubscriptions->second.find(propertyName); + if (propertySubscription != propertySubscriptions->second.end()) { + // Property subscription already exists + removeExistingPropertySubscription(identifier, property, onChangeHandle); + propertySubscription->second = onChangeHandle; + } + else { + // Property subscription doesn't exist + propertySubscriptions->second.emplace(propertyName, onChangeHandle); + } + } + else { + // No properties have been subscribed to on this SGN + PropertySubscriptions newPropertySubscriptionMap{ { propertyName, onChangeHandle } }; + _subscribedProperties.emplace(identifier, std::move(newPropertySubscriptionMap)); + } +} + +void SoftwareConnection::removePropertySubscriptions(const std::string& identifier) { + // Get renderable + auto r = renderable(identifier); + if (!r) { + LWARNING(fmt::format( + "Couldn't remove property subscriptions, renderable {} doesn't exist", + identifier + )); + return; + } + + auto propertySubscriptions = _subscribedProperties.find(identifier); + + if (propertySubscriptions == _subscribedProperties.end()) return; + + for (auto& [propertyName, onChangeHandle] : propertySubscriptions->second) { + if (!r->hasProperty(propertyName)) { + LWARNING(fmt::format( + "Couldn't remove property subscription. Property \"{}\" doesn't exist on \"{}\"", + propertyName, identifier + )); + continue; + } + + auto propertySubscription = propertySubscriptions->second.find(propertyName); + if (propertySubscription == propertySubscriptions->second.end()) continue; + + auto property = r->property(propertyName); + removeExistingPropertySubscription(identifier, property, onChangeHandle); + } + + _subscribedProperties.erase(propertySubscriptions); +} + +void SoftwareConnection::removeExistingPropertySubscription( + const std::string& identifier, + properties::Property *property, + OnChangeHandle onChangeHandle +) { + property->removeOnChange(onChangeHandle); + + auto propertySubscriptions = _subscribedProperties.find(identifier); + propertySubscriptions->second.erase(property->identifier()); +} + +void SoftwareConnection::disconnect() { + _socket->disconnect(); +} + +bool SoftwareConnection::isConnected() const { + return _isConnected && _socket && _socket->isConnected(); +} + +bool SoftwareConnection::isConnectedOrConnecting() const { + return _isConnected && _socket && (_socket->isConnected() || _socket->isConnecting()); +} + +bool SoftwareConnection::sendMessage(const std::string& message) { + try { + if (!_socket || !isConnected()) { + throw SoftwareConnectionLostError("Connection lost..."); + } + if (_socket->put(message.data(), message.size())) { + LDEBUG(fmt::format("Message sent: {}", message)); + return true; + } + } + catch (const SoftwareConnectionLostError& err) { + LERROR(fmt::format("Couldn't send message: \"{}\", due to: {}", message, err.message)); + } + catch (const std::exception& err) { + LERROR(fmt::format("Couldn't send message: \"{}\", due to: {}", message, err.what())); + } + + return false; +} + +void SoftwareConnection::addSceneGraphNode(const std::string& identifier) { + _sceneGraphNodes.insert(identifier); +} + +void SoftwareConnection::removeSceneGraphNode(const std::string& identifier) { + size_t amountRemoved = _sceneGraphNodes.erase(identifier); + + if (amountRemoved > 0) { + removePropertySubscriptions(identifier); + } } /** @@ -102,15 +223,14 @@ SoftwareConnection::Message SoftwareConnection::receiveMessageFromSoftware() { throw SoftwareConnectionLostError("Failed to read header from socket. Disconnecting."); } - // Read and convert version number: Byte 0-2 - std::string version; + // Read the protocol version number: Byte 0-2 + std::string protocolVersionIn; for (int i = 0; i < 3; i++) { - version.push_back(headerBuffer[i]); + protocolVersionIn.push_back(headerBuffer[i]); } - const float protocolVersionIn = std::stof(version); // Make sure that header matches the protocol version - if (abs(protocolVersionIn - simp::ProtocolVersion) >= FLT_EPSILON) { + if (protocolVersionIn != simp::ProtocolVersion) { throw SoftwareConnectionLostError(fmt::format( "Protocol versions do not match. Remote version: {}, Local version: {}", protocolVersionIn, @@ -131,36 +251,44 @@ SoftwareConnection::Message SoftwareConnection::receiveMessageFromSoftware() { } const size_t subjectSize = std::stoi(subjectSizeIn); - std::string rawHeader; - for (int i = 0; i < 22; i++) { - rawHeader.push_back(headerBuffer[i]); - } - LDEBUG(fmt::format("Message received with header: {}", rawHeader)); + // TODO: Remove this + // std::string rawHeader; + // for (int i = 0; i < 22; i++) { + // rawHeader.push_back(headerBuffer[i]); + // } + // LDEBUG(fmt::format("Message received with header: {}", rawHeader)); auto typeEnum = simp::getMessageType(type); // Receive the message data - if (typeEnum != simp::MessageType::Disconnection) { + if (typeEnum != simp::MessageType::Disconnection && typeEnum != simp::MessageType::Unknown) { subjectBuffer.resize(subjectSize); if (!_socket->get(subjectBuffer.data(), subjectSize)) { throw SoftwareConnectionLostError("Failed to read message from socket. Disconnecting."); } } - // Delegate decoding depending on message type - if (typeEnum == simp::MessageType::Color - || typeEnum == simp::MessageType::Opacity - || typeEnum == simp::MessageType::Size - || typeEnum == simp::MessageType::Visibility - ) { - _isListening = false; - } + return Message{ typeEnum, subjectBuffer, type }; +} - if (typeEnum == simp::MessageType::Unknown) { - LERROR(fmt::format("Unsupported message type: {}. Disconnecting...", type)); - } +bool SoftwareConnection::shouldStopThread() { + return _shouldStopThread; +} - return Message(typeEnum, subjectBuffer); +size_t SoftwareConnection::id() { + return _id; +} + +void SoftwareConnection::setThread(std::thread& t) { + _thread = std::move(t); +} + +void SoftwareConnection::NetworkEngineFriends::stopThread(std::shared_ptr connectionPtr) { + connectionPtr->_shouldStopThread = true; + connectionPtr->disconnect(); + if (connectionPtr->_thread.joinable()) { + connectionPtr->_thread.join(); + } } } // namespace openspace diff --git a/modules/softwareintegration/network/softwareconnection.h b/modules/softwareintegration/network/softwareconnection.h index b913905256..51985db73e 100644 --- a/modules/softwareintegration/network/softwareconnection.h +++ b/modules/softwareintegration/network/softwareconnection.h @@ -28,38 +28,85 @@ #include #include #include +#include + +#include +#include namespace openspace { +class Renderable; + class SoftwareConnection { public: + using OnChangeHandle = properties::Property::OnChangeHandle; + using PropertySubscriptions = std::unordered_map; + using SubscribedProperties = std::unordered_map; + struct Message { - Message() = default; - Message(simp::MessageType type, std::vector content); - simp::MessageType type; - std::vector content; + std::vector content{}; + std::string rawMessageType{ "" }; }; - class SoftwareConnectionLostError : public ghoul::RuntimeError { - public: - explicit SoftwareConnectionLostError(const std::string& msg); - }; + class SoftwareConnectionLostError; - SoftwareConnection(std::unique_ptr socket); + explicit SoftwareConnection(std::unique_ptr socket); + SoftwareConnection(SoftwareConnection&& p); + ~SoftwareConnection(); + void disconnect(); bool isConnected() const; bool isConnectedOrConnecting() const; - bool sendMessage(std::string message); - void disconnect(); + bool sendMessage(const std::string& message); - ghoul::io::TcpSocket* socket(); + void addPropertySubscription( + const std::string propertyName, + const std::string& identifier, + std::function newHandler + ); SoftwareConnection::Message receiveMessageFromSoftware(); + void addSceneGraphNode(const std::string& identifier); + void removeSceneGraphNode(const std::string& identifier); + + size_t id(); + size_t nConnections(); + void setThread(std::thread& t); + bool shouldStopThread(); + + class NetworkEngineFriends { + private: + static void stopThread(std::shared_ptr connectionPtr); + friend class NetworkEngine; + }; + private: - bool _isListening = true; + void removePropertySubscriptions(const std::string& identifier); + void removeExistingPropertySubscription( + const std::string& identifier, + properties::Property *property, + OnChangeHandle onChangeHandle + ); + + SubscribedProperties _subscribedProperties; + + std::unordered_set _sceneGraphNodes; + + bool _isConnected = true; std::unique_ptr _socket; + + size_t _id; + std::thread _thread; + std::atomic_bool _shouldStopThread; + + static std::atomic_size_t _nextConnectionId; +}; + +class SoftwareConnection::SoftwareConnectionLostError : public ghoul::RuntimeError { +public: + explicit SoftwareConnectionLostError(const std::string& msg); }; } // namespace openspace diff --git a/modules/softwareintegration/pointdatamessagehandler.cpp b/modules/softwareintegration/pointdatamessagehandler.cpp index f30b2cd1fd..d0071c968f 100644 --- a/modules/softwareintegration/pointdatamessagehandler.cpp +++ b/modules/softwareintegration/pointdatamessagehandler.cpp @@ -23,9 +23,10 @@ ****************************************************************************************/ #include - #include #include + +#include #include #include #include @@ -44,272 +45,462 @@ namespace { namespace openspace { -void PointDataMessageHandler::handlePointDataMessage(const std::vector& message, - SoftwareConnection& connection, - std::list& sceneGraphNodes) -{ +void PointDataMessageHandler::handlePointDataMessage(const std::vector& message, std::shared_ptr connection) { size_t messageOffset = 0; - std::string identifier; - glm::vec4 color; - float opacity; - float size; - std::string guiName; - int nPoints; - int dimensionality; + + checkRenderable(message, messageOffset, connection, identifier); + + size_t nPoints; + size_t dimensionality; std::vector points; try { // The following order of creating variables is the exact order they are received // in the message. If the order is not the same, the global variable // 'message offset' will be wrong - identifier = readString(message, messageOffset); - sceneGraphNodes.push_back(identifier); - color = readColor(message, messageOffset); - opacity = readFloatValue(message, messageOffset); - size = readFloatValue(message, messageOffset); - guiName = readString(message, messageOffset); - nPoints = readIntValue(message, messageOffset); - dimensionality = readIntValue(message, messageOffset); - points.reserve(nPoints * dimensionality); - points = readPointData(message, messageOffset, nPoints, dimensionality); + nPoints = static_cast(simp::readIntValue(message, messageOffset)); + dimensionality = static_cast(simp::readIntValue(message, messageOffset)); + simp::readPointData(message, messageOffset, nPoints, dimensionality, points); } catch (const simp::SimpError& err) { LERROR(fmt::format("Error when reading point data message: {}", err.message)); return; } - using namespace std::string_literals; - - // Create a renderable - ghoul::Dictionary renderable; - renderable.setValue("Type", "RenderablePointsCloud"s); - renderable.setValue("Color", static_cast(glm::vec3{color.r, color.g, color.b})); - renderable.setValue("Opacity", static_cast(opacity)); - renderable.setValue("Size", static_cast(size)); - renderable.setValue("Identifier", identifier); - // Use the renderable identifier as the data key const std::string key = identifier + "-DataPoints"; auto module = global::moduleEngine->module(); module->storeData(key, std::move(points)); - - renderable.setValue("DataStorageKey", key); - - ghoul::Dictionary gui; - gui.setValue("Name", guiName); - gui.setValue("Path", "/Software Integration"s); - - ghoul::Dictionary node; - node.setValue("Identifier", identifier); - node.setValue("Renderable", renderable); - node.setValue("GUI", gui); - - openspace::global::scriptEngine->queueScript( - "openspace.addSceneGraphNode(" + ghoul::formatLua(node) + ")", - scripting::ScriptEngine::RemoteScripting::Yes - ); - - openspace::global::scriptEngine->queueScript( - "openspace.setPropertyValueSingle('NavigationHandler.OrbitalNavigator.RetargetAnchor', nil)" - "openspace.setPropertyValueSingle('NavigationHandler.OrbitalNavigator.Anchor', '" + identifier + "')" - "openspace.setPropertyValueSingle('NavigationHandler.OrbitalNavigator.Aim', '')" - "openspace.setPropertyValueSingle('Modules.CefWebGui.Reload', nil)", // Reload WebGUI so that SoftwareIntegration GUI appears - scripting::ScriptEngine::RemoteScripting::Yes - ); - - // We have to wait until the renderable exists before we can subscribe to - // changes in its properties - auto callback = [this, identifier, &connection]() { - subscribeToRenderableUpdates(identifier, connection); - }; - _onceNodeExistsCallbacks.emplace(identifier, callback); } -void PointDataMessageHandler::handleColorMessage(const std::vector& message) { +void PointDataMessageHandler::handleFixedColorMessage(const std::vector& message, std::shared_ptr connection) { size_t messageOffset = 0; - const std::string identifier = readString(message, messageOffset); - const glm::vec3 color = readColor(message, messageOffset); + std::string identifier; - // Get renderable - auto r = getRenderable(identifier); - if (!r) return; + checkRenderable(message, messageOffset, connection, identifier); - // Get color of renderable - properties::Property* colorProperty = r->property("Color"); - std::any propertyAny = colorProperty->get(); - glm::vec3 propertyColor = std::any_cast(propertyAny); - - if (propertyColor != glm::vec3(0, 0, 0)) { - LWARNING(fmt::format("propertyColor '{}'", propertyColor)); + glm::vec4 color; + try { + color = simp::readColor(message, messageOffset); } - - // Update color of renderable - if (propertyColor != color) { - std::string colorScript = fmt::format( - "openspace.setPropertyValueSingle('Scene.{}.Renderable.Color', {});", - identifier, ghoul::to_string(color) - ); - - std::string disableColorMapScript = fmt::format( - "openspace.setPropertyValueSingle('Scene.{}.Renderable.ColorMapEnabled', {});", - identifier, false - ); - - openspace::global::scriptEngine->queueScript( - colorScript, - scripting::ScriptEngine::RemoteScripting::Yes - ); - openspace::global::scriptEngine->queueScript( - disableColorMapScript, - scripting::ScriptEngine::RemoteScripting::Yes - ); - } -} - -void PointDataMessageHandler::handleColorMapMessage(const std::vector& message) { - size_t messageOffset = 0; - const std::string identifier = readString(message, messageOffset); - const std::vector colorMap = readColorMap(message, messageOffset); - - // Use the renderable identifier as the data key - const std::string key = identifier + "-ColorMap"; - auto module = global::moduleEngine->module(); - module->storeData(key, std::move(colorMap)); - - // Get renderable - auto r = getRenderable(identifier); - if (!r) return; - - std::string script = fmt::format( - "openspace.setPropertyValueSingle('Scene.{}.Renderable.LoadNewColorMap', {});", - identifier, true - ); - openspace::global::scriptEngine->queueScript( - script, - scripting::ScriptEngine::RemoteScripting::Yes - ); -} - -void PointDataMessageHandler::handleOpacityMessage(const std::vector& message) { - size_t messageOffset = 0; - const std::string identifier = readString(message, messageOffset); - const float opacity = readFloatValue(message, messageOffset); - - // Get renderable - auto r = getRenderable(identifier); - if (!r) return; - - // Get opacity of renderable - properties::Property* opacityProperty = r->property("Opacity"); - auto propertyAny = opacityProperty->get(); - float propertyOpacity = std::any_cast(propertyAny); - - // Update opacity of renderable - if (propertyOpacity != opacity) { - std::string script = fmt::format( - "openspace.setPropertyValueSingle('Scene.{}.Renderable.Opacity', {});", - identifier, ghoul::to_string(opacity) - ); - openspace::global::scriptEngine->queueScript( - script, - scripting::ScriptEngine::RemoteScripting::Yes - ); - } -} - -void PointDataMessageHandler::handlePointSizeMessage(const std::vector& message) { - size_t messageOffset = 0; - const std::string identifier = readString(message, messageOffset); - float size = readFloatValue(message, messageOffset); - - // Get renderable - auto r = getRenderable(identifier); - if (!r) return; - - // Get size of renderable - properties::Property* sizeProperty = r->property("Size"); - auto propertyAny = sizeProperty->get(); - float propertySize = std::any_cast(propertyAny); - - // Update size of renderable - if (propertySize != size) { - // TODO: Add to script when the "send back to glue" stuff is done - // "openspace.setPropertyValueSingle('Scene.{}.Renderable.Size', {}, 1);", - std::string script = fmt::format( - "openspace.setPropertyValueSingle('Scene.{}.Renderable.Size', {});", - identifier, ghoul::to_string(size) - ); - openspace::global::scriptEngine->queueScript( - script, - scripting::ScriptEngine::RemoteScripting::Yes - ); - } -} - -void PointDataMessageHandler::handleVisiblityMessage(const std::vector& message) { - size_t messageOffset = 0; - const std::string identifier = readString(message, messageOffset); - std::string visibilityMessage; - visibilityMessage.push_back(message[messageOffset]); - - // Get renderable - auto r = getRenderable(identifier); - if (!r) return; - - // Toggle visibility of renderable - const std::string visability = visibilityMessage == "T" ? "true" : "false"; - std::string script = fmt::format( - "openspace.setPropertyValueSingle('Scene.{}.Renderable.Enabled', {});", - identifier, visability - ); - openspace::global::scriptEngine->queueScript( - script, - scripting::ScriptEngine::RemoteScripting::Yes - ); -} - -void PointDataMessageHandler::preSyncUpdate() { - if (_onceNodeExistsCallbacks.empty()) { + catch (const simp::SimpError& err) { + LERROR(fmt::format("Error when reading fixed color message: {}", err.message)); return; } - // Check if the scene graph node has been created. If so, call the corresponding - // callback function to set up any subscriptions - auto it = _onceNodeExistsCallbacks.begin(); - while (it != _onceNodeExistsCallbacks.end()) { - const std::string& identifier = it->first; - const std::function& callback = it->second; - const SceneGraphNode* sgn = - global::renderEngine->scene()->sceneGraphNode(identifier); + auto callback = [this, identifier, color] { + // Get renderable + auto r = getRenderable(identifier); - if (sgn) { - callback(); - it = _onceNodeExistsCallbacks.erase(it); - continue; + // Get color of renderable + properties::Property* colorProperty = r->property("Color"); + glm::vec4 propertyColor = std::any_cast(colorProperty->get()); + + // Update color of renderable + if (propertyColor != color) { + openspace::global::scriptEngine->queueScript( + fmt::format( + "openspace.setPropertyValueSingle('Scene.{}.Renderable.Color', {});", + identifier, ghoul::to_string(color) + ), + scripting::ScriptEngine::RemoteScripting::Yes + ); + } + + openspace::global::scriptEngine->queueScript( + fmt::format( + "openspace.setPropertyValueSingle('Scene.{}.Renderable.ColormapEnabled', {});", + identifier, "false" + ), + scripting::ScriptEngine::RemoteScripting::Yes + ); + }; + addCallback(identifier, callback); +} + +void PointDataMessageHandler::handleColormapMessage(const std::vector& message, std::shared_ptr connection) { + size_t messageOffset = 0; + std::string identifier; + + checkRenderable(message, messageOffset, connection, identifier); + + float min; + float max; + size_t nColors; + std::vector colorMap; + try { + min = simp::readFloatValue(message, messageOffset); + max = simp::readFloatValue(message, messageOffset); + nColors = static_cast(simp::readIntValue(message, messageOffset)); + simp::readColormap(message, messageOffset, nColors, colorMap); + } + catch (const simp::SimpError& err) { + LERROR(fmt::format("Error when reading color map message: {}", err.message)); + return; + } + + // Use the renderable identifier as the data key + const std::string key = identifier + "-Colormap"; + auto module = global::moduleEngine->module(); + module->storeData(key, std::move(colorMap)); + + auto callback = [this, identifier, min, max] { + // Get renderable + auto r = getRenderable(identifier); + + openspace::global::scriptEngine->queueScript( + fmt::format( + "openspace.setPropertyValueSingle('Scene.{}.Renderable.ColormapEnabled', {});", + identifier, "true" + ), + scripting::ScriptEngine::RemoteScripting::Yes + ); + + properties::Property* colormapMinProperty = r->property("ColormapMin"); + float colormapMin = std::any_cast(colormapMinProperty->get()); + if (min != colormapMin) { + openspace::global::scriptEngine->queueScript( + fmt::format( + "openspace.setPropertyValueSingle('Scene.{}.Renderable.ColormapMin', {});", + identifier, ghoul::to_string(min) + ), + scripting::ScriptEngine::RemoteScripting::Yes + ); + } + + properties::Property* colormapMaxProperty = r->property("ColormapMax"); + float colormapMax = std::any_cast(colormapMaxProperty->get()); + if (max != colormapMax) { + openspace::global::scriptEngine->queueScript( + fmt::format( + "openspace.setPropertyValueSingle('Scene.{}.Renderable.ColormapMax', {});", + identifier, ghoul::to_string(max) + ), + scripting::ScriptEngine::RemoteScripting::Yes + ); + } + }; + addCallback(identifier, callback); +} + +void PointDataMessageHandler::handleAttributeDataMessage(const std::vector& message, std::shared_ptr connection) { + size_t messageOffset = 0; + std::string identifier; + + checkRenderable(message, messageOffset, connection, identifier); + + std::string usedFor; + size_t nValues; + std::vector attributeData; + try { + usedFor = simp::readString(message, messageOffset); + nValues = static_cast(simp::readIntValue(message, messageOffset)); + attributeData.reserve(nValues); + for (size_t i = 0; i < nValues; ++i) + attributeData.push_back(simp::readFloatValue(message, messageOffset)); + } + catch (const simp::SimpError& err) { + LERROR(fmt::format("Error when reading message with scalars for color map: {}", err.message)); + return; + } + + auto module = global::moduleEngine->module(); + // Use the renderable identifier as the data key + std::string key = identifier; + + if (usedFor == "ColormapAttributeData") { + key += "-CmapAttributeData"; + } + else { + LERROR(fmt::format( + "The received attribute data had the \"usedFor\" value {}, which is unrecognized.", + usedFor + )); + return; + } + + module->storeData(key, std::move(attributeData)); + + auto callback = [this, identifier, usedFor] { + if (usedFor == "ColormapAttributeData") { + openspace::global::scriptEngine->queueScript( + fmt::format( + "openspace.setPropertyValueSingle('Scene.{}.Renderable.ColormapEnabled', {});", + identifier, "true" + ), + scripting::ScriptEngine::RemoteScripting::Yes + ); + } + }; + addCallback(identifier, callback); +} + +void PointDataMessageHandler::handleOpacityMessage(const std::vector& message, std::shared_ptr connection) { + size_t messageOffset = 0; + std::string identifier; + + checkRenderable(message, messageOffset, connection, identifier); + + float opacity; + try { + opacity = simp::readFloatValue(message, messageOffset); + } + catch (const simp::SimpError& err) { + LERROR(fmt::format("Error when reading opacity message: {}", err.message)); + return; + } + + auto callback = [this, identifier, opacity] { + // Get renderable + auto r = getRenderable(identifier); + + // Get opacity from renderable + properties::Property* opacityProperty = r->property("Opacity"); + auto propertyAny = opacityProperty->get(); + float propertyOpacity = std::any_cast(propertyAny); + + // Update opacity of renderable + if (propertyOpacity != opacity) { + std::string script = fmt::format( + "openspace.setPropertyValueSingle('Scene.{}.Renderable.Opacity', {});", + identifier, ghoul::to_string(opacity) + ); + openspace::global::scriptEngine->queueScript( + script, + scripting::ScriptEngine::RemoteScripting::Yes + ); + } + }; + addCallback(identifier, callback); +} + +void PointDataMessageHandler::handleFixedPointSizeMessage(const std::vector& message, std::shared_ptr connection) { + size_t messageOffset = 0; + std::string identifier; + + checkRenderable(message, messageOffset, connection, identifier); + + float size; + try { + size = simp::readFloatValue(message, messageOffset); + } + catch (const simp::SimpError& err) { + LERROR(fmt::format("Error when reading fixed point size message: {}", err.message)); + return; + } + + auto callback = [this, identifier, size] { + // Get renderable + auto r = getRenderable(identifier); + + // Get size from renderable + properties::Property* sizeProperty = r->property("Size"); + auto propertyAny = sizeProperty->get(); + float propertySize = std::any_cast(propertyAny); + + // Update size of renderable + if (propertySize != size) { + // TODO: Add interpolation to script, but do not send back + // updates to external software until the interpolation is done + // Use this: "openspace.setPropertyValueSingle('Scene.{}.Renderable.Size', {}, 1);", + std::string script = fmt::format( + "openspace.setPropertyValueSingle('Scene.{}.Renderable.Size', {});", + identifier, ghoul::to_string(size) + ); + openspace::global::scriptEngine->queueScript( + script, + scripting::ScriptEngine::RemoteScripting::Yes + ); + } + }; + addCallback(identifier, callback); +} + +void PointDataMessageHandler::handleVisiblityMessage(const std::vector& message, std::shared_ptr connection) { + size_t messageOffset = 0; + std::string identifier; + + checkRenderable(message, messageOffset, connection, identifier); + + std::string visibilityMessage; + try { + visibilityMessage = simp::readString(message, messageOffset); + } + catch (const simp::SimpError& err) { + LERROR(fmt::format("Error when reading visibility message: {}", err.message)); + return; + } + + auto callback = [this, identifier, visibilityMessage] { + // Toggle visibility from renderable + const std::string visability = visibilityMessage == "T" ? "true" : "false"; + std::string script = fmt::format( + "openspace.setPropertyValueSingle('Scene.{}.Renderable.Enabled', {});", + identifier, visability + ); + openspace::global::scriptEngine->queueScript( + script, + scripting::ScriptEngine::RemoteScripting::Yes + ); + }; + addCallback(identifier, callback); +} + +void PointDataMessageHandler::handleRemoveSGNMessage(const std::vector& message,std::shared_ptr connection) { + size_t messageOffset = 0; + std::string identifier; + + try { + identifier = simp::readString(message, messageOffset); + } + catch (const simp::SimpError& err) { + LERROR(fmt::format("Error when reading message: {}", err.message)); + return; + } + + const std::string currentAnchor = + global::navigationHandler->orbitalNavigator().anchorNode()->identifier(); + + if (currentAnchor == identifier) { + // If the deleted node is the current anchor, first change focus to the Sun + openspace::global::scriptEngine->queueScript( + "openspace.setPropertyValueSingle('NavigationHandler.OrbitalNavigator.Anchor', 'Sun')" + "openspace.setPropertyValueSingle('NavigationHandler.OrbitalNavigator.Aim', '')", + scripting::ScriptEngine::RemoteScripting::Yes + ); + } + openspace::global::scriptEngine->queueScript( + "openspace.removeSceneGraphNode('" + identifier + "');", + scripting::ScriptEngine::RemoteScripting::Yes + ); + + connection->removeSceneGraphNode(identifier); + + LDEBUG(fmt::format("Scene graph node '{}' removed.", identifier)); +} + +void PointDataMessageHandler::postSync() { + std::lock_guard guard(_onceNodeExistsCallbacksMutex); + // Check if the scene graph node has been created. + // If so, call the corresponding callback functions to set up any subscriptions + auto callbackMapIt = _onceNodeExistsCallbacks.begin(); + while (callbackMapIt != _onceNodeExistsCallbacks.end()) { + auto& [identifier, callbackList] = *callbackMapIt; + + try { + const SceneGraphNode* sgn = global::renderEngine->scene()->sceneGraphNode(identifier); + if (!sgn) throw std::exception{}; + + auto r = getRenderable(identifier); + if (!r) throw std::exception{}; + + auto callbacksIt = callbackList.begin(); + while (callbacksIt != callbackList.end()) { + auto& callback = *callbacksIt; + try { + callback(); + callbacksIt = callbackList.erase(callbacksIt); + } + catch (std::exception&) { + ++callbacksIt; + } + } + + callbackMapIt = _onceNodeExistsCallbacks.erase(callbackMapIt); + _onceNodeExistsCallbacksRetries = 0; + } + catch(std::exception &err) { + ++_onceNodeExistsCallbacksRetries; + ghoul_assert(_onceNodeExistsCallbacksRetries < 10, "Too many callback retries"); + LDEBUG(fmt::format("Error when trying to run callback: {}", err.what())); + break; } - it++; } } -const Renderable* PointDataMessageHandler::getRenderable(const std::string& identifier) const { +const Renderable* PointDataMessageHandler::getRenderable(const std::string& identifier) { + return renderable(identifier); +} + +void PointDataMessageHandler::checkRenderable( + const std::vector& message, size_t& messageOffset, + std::shared_ptr connection, std::string& identifier +) { + std::string guiName; + + try { + // The following order of creating variables is the exact order they are received + // in the message. If the order is not the same, the global variable + // 'message offset' will be wrong + identifier = simp::readString(message, messageOffset); + guiName = simp::readString(message, messageOffset); + } + catch (const simp::SimpError& err) { + LERROR(fmt::format("Error when reading identifier and guiName from message: {}", err.message)); + return; + } + + connection->addSceneGraphNode(identifier); + const Renderable* r = renderable(identifier); - if (!r) { - LERROR(fmt::format("No renderable with identifier '{}' was found", identifier)); - return nullptr; + bool hasCallbacks = false; + { + std::lock_guard guard(_onceNodeExistsCallbacksMutex); + hasCallbacks = _onceNodeExistsCallbacks.count(identifier) > 0; + } + if (!r && !hasCallbacks) { + LDEBUG(fmt::format("No renderable with identifier '{}' was found. Creating it.", identifier)); + + // Create a renderable, since it didn't exist + using namespace std::string_literals; + ghoul::Dictionary renderablePointsCloud; + renderablePointsCloud.setValue("Type", "RenderablePointsCloud"s); + renderablePointsCloud.setValue("Identifier", identifier); + + ghoul::Dictionary gui; + gui.setValue("Name", guiName); + gui.setValue("Path", "/Software Integration"s); + + ghoul::Dictionary node; + node.setValue("Identifier", identifier); + node.setValue("Renderable", renderablePointsCloud); + node.setValue("GUI", gui); + + openspace::global::scriptEngine->queueScript( + "openspace.addSceneGraphNode(" + ghoul::formatLua(node) + ")" + "openspace.setPropertyValueSingle('Modules.CefWebGui.Reload', nil)", // Reload WebGUI so that SoftwareIntegration GUI appears + scripting::ScriptEngine::RemoteScripting::Yes + ); + auto subscriptionCallback = [this, identifier, connection] { + subscribeToRenderableUpdates(identifier, connection); + }; + addCallback(identifier, subscriptionCallback); + } + else { + subscribeToRenderableUpdates(identifier, connection); + } + + auto reanchorCallback = [identifier] { + openspace::global::scriptEngine->queueScript( + "openspace.setPropertyValueSingle('NavigationHandler.OrbitalNavigator.RetargetAnchor', nil)" + "openspace.setPropertyValueSingle('NavigationHandler.OrbitalNavigator.Anchor', '" + identifier + "')" + "openspace.setPropertyValueSingle('NavigationHandler.OrbitalNavigator.Aim', '')", + scripting::ScriptEngine::RemoteScripting::Yes + ); }; - return r; + addCallback(identifier, reanchorCallback); } void PointDataMessageHandler::subscribeToRenderableUpdates( const std::string& identifier, - SoftwareConnection& connection + std::shared_ptr connection ) { // Get renderable auto aRenderable = getRenderable(identifier); if (!aRenderable) return; - if (!connection.isConnected()) { + if (!connection->isConnected()) { LERROR(fmt::format( "Could not subscribe to updates for renderable '{}' due to lost connection", identifier @@ -320,206 +511,67 @@ void PointDataMessageHandler::subscribeToRenderableUpdates( properties::Property* colorProperty = aRenderable->property("Color"); properties::Property* opacityProperty = aRenderable->property("Opacity"); properties::Property* sizeProperty = aRenderable->property("Size"); - properties::Property* visibilityProperty = aRenderable->property("ToggleVisibility"); + properties::Property* visibilityProperty = aRenderable->property("Enabled"); // Update color of renderable - auto updateColor = [colorProperty, identifier, &connection]() { - const std::string value = colorProperty->getStringValue(); - const std::string message = simp::formatUpdateMessage(simp::MessageType::Color, identifier, value); - connection.sendMessage(message); + auto updateColor = [colorProperty, identifier, connection]() { + glm::vec4 color = std::any_cast(colorProperty->get()); + + const std::string message = simp::formatColorMessage(identifier, color); + connection->sendMessage(message); }; if (colorProperty) { - colorProperty->onChange(updateColor); + connection->addPropertySubscription("Color", identifier, updateColor); } // Update opacity of renderable - auto updateOpacity = [opacityProperty, identifier, &connection]() { - const std::string value = opacityProperty->getStringValue(); - const std::string message = simp::formatUpdateMessage(simp::MessageType::Opacity, identifier, value); - connection.sendMessage(message); + auto updateOpacity = [opacityProperty, identifier, connection]() { + float value = std::any_cast(opacityProperty->get()); + std::string hex_value = simp::floatToHex(value); + + const std::string message = simp::formatUpdateMessage(simp::MessageType::Opacity, identifier, hex_value); + connection->sendMessage(message); }; if (opacityProperty) { - opacityProperty->onChange(updateOpacity); + connection->addPropertySubscription("Opacity", identifier, updateOpacity); } // Update size of renderable - auto updateSize = [sizeProperty, identifier, &connection]() { - const std::string value = sizeProperty->getStringValue(); - const std::string message = simp::formatUpdateMessage(simp::MessageType::Size, identifier, value); - connection.sendMessage(message); + auto updateSize = [sizeProperty, identifier, connection]() { + float value = std::any_cast(sizeProperty->get()); + std::string hex_value = simp::floatToHex(value); + + const std::string message = simp::formatUpdateMessage(simp::MessageType::Size, identifier, hex_value); + connection->sendMessage(message); }; if (sizeProperty) { - sizeProperty->onChange(updateSize); + connection->addPropertySubscription("Size", identifier, updateSize); } // Toggle visibility of renderable - auto toggleVisibility = [visibilityProperty, identifier, &connection]() { - bool isVisible = visibilityProperty->getStringValue() == "true"; + auto toggleVisibility = [visibilityProperty, identifier, connection]() { + bool isVisible = std::any_cast(visibilityProperty->get()); std::string_view visibilityFlag = isVisible ? "T" : "F"; const std::string message = simp::formatUpdateMessage(simp::MessageType::Visibility, identifier, visibilityFlag); - connection.sendMessage(message); + connection->sendMessage(message); }; if (visibilityProperty) { - visibilityProperty->onChange(toggleVisibility); + connection->addPropertySubscription("Enabled", identifier, toggleVisibility); } + } -int PointDataMessageHandler::readIntValue(const std::vector& message, size_t& offset) { - std::string string_value; - int value; - bool isHex = false; - - while (!simp::isEndOfCurrentValue(message, offset)) { - char c = message[offset]; - if (c == 'x' || c == 'X') isHex = true; - string_value.push_back(c); - offset++; +void PointDataMessageHandler::addCallback(const std::string& identifier, const Callback& newCallback) { + std::lock_guard guard(_onceNodeExistsCallbacksMutex); + auto it = _onceNodeExistsCallbacks.find(identifier); + if (it == _onceNodeExistsCallbacks.end()) { + CallbackList callbacks{ std::move(newCallback) }; + _onceNodeExistsCallbacks.emplace(std::move(identifier), std::move(callbacks) ); } - - try { - value = std::stoi(string_value, nullptr, isHex ? 16 : 10); + else { + it->second.push_back(std::move(newCallback)); } - catch(std::exception &err) { - throw simp::SimpError( - simp::ErrorCode::Generic, - fmt::format("Error when trying to parse the integer {}: {}", string_value, err.what()) - ); - } - - ++offset; - return value; -} - -float PointDataMessageHandler::readFloatValue(const std::vector& message, size_t& offset) { - std::string string_value; - float value; - - while (!simp::isEndOfCurrentValue(message, offset)) { - string_value.push_back(message[offset]); - offset++; - } - - try { - // long l; - - // l = std::strtol(string_value.data(), (char**)NULL, 16); - // value = (float)l; - value = std::stof(string_value); - - // if ((*s == '0') && ((*s == 'X') || (*s == 'x'))) { - // unsigned long ul = strtoul(d, NULL, 16); - // return (float) ul; - // } - // double d = atof(s, NULL); - // return (float) d; - } - catch(std::exception &err) { - throw simp::SimpError( - simp::ErrorCode::Generic, - fmt::format("Error when trying to parse the float {}: {}", string_value, err.what()) - ); - } - - ++offset; - return value; -} - -std::vector PointDataMessageHandler::readColorMap(const std::vector& message, - size_t& offset) { - std::vector colorMap; - while (message[offset] != simp::SEP) { - glm::vec4 color = readSingleColor(message, offset); - - // ColorMap should be stored in a sequential vector - // of floats for syncing between nodes and when - // loaded to as a texture in the shader. - colorMap.push_back(color[0]); - colorMap.push_back(color[1]); - colorMap.push_back(color[2]); - colorMap.push_back(color[3]); - } - - offset++; - return colorMap; -} - -glm::vec4 PointDataMessageHandler::readSingleColor(const std::vector& message, size_t& offset) { - if (message[offset] != '[') { - throw simp::SimpError( - simp::ErrorCode::Generic, - fmt::format("Expected to read '[', got {} in 'readColor'", message[offset]) - ); - } - ++offset; - - float r = readFloatValue(message, offset); - float g = readFloatValue(message, offset); - float b = readFloatValue(message, offset); - float a = readFloatValue(message, offset); - - if (message[offset] != ']') { - throw simp::SimpError( - simp::ErrorCode::Generic, - fmt::format("Expected to read ']', got {} in 'readColor'", message[offset]) - ); - } - ++offset; - - return { r, g, b, a }; -} - -glm::vec4 PointDataMessageHandler::readColor(const std::vector& message, size_t& offset) { - glm::vec4 color = readSingleColor(message, offset); - - ++offset; - return color; -} - -std::string PointDataMessageHandler::readString(const std::vector& message, size_t& offset) { - std::string value; - while (!simp::isEndOfCurrentValue(message, offset)) { - value.push_back(message[offset]); - ++offset; - } - - ++offset; - return value; -} - -std::vector PointDataMessageHandler::readPointData( - const std::vector& message, - size_t& offset, - int nPoints, - int dimensionality -) { - std::vector result; - result.reserve(nPoints * dimensionality); - - while (!simp::isEndOfCurrentValue(message, offset)) { - if (message[offset] != '[') { - throw simp::SimpError( - simp::ErrorCode::Generic, - fmt::format("Expected to read '[', got {} in 'readPointData'", message[offset]) - ); - } - ++offset; - - for (int i = 0; i < dimensionality; ++i) { - result.push_back(readFloatValue(message, offset)); - } - - if (message[offset] != ']') { - throw simp::SimpError( - simp::ErrorCode::Generic, - fmt::format("Expected to read ']', got {} in 'readPointData'", message[offset]) - ); - } - ++offset; - } - - ++offset; - return result; } } // namespace openspace diff --git a/modules/softwareintegration/pointdatamessagehandler.h b/modules/softwareintegration/pointdatamessagehandler.h index 81ea3b4e79..d28a8e29e9 100644 --- a/modules/softwareintegration/pointdatamessagehandler.h +++ b/modules/softwareintegration/pointdatamessagehandler.h @@ -25,6 +25,8 @@ #ifndef __OPENSPACE_MODULE_SOFTWAREINTEGRATION___POINTDATAMESSAGEHANDLER___H__ #define __OPENSPACE_MODULE_SOFTWAREINTEGRATION___POINTDATAMESSAGEHANDLER___H__ +#include + #include #include @@ -34,34 +36,36 @@ namespace openspace { class Renderable; class PointDataMessageHandler { -public: - void handlePointDataMessage(const std::vector& message, - SoftwareConnection& connection, std::list& sceneGraphNodes); - void handleColorMessage(const std::vector& message); - void handleColorMapMessage(const std::vector& message); - void handleOpacityMessage(const std::vector& message); - void handlePointSizeMessage(const std::vector& message); - void handleVisiblityMessage(const std::vector& message); + using Callback = std::function; + using CallbackList = std::vector>; + using CallbackMap = std::unordered_map; - void preSyncUpdate(); +public: + void handlePointDataMessage(const std::vector& message, std::shared_ptr connection); + void handleFixedColorMessage(const std::vector& message, std::shared_ptr connection); + void handleColormapMessage(const std::vector& message, std::shared_ptr connection); + void handleAttributeDataMessage(const std::vector& message, std::shared_ptr connection); + void handleOpacityMessage(const std::vector& message, std::shared_ptr connection); + void handleFixedPointSizeMessage(const std::vector& message, std::shared_ptr connection); + void handleVisiblityMessage(const std::vector& message, std::shared_ptr connection); + void handleRemoveSGNMessage(const std::vector& message, std::shared_ptr connection); + + void postSync(); private: - const Renderable* getRenderable(const std::string& identifier) const; - - void subscribeToRenderableUpdates(const std::string& identifier, - SoftwareConnection& connection); - - float readFloatValue(const std::vector& message, size_t& offset); - int readIntValue(const std::vector& message, size_t& offset); - glm::vec4 readSingleColor(const std::vector& message, size_t& offset); - glm::vec4 readColor(const std::vector& message, size_t& offset); - std::string readString(const std::vector& message, size_t& offset); - std::vector readPointData( - const std::vector& message, size_t& offset, int nPoints, int dimensionality + const Renderable* getRenderable(const std::string& identifier); + void checkRenderable( + const std::vector& message, size_t& messageOffset, + std::shared_ptr connection, std::string& identifier ); - std::vector readColorMap(const std::vector& message, size_t& offset); - std::unordered_map> _onceNodeExistsCallbacks; + void subscribeToRenderableUpdates(const std::string& identifier, std::shared_ptr connection); + + void addCallback(const std::string& identifier, const Callback& newCallback); + + CallbackMap _onceNodeExistsCallbacks; + std::mutex _onceNodeExistsCallbacksMutex; + size_t _onceNodeExistsCallbacksRetries{0}; }; } // namespace openspace diff --git a/modules/softwareintegration/rendering/renderablepointscloud.cpp b/modules/softwareintegration/rendering/renderablepointscloud.cpp index cf82cf120d..d9f35b5b6b 100644 --- a/modules/softwareintegration/rendering/renderablepointscloud.cpp +++ b/modules/softwareintegration/rendering/renderablepointscloud.cpp @@ -36,17 +36,17 @@ #include #include #include +#include #include -#include namespace { constexpr const char* _loggerCat = "PointsCloud"; - constexpr const std::array UniformNames = { - "color", "opacity", "size", "modelMatrix", - "cameraUp", "cameraViewProjectionMatrix", "eyePosition", "sizeOption" + constexpr const std::array UniformNames = { + "color", "opacity", "size", "modelMatrix", "cameraUp", "cameraViewProjectionMatrix", + "eyePosition", "sizeOption", "colormapTexture", "colormapMin", "colormapMax", + "colormapEnabled" }; - //"colorMapTexture", constexpr openspace::properties::Property::PropertyInfo ColorInfo = { "Color", @@ -60,28 +60,16 @@ namespace { "The size of the points." }; - constexpr openspace::properties::Property::PropertyInfo ToggleVisibilityInfo = { - "ToggleVisibility", - "Toggle Visibility", - "Enables/Disables the drawing of points." - }; - constexpr openspace::properties::Property::PropertyInfo DataInfo = { "Data", "Data", "Data to use for the positions of the points, given in Parsec." }; - constexpr openspace::properties::Property::PropertyInfo DataStorageKeyInfo = { - "DataStorageKey", - "Data Storage Key", - "Key used to access a dataset in the module's centralized storage, which is synced to all nodes." - }; - constexpr openspace::properties::Property::PropertyInfo IdentifierInfo = { "Identifier", "Identifier", - "Identifier used as part of key to access data in syncable central storage." + "Identifier used as part of key to access data in centralized central storage." }; constexpr openspace::properties::Property::PropertyInfo SizeOptionInfo = { @@ -90,44 +78,47 @@ namespace { "This value determines how the size of the data points are rendered." }; - constexpr openspace::properties::Property::PropertyInfo ColorMapEnabledInfo = { - "ColorMapEnabled", - "ColorMap Enabled", + constexpr openspace::properties::Property::PropertyInfo ColormapEnabledInfo = { + "ColormapEnabled", + "Colormap Enabled", "Boolean to determine whether to use colormap or not." }; - constexpr openspace::properties::Property::PropertyInfo LoadNewColorMapInfo = { - "LoadNewColorMap", - "Load New ColorMap", - "Boolean to determine whether to load new colormap or not." + constexpr openspace::properties::Property::PropertyInfo ColormapMinInfo = { + "ColormapMin", + "Colormap min", + "Minimum value to sample from color map." + }; + + constexpr openspace::properties::Property::PropertyInfo ColormapMaxInfo = { + "ColormapMax", + "Colormap max", + "Maximum value to sample from color map." }; struct [[codegen::Dictionary(RenderablePointsCloud)]] Parameters { // [[codegen::verbatim(ColorInfo.description)]] - std::optional color; + std::optional color; // [[codegen::verbatim(SizeInfo.description)]] std::optional size; - // [[codegen::verbatim(ToggleVisibilityInfo.description)]] - std::optional toggleVisiblity; - // [[codegen::verbatim(DataInfo.description)]] std::optional> data; - // [[codegen::verbatim(DataStorageKeyInfo.description)]] - std::optional dataStorageKey; - // [[codegen::verbatim(IdentifierInfo.description)]] std::optional identifier; - // [[codegen::verbatim(ColorMapEnabledInfo.description)]] - std::optional colorMapEnabled; + // [[codegen::verbatim(ColormapMinInfo.description)]] + std::optional colormapMin; - // [[codegen::verbatim(LoadNewColorMapInfo.description)]] - std::optional loadNewColorMap; + // [[codegen::verbatim(ColormapMaxInfo.description)]] + std::optional colormapMax; - enum class SizeOption { + // [[codegen::verbatim(ColormapEnabledInfo.description)]] + std::optional colormapEnabled; + + enum class SizeOption : uint32_t { Uniform, NonUniform }; @@ -145,12 +136,12 @@ documentation::Documentation RenderablePointsCloud::Documentation() { RenderablePointsCloud::RenderablePointsCloud(const ghoul::Dictionary& dictionary) : Renderable(dictionary) - , _color(ColorInfo, glm::vec3(0.5f), glm::vec3(0.f), glm::vec3(1.f)) - , _size(SizeInfo, 1.f, 0.f, 150.f) - , _isVisible(ToggleVisibilityInfo, true) + , _color(ColorInfo, glm::vec4(glm::vec3(0.5f), 1.f), glm::vec4(0.f), glm::vec4(1.f), glm::vec4(.001f)) + , _size(SizeInfo, 1.f, 0.f, 30.f, .001f) , _sizeOption(SizeOptionInfo, properties::OptionProperty::DisplayType::Dropdown) - , _colorMapEnabled(ColorMapEnabledInfo, false) - , _loadNewColorMap(LoadNewColorMapInfo, false) + , _colormapEnabled(ColormapEnabledInfo, false) + , _colormapMin(ColormapMinInfo) + , _colormapMax(ColormapMaxInfo) { const Parameters p = codegen::bake(dictionary); @@ -158,27 +149,37 @@ RenderablePointsCloud::RenderablePointsCloud(const ghoul::Dictionary& dictionary _color.setViewOption(properties::Property::ViewOptions::Color); addProperty(_color); - // Check if a key to data stored in the module's centralized memory was included - _nValuesPerPoint = 3; - _dataStorageKey = p.dataStorageKey.value(); - _identifier = p.identifier.value(); _size = p.size.value_or(_size); addProperty(_size); - _isVisible = p.toggleVisiblity.value_or(_isVisible); - addProperty(_isVisible); - addProperty(_opacity); - addProperty(_colorMapEnabled); + auto colormapMinMaxChecker = [this] { + if (_colormapMin.value() > _colormapMax.value()) { + auto temp = _colormapMin.value(); + _colormapMin = _colormapMax.value(); + _colormapMax = temp; + } + }; - addProperty(_loadNewColorMap); + _colormapMin = p.colormapMin.value_or(_colormapMin); + _colormapMin.setVisibility(properties::Property::Visibility::Hidden); + _colormapMin.onChange(colormapMinMaxChecker); + addProperty(_colormapMin); + + _colormapMax = p.colormapMax.value_or(_colormapMax); + _colormapMax.setVisibility(properties::Property::Visibility::Hidden); + _colormapMax.onChange(colormapMinMaxChecker); + addProperty(_colormapMax); + + _colormapEnabled = p.colormapEnabled.value_or(_colormapEnabled); + addProperty(_colormapEnabled); _sizeOption.addOptions({ { SizeOption::Uniform, "Uniform" }, - { SizeOption::NonUniform, "NonUniform" } + { SizeOption::NonUniform, "Non Uniform" } }); if (p.sizeOption.has_value()) { switch (*p.sizeOption) { @@ -190,16 +191,11 @@ RenderablePointsCloud::RenderablePointsCloud(const ghoul::Dictionary& dictionary break; } } - _sizeOption.onChange([&] { _isDirty = true; }); addProperty(_sizeOption); } bool RenderablePointsCloud::isReady() const { - return _shaderProgram && (!_fullData.empty()); -} - -void RenderablePointsCloud::initialize() { - loadData(); + return _shaderProgram && _identifier.has_value() && _identifier.value() != ""; } void RenderablePointsCloud::initializeGL() { @@ -214,11 +210,12 @@ void RenderablePointsCloud::initializeGL() { } void RenderablePointsCloud::deinitializeGL() { - glDeleteVertexArrays(1, &_vertexArrayObjectID); - _vertexArrayObjectID = 0; + glDeleteBuffers(1, &_vbo); + _vbo = 0; + glDeleteVertexArrays(1, &_vao); + _vao = 0; - glDeleteBuffers(1, &_vertexBufferObjectID); - _vertexBufferObjectID = 0; + _colormapTexture.reset(); if (_shaderProgram) { global::renderEngine->removeRenderProgram(_shaderProgram.get()); @@ -227,13 +224,8 @@ void RenderablePointsCloud::deinitializeGL() { } void RenderablePointsCloud::render(const RenderData& data, RendererTasks&) { - if (_fullData.empty()) { - return; - } - - if (!_isVisible) { - return; - } + auto pointDataSlice = getDataSlice(DataSliceKey::Points); + if (pointDataSlice->empty()) return; _shaderProgram->activate(); @@ -259,17 +251,28 @@ void RenderablePointsCloud::render(const RenderData& data, RendererTasks&) { cameraViewProjectionMatrix ); - if (_loadNewColorMap) loadColorMap(); - - if (_colorMapEnabled && _colorMapTexture) { - // TODO: Set _colorMapTexture in shader. A trasnfer function similar to + ghoul::opengl::TextureUnit colorUnit; + if (_colormapTexture) { + // _colormapAttributeData + // TODO: Set _colormapTextre in shader. A trasnfer function similar to // 'bv2rgb' in C:\OpenSpace\SoftwareIntegration\modules\space\shaders\star_fs.glsl // should probably be used. + + colorUnit.activate(); + _colormapTexture->bind(); + _shaderProgram->setUniform(_uniformCache.colormapTexture, colorUnit); } else { - _shaderProgram->setUniform(_uniformCache.color, _color); + // We need to set the uniform to something, or the shader doesn't work + _shaderProgram->setUniform(_uniformCache.colormapTexture, colorUnit); } + _shaderProgram->setUniform(_uniformCache.colormapMin, _colormapMin); + _shaderProgram->setUniform(_uniformCache.colormapMax, _colormapMax); + _shaderProgram->setUniform(_uniformCache.colormapEnabled, _colormapEnabled); + + _shaderProgram->setUniform(_uniformCache.color, _color); + _shaderProgram->setUniform(_uniformCache.opacity, _opacity); _shaderProgram->setUniform(_uniformCache.size, _size); _shaderProgram->setUniform(_uniformCache.sizeOption, _sizeOption); @@ -279,8 +282,8 @@ void RenderablePointsCloud::render(const RenderData& data, RendererTasks&) { glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glDepthMask(false); - glBindVertexArray(_vertexArrayObjectID); - const GLsizei nPoints = static_cast(_fullData.size() / _nValuesPerPoint); + glBindVertexArray(_vao); + const GLsizei nPoints = static_cast(pointDataSlice->size() / 3); glDrawArrays(GL_POINTS, 0, nPoints); glBindVertexArray(0); @@ -297,130 +300,228 @@ void RenderablePointsCloud::update(const UpdateData&) { ghoul::opengl::updateUniformLocations(*_shaderProgram, _uniformCache, UniformNames); } - if (!_isDirty) { + bool updatedDataSlices = checkDataStorage(); + + if (updatedDataSlices) { + if (_vao == 0) { + glGenVertexArrays(1, &_vao); + LDEBUG(fmt::format("Generating Vertex Array id '{}'", _vao)); + } + if (_vbo == 0) { + glGenBuffers(1, &_vbo); + LDEBUG(fmt::format("Generating Vertex Buffer Object id '{}'", _vbo)); + } + + glBindVertexArray(_vao); + glBindBuffer(GL_ARRAY_BUFFER, _vbo); + + auto pointDataSlice = getDataSlice(DataSliceKey::Points); + auto colormapAttrDataSlice = getDataSlice(DataSliceKey::ColormapAttributes); + + if (pointDataSlice->empty()) return; + + // ========================== Create resulting data slice and buffer it ========================== + std::vector bufferData; + bufferData.reserve(pointDataSlice->size() / 3); + + for(size_t i = 0, j = 0; j < pointDataSlice->size(); ++i, j += 3) { + bufferData.push_back(pointDataSlice->at(j)); + bufferData.push_back(pointDataSlice->at(j + 1)); + bufferData.push_back(pointDataSlice->at(j + 2)); + + if (colormapAttrDataSlice->size() > i) { + bufferData.push_back(colormapAttrDataSlice->at(i)); + } + else { + bufferData.push_back(0.0); + } + } + + glBufferData( + GL_ARRAY_BUFFER, + bufferData.size() * sizeof(GLfloat), + bufferData.data(), + GL_STATIC_DRAW + ); + // ============================================================================================== + + // ========================================= VAO stuff ========================================= + GLsizei stride = static_cast(sizeof(GLfloat) * 4); + GLint positionAttribute = _shaderProgram->attributeLocation("in_position"); + glEnableVertexAttribArray(positionAttribute); + glVertexAttribPointer( + positionAttribute, + 3, + GL_FLOAT, + GL_FALSE, + stride, + nullptr + ); + + if (_hasLoadedColormapAttributeData) { + GLint colormapScalarsAttribute = _shaderProgram->attributeLocation("in_colormapAttributeScalar"); + glEnableVertexAttribArray(colormapScalarsAttribute); + glVertexAttribPointer( + colormapScalarsAttribute, + 1, + GL_FLOAT, + GL_FALSE, + stride, + reinterpret_cast(sizeof(GLfloat) * 3) + ); + } + // ============================================================================================== + + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + } + + // This can happen if the user checks the "_colormapEnabled" checkbox in the GUI + auto colormapAttributeData = getDataSlice(DataSliceKey::ColormapAttributes); + if (_colormapEnabled.value() && (!_colormapTexture || colormapAttributeData->empty())) { + if (!_colormapTexture) { + LINFO("Color map not loaded. Has it been sent from external software?"); + } + else { + LINFO("Color map attribute data not loaded. Has it been sent from external software?"); + } + _colormapEnabled = false; + } +} + +bool RenderablePointsCloud::checkDataStorage() { + if (!_identifier.has_value()) { + LERROR("No identifier found in renderable"); + return false; + } + + bool updatedDataSlices = false; + auto softwareIntegrationModule = global::moduleEngine->module(); + std::string dataPointsKey = _identifier.value() + "-DataPoints"; + std::string colorMapKey = _identifier.value() + "-Colormap"; + std::string colorMapScalarsKey = _identifier.value() + "-CmapAttributeData"; + + if (softwareIntegrationModule->isDataDirty(dataPointsKey)) { + loadData(dataPointsKey, softwareIntegrationModule); + updatedDataSlices = true; + } + + if (softwareIntegrationModule->isDataDirty(colorMapKey)) { + loadColormap(colorMapKey, softwareIntegrationModule); + } + + if (softwareIntegrationModule->isDataDirty(colorMapScalarsKey)) { + loadCmapAttributeData(colorMapScalarsKey, softwareIntegrationModule); + updatedDataSlices = true; + } + + return updatedDataSlices; +} + +void RenderablePointsCloud::loadData( + const std::string& storageKey, SoftwareIntegrationModule* softwareIntegrationModule +) { + // Fetch data from module's centralized storage + auto fullPointData = softwareIntegrationModule->fetchData(storageKey); + + if (fullPointData.empty()) { + LWARNING("There was an issue trying to fetch the point data from the centralized storage."); return; } - LDEBUG("Regenerating data"); - - // @TODO (emmbr26 2021-02-11) This 'createDataSlice'step doesn't really seem - // necessary, but could rather be combined with the loadData() step? - createDataSlice(); - - int size = static_cast(_slicedData.size()); - - if (_vertexArrayObjectID == 0) { - glGenVertexArrays(1, &_vertexArrayObjectID); - LDEBUG(fmt::format("Generating Vertex Array id '{}'", _vertexArrayObjectID)); - } - if (_vertexBufferObjectID == 0) { - glGenBuffers(1, &_vertexBufferObjectID); - LDEBUG(fmt::format("Generating Vertex Buffer Object id '{}'", _vertexBufferObjectID)); - } - - glBindVertexArray(_vertexArrayObjectID); - glBindBuffer(GL_ARRAY_BUFFER, _vertexBufferObjectID); - glBufferData( - GL_ARRAY_BUFFER, - size * sizeof(float), - _slicedData.data(), - GL_STATIC_DRAW - ); - - GLint positionAttribute = _shaderProgram->attributeLocation("in_position"); - - glEnableVertexAttribArray(positionAttribute); - glVertexAttribPointer( - positionAttribute, - 4, - GL_FLOAT, - GL_FALSE, - 0, - nullptr - ); - - glBindVertexArray(0); - - _isDirty = false; -} - -void RenderablePointsCloud::createDataSlice() { - _slicedData.clear(); - _slicedData.reserve(4 * (_fullData.size() / _nValuesPerPoint)); + auto pointDataSlice = getDataSlice(DataSliceKey::Points); + pointDataSlice->clear(); + pointDataSlice->reserve(fullPointData.size() * 3); + // Create data slice auto addPosition = [&](const glm::vec4& pos) { - for (int j = 0; j < 4; ++j) { - _slicedData.push_back(pos[j]); + for (glm::vec4::length_type j = 0; j < glm::vec4::length() - 1; ++j) { + pointDataSlice->push_back(pos[j]); } }; - for (size_t i = 0; i < _fullData.size(); i += _nValuesPerPoint) { - glm::dvec4 transformedPos = glm::dvec4( - _fullData[i + 0], - _fullData[i + 1], - _fullData[i + 2], + for (size_t i = 0; i < fullPointData.size(); i += 3) { + glm::dvec4 transformedPos = { + fullPointData[i + 0], + fullPointData[i + 1], + fullPointData[i + 2], 1.0 - ); + }; // W-normalization transformedPos /= transformedPos.w; - transformedPos *= openspace::distanceconstants::Parsec; + transformedPos *= distanceconstants::Parsec; + addPosition(transformedPos); } } -void RenderablePointsCloud::loadData() { - if (!_dataStorageKey.has_value()) { - LWARNING("No point data found"); - return; - } - - // @TODO: potentially combine point data with additional data about the points, - // such as luminosity - - // Fetch data from module's centralized storage - auto module = global::moduleEngine->module(); - _fullData = module->fetchData(_dataStorageKey.value()); - - _isDirty = true; -} - -void RenderablePointsCloud::loadColorMap() { - if (!_identifier.has_value()) { - LWARNING("No data storage identifier found"); - return; - } - - // Fetch data from module's centralized storage - auto module = global::moduleEngine->module(); - std::vector colorMap = module->fetchData(_identifier.value() + "-ColorMap"); +void RenderablePointsCloud::loadColormap( + const std::string& storageKey, SoftwareIntegrationModule* softwareIntegrationModule +) { + std::vector colorMap = softwareIntegrationModule->fetchData(storageKey); if (colorMap.empty()) { - LWARNING("There was an issue trying to fetch the colormap data from the syncable storage."); + LWARNING("There was an issue trying to fetch the colormap data from the centralized storage."); return; } - int width = colorMap.size(); - uint8_t* values = new uint8_t[width]; + size_t nValues = colorMap.size(); + uint8_t* values = new uint8_t[nValues]; - int i = 0; - while (i < width) { - values[i++] = static_cast(colorMap[i] * 255); - values[i++] = static_cast(colorMap[i] * 255); - values[i++] = static_cast(colorMap[i] * 255); - values[i++] = static_cast(colorMap[i] * 255); + for (size_t i = 0; i < nValues; ++i) { + values[i] = static_cast(colorMap[i] * 255); } - GLenum type = GL_TEXTURE_1D; - _colorMapTexture = std::make_unique( + _colormapTexture.reset(); + _colormapTexture = std::make_unique( values, - glm::uvec3(width, 1, 1), - type, + glm::size3_t(nValues / 4, 1, 1), + GL_TEXTURE_1D, ghoul::opengl::Texture::Format::RGBA ); + _colormapTexture->uploadTexture(); - _isDirty = true; - _loadNewColorMap = false; - _colorMapEnabled = true; + _hasLoadedColormap = true; +} + +void RenderablePointsCloud::loadCmapAttributeData( + const std::string& storageKey, SoftwareIntegrationModule* softwareIntegrationModule +) { + auto colormapAttributeData = softwareIntegrationModule->fetchData(storageKey); + + if (colormapAttributeData.empty()) { + LWARNING("There was an issue trying to fetch the colormap data from the centralized storage."); + return; + } + + auto pointDataSlice = getDataSlice(DataSliceKey::Points); + + if (pointDataSlice->size() / 3 != colormapAttributeData.size()) { + LWARNING(fmt::format( + "There is a mismatch in the amount of colormap attribute scalars ({}) and the amount of points ({})", + colormapAttributeData.size(), pointDataSlice->size() / 3 + )); + _colormapEnabled = false; + return; + } + + auto colormapAttributeDataSlice = getDataSlice(DataSliceKey::ColormapAttributes); + colormapAttributeDataSlice->clear(); + colormapAttributeDataSlice->reserve(colormapAttributeData.size()); + + for (size_t i = 0; i < (pointDataSlice->size() / 3); ++i) { + colormapAttributeDataSlice->push_back(colormapAttributeData[i]); + } + + _hasLoadedColormapAttributeData = true; + LDEBUG("Rerendering colormap attribute data"); +} + +std::shared_ptr RenderablePointsCloud::getDataSlice(DataSliceKey key) { + if (!_dataSlices.count(key)) { + _dataSlices.insert({ key, std::make_shared() }); + } + return _dataSlices.at(key); } } // namespace openspace diff --git a/modules/softwareintegration/rendering/renderablepointscloud.h b/modules/softwareintegration/rendering/renderablepointscloud.h index 539843bd08..5037aca698 100644 --- a/modules/softwareintegration/rendering/renderablepointscloud.h +++ b/modules/softwareintegration/rendering/renderablepointscloud.h @@ -25,15 +25,17 @@ #ifndef __OPENSPACE_MODULE_SOFTWAREINTEGRATION___RENDERABLEPOINTSCLOUD___H__ #define __OPENSPACE_MODULE_SOFTWAREINTEGRATION___RENDERABLEPOINTSCLOUD___H__ +#include + #include #include #include -#include +#include +#include #include #include #include -#include namespace openspace::documentation { struct Documentation; } @@ -45,56 +47,69 @@ namespace ghoul::opengl { namespace openspace { +class SoftwareIntegrationModule; + class RenderablePointsCloud : public Renderable { public: RenderablePointsCloud(const ghoul::Dictionary& dictionary); - void initialize() override; void initializeGL() override; void deinitializeGL() override; bool isReady() const override; - void render(const RenderData& data, RendererTasks& rendererTask) override; + void render(const RenderData& data, RendererTasks&) override; void update(const UpdateData& data) override; static documentation::Documentation Documentation(); protected: - void createDataSlice(); - void loadData(); - void loadColorMap(); + void loadData(const std::string& storageKey, SoftwareIntegrationModule* softwareIntegrationModule); + void loadColormap(const std::string& storageKey, SoftwareIntegrationModule* softwareIntegrationModule); + void loadCmapAttributeData(const std::string& storageKey, SoftwareIntegrationModule* softwareIntegrationModule); - bool _hasPointData = false; - bool _isDirty = true; + bool checkDataStorage(); std::unique_ptr _shaderProgram = nullptr; - UniformCache(color, opacity, size, modelMatrix, cameraUp, - cameraViewProjectionMatrix, eyePosition, sizeOption) _uniformCache; + UniformCache( + color, opacity, size, modelMatrix, cameraUp, + cameraViewProjectionMatrix, eyePosition, sizeOption, + colormapTexture, colormapMin, colormapMax, colormapEnabled + ) _uniformCache; - properties::BoolProperty _isVisible; properties::FloatProperty _size; - properties::Vec3Property _color; + properties::Vec4Property _color; properties::OptionProperty _sizeOption; - - std::vector _fullData; - std::vector _slicedData; - std::unique_ptr _colorMapTexture; + properties::FloatProperty _colormapMin; + properties::FloatProperty _colormapMax; + + std::unique_ptr _colormapTexture; + properties::BoolProperty _colormapEnabled; - std::optional _dataStorageKey = std::nullopt; std::optional _identifier = std::nullopt; - properties::BoolProperty _colorMapEnabled; - properties::BoolProperty _loadNewColorMap; - int _nValuesPerPoint = 0; + bool _hasLoadedColormapAttributeData = false; + bool _hasLoadedColormap = false; - GLuint _vertexArrayObjectID = 0; - GLuint _vertexBufferObjectID = 0; + enum class DataSliceKey : size_t { + Points = 0, + ColormapAttributes + }; + using DataSlice = std::vector; + std::unordered_map> _dataSlices; + std::shared_ptr getDataSlice(DataSliceKey key); enum SizeOption { Uniform = 0, NonUniform = 1 }; + + struct VBOLayout { + glm::vec3 position; + float colormapAttributeData; + }; + GLuint _vbo = 0; + GLuint _vao = 0; }; }// namespace openspace diff --git a/modules/softwareintegration/shaders/point_fs.glsl b/modules/softwareintegration/shaders/point_fs.glsl index 98e201dc25..d33db12e3d 100644 --- a/modules/softwareintegration/shaders/point_fs.glsl +++ b/modules/softwareintegration/shaders/point_fs.glsl @@ -25,25 +25,43 @@ #include "fragment.glsl" #include "PowerScaling/powerScaling_fs.hglsl" +flat in float ge_colormapAttributeScalar; in vec2 coords; flat in float ge_screenSpaceDepth; in vec4 ge_positionViewSpace; -uniform vec3 color; +uniform vec4 color; uniform float opacity; -Fragment getFragment() { - if (opacity < 0.01) discard; +uniform float colormapMin; +uniform float colormapMax; +uniform bool colormapEnabled; +uniform sampler1D colormapTexture; +vec4 attributeScalarToRgb(float attributeData) { + // Linear interpolation + float t = (attributeData - colormapMin) / (colormapMax - colormapMin); + + // Sample texture with interpolated value + return texture(colormapTexture, t); +} + +Fragment getFragment() { const float radius = 0.5; - float d = length(coords - radius); - if (d > 0.5) discard; + float distance = length(coords - radius); + if (distance > 0.6) discard; // calculate distance from the origin point - float circle = smoothstep(radius, radius - (radius * 0.3), d); + float circle = smoothstep(radius, radius - (radius * 0.2), distance); + + vec4 outputColor = vec4(color.rgb, color.a * opacity); + if (colormapEnabled) { + vec4 colorFromColormap = attributeScalarToRgb(ge_colormapAttributeScalar); + outputColor = vec4(colorFromColormap.rgb, colorFromColormap.a * opacity); + } Fragment frag; - frag.color = vec4(color, opacity) * vec4(circle); + frag.color = outputColor * vec4(circle); frag.depth = ge_screenSpaceDepth; frag.gPosition = ge_positionViewSpace; frag.gNormal = vec4(0.0, 0.0, 0.0, 1.0); diff --git a/modules/softwareintegration/shaders/point_ge.glsl b/modules/softwareintegration/shaders/point_ge.glsl index 407d46fa2e..288b4645a9 100644 --- a/modules/softwareintegration/shaders/point_ge.glsl +++ b/modules/softwareintegration/shaders/point_ge.glsl @@ -29,10 +29,13 @@ layout(points) in; layout(triangle_strip, max_vertices = 4) out; +in float vs_colormapAttributeScalar[]; + flat out float ge_screenSpaceDepth; out vec4 ge_positionViewSpace; out vec2 coords; +out float ge_colormapAttributeScalar; uniform dvec3 eyePosition; uniform dvec3 cameraUp; @@ -59,6 +62,8 @@ double scaleForUniform(vec3 pos) { } void main() { + ge_colormapAttributeScalar = vs_colormapAttributeScalar[0]; + vec3 pos = gl_in[0].gl_Position.xyz; double scaleMultiply = 1.0; diff --git a/modules/softwareintegration/shaders/point_vs.glsl b/modules/softwareintegration/shaders/point_vs.glsl index 607c518511..71ca3ca0bb 100644 --- a/modules/softwareintegration/shaders/point_vs.glsl +++ b/modules/softwareintegration/shaders/point_vs.glsl @@ -27,7 +27,12 @@ #include "PowerScaling/powerScaling_vs.hglsl" layout(location = 0) in vec3 in_position; +in float in_colormapAttributeScalar; + +out float vs_colormapAttributeScalar; void main() { + vs_colormapAttributeScalar = in_colormapAttributeScalar; + gl_Position = vec4(in_position, 1.0); } diff --git a/modules/softwareintegration/simp.cpp b/modules/softwareintegration/simp.cpp index 4e10ae47e9..382888024f 100644 --- a/modules/softwareintegration/simp.cpp +++ b/modules/softwareintegration/simp.cpp @@ -37,21 +37,21 @@ namespace openspace { namespace simp { -SimpError::SimpError(const ErrorCode _errorCode, const std::string& msg) +SimpError::SimpError(const utils::ErrorCode _errorCode, const std::string& msg) : errorCode{errorCode}, ghoul::RuntimeError(fmt::format("{}: Error Code: {} - {}", "SIMP error", static_cast(_errorCode), msg), "Software Integration Messaging Protocol error") {} -bool isEndOfCurrentValue(const std::vector& message, size_t offset) { +bool utils::isEndOfCurrentValue(const std::vector& message, size_t offset) { if (offset >= message.size()) { throw SimpError( - ErrorCode::OffsetLargerThanMessageSize, + utils::ErrorCode::OffsetLargerThanMessageSize, "Unexpectedly reached the end of the message..." ); } if (message.size() > 0 && offset == message.size() - 1 && message[offset] != SEP) { throw SimpError( - ErrorCode::ReachedEndBeforeSeparator, + utils::ErrorCode::ReachedEndBeforeSeparator, "Reached end of message before reading separator character..." ); } @@ -60,15 +60,20 @@ bool isEndOfCurrentValue(const std::vector& message, size_t offset) { } MessageType getMessageType(const std::string& type) { - if (_messageTypeFromSIMPType.count(type) == 0) return MessageType::Unknown; - return _messageTypeFromSIMPType.at(type); + if (utils::_messageTypeFromSIMPType.count(type) == 0) return MessageType::Unknown; + return utils::_messageTypeFromSIMPType.at(type); } -std::string getSIMPType(const MessageType& type){ - for (auto [key, messageType] : _messageTypeFromSIMPType) { - if (messageType == type) return key; - } - return ""; +std::string getSIMPType(const MessageType& type) { + auto it = std::find_if( + utils::_messageTypeFromSIMPType.begin(), + utils::_messageTypeFromSIMPType.end(), + [type](const std::pair& p) { + return type == p.second; + } + ); + if (it == utils::_messageTypeFromSIMPType.end()) return "UNKN"; + return it->first; } std::string formatLengthOfSubject(size_t lengthOfSubject) { @@ -82,10 +87,8 @@ std::string formatUpdateMessage(MessageType messageType, std::string_view identifier, std::string_view value) { - const int lengthOfIdentifier = static_cast(identifier.length()); - const int lengthOfValue = static_cast(value.length()); std::string subject = fmt::format( - "{}{}{}{}", lengthOfIdentifier, identifier, lengthOfValue, value + "{}{}{}{}", identifier, SEP, value, SEP ); const std::string lengthOfSubject = formatLengthOfSubject(subject.length()); @@ -95,19 +98,191 @@ std::string formatUpdateMessage(MessageType messageType, std::string formatConnectionMessage(std::string_view software) { std::string subject = fmt::format( - "{}", software + "{}{}", software, SEP ); const std::string lengthOfSubject = formatLengthOfSubject(subject.length()); - return fmt::format("{}{}{}{}", ProtocolVersion, "CONN", lengthOfSubject, subject); + return fmt::format("{}{}{}{}", ProtocolVersion, getSIMPType(MessageType::Connection), lengthOfSubject, subject); } -std::string formatDisconnectionMessage() { - const std::string lengthOfSubject = formatLengthOfSubject(0); - return fmt::format("{}{}{}", ProtocolVersion, "DISC", lengthOfSubject); +std::string formatColorMessage(std::string_view identifier, glm::vec4 color) { + std::ostringstream value_stream; + value_stream << "[" << floatToHex(color.r) << SEP << floatToHex(color.g) << SEP + << floatToHex(color.b) << SEP << floatToHex(color.a) << SEP << "]"; + + return formatUpdateMessage(MessageType::Color, identifier, value_stream.str()); +} + +std::string formatPointDataCallbackMessage(std::string_view identifier) { + std::string subject = fmt::format( + "{}{}", identifier, SEP + ); + + const std::string lengthOfSubject = formatLengthOfSubject(subject.length()); + + return fmt::format("{}{}{}{}", ProtocolVersion, getSIMPType(MessageType::PointData), lengthOfSubject, subject); +} + +std::string floatToHex(const float f) { + const uint32_t *int_representation = reinterpret_cast(&f); + std::ostringstream stream; + stream << "0x" << std::setfill ('0') << std::setw(sizeof(float)*2) << std::hex << *int_representation; + + return stream.str(); +} + +float hexToFloat(const std::string& f) { + uint32_t int_value = static_cast(std::stoul(f, nullptr, 16)); + std::ostringstream stream; + stream << std::dec << int_value; + return *reinterpret_cast(&int_value); +} + +int readIntValue(const std::vector& message, size_t& offset) { + std::string string_value; + int value; + bool isHex = false; + + while (!utils::isEndOfCurrentValue(message, offset)) { + char c = message[offset]; + if (c == 'x' || c == 'X') isHex = true; + string_value.push_back(c); + offset++; + } + + try { + value = std::stoi(string_value, nullptr, isHex ? 16 : 10); + } + catch(std::exception &err) { + throw SimpError( + utils::ErrorCode::Generic, + fmt::format("Error when trying to parse the integer {}: {}", string_value, err.what()) + ); + } + + ++offset; + return value; +} + +float readFloatValue(const std::vector& message, size_t& offset) { + std::string string_value; + float value; + + while (!utils::isEndOfCurrentValue(message, offset)) { + string_value.push_back(message[offset]); + offset++; + } + + try { + value = hexToFloat(string_value); + } + catch(std::exception &err) { + throw SimpError( + utils::ErrorCode::Generic, + fmt::format("Error when trying to parse the float {}: {}", string_value, err.what()) + ); + } + + ++offset; + return value; +} + +void readColormap( + const std::vector& message, size_t& offset, size_t nColors, std::vector& colorMap +) { + colorMap.reserve(nColors * 4); + while (message[offset] != SEP) { + glm::vec4 color = utils::readSingleColor(message, offset); + + // Colormap should be stored in a sequential vector + // of floats for syncing between nodes and when + // loaded to as a texture in the shader. + colorMap.push_back(color[0]); + colorMap.push_back(color[1]); + colorMap.push_back(color[2]); + colorMap.push_back(color[3]); + } + + offset++; +} + +glm::vec4 utils::readSingleColor(const std::vector& message, size_t& offset) { + if (message[offset] != '[') { + throw SimpError( + utils::ErrorCode::Generic, + fmt::format("Expected to read '[', got {} in 'readColor'", message[offset]) + ); + } + ++offset; + + float r = readFloatValue(message, offset); + float g = readFloatValue(message, offset); + float b = readFloatValue(message, offset); + float a = readFloatValue(message, offset); + + if (message[offset] != ']') { + throw SimpError( + utils::ErrorCode::Generic, + fmt::format("Expected to read ']', got {} in 'readColor'", message[offset]) + ); + } + ++offset; + + return { r, g, b, a }; +} + +glm::vec4 readColor(const std::vector& message, size_t& offset) { + glm::vec4 color = utils::readSingleColor(message, offset); + ++offset; + return color; +} + +std::string readString(const std::vector& message, size_t& offset) { + std::string value; + while (!utils::isEndOfCurrentValue(message, offset)) { + value.push_back(message[offset]); + ++offset; + } + + ++offset; + return value; +} + +void readPointData( + const std::vector& message, + size_t& offset, + size_t nPoints, + size_t dimensionality, + std::vector& pointData +) { + pointData.reserve(nPoints * dimensionality); + + while (!utils::isEndOfCurrentValue(message, offset)) { + if (message[offset] != '[') { + throw SimpError( + utils::ErrorCode::Generic, + fmt::format("Expected to read '[', got {} in 'readPointData'", message[offset]) + ); + } + ++offset; + + for (int i = 0; i < dimensionality; ++i) { + pointData.push_back(readFloatValue(message, offset)); + } + + if (message[offset] != ']') { + throw SimpError( + utils::ErrorCode::Generic, + fmt::format("Expected to read ']', got {} in 'readPointData'", message[offset]) + ); + } + ++offset; + } + + ++offset; } } // namespace simp -} // namespace openspace \ No newline at end of file +} // namespace openspace diff --git a/modules/softwareintegration/simp.h b/modules/softwareintegration/simp.h index 984ffc32b2..a3e0e78229 100644 --- a/modules/softwareintegration/simp.h +++ b/modules/softwareintegration/simp.h @@ -25,14 +25,32 @@ #ifndef __OPENSPACE_MODULE_SOFTWAREINTEGRATION___SIMP___H__ #define __OPENSPACE_MODULE_SOFTWAREINTEGRATION___SIMP___H__ +#include + namespace openspace { namespace simp { -const float ProtocolVersion = 1.6; +const std::string ProtocolVersion = "1.8"; const char SEP = ';'; +enum class MessageType : uint32_t { + Connection = 0, + PointData, + RemoveSceneGraphNode, + Color, + Colormap, + AttributeData, + Opacity, + Size, + Visibility, + Disconnection, + Unknown +}; + +namespace utils { + enum class ErrorCode : uint32_t { ReachedEndBeforeSeparator = 0, OffsetLargerThanMessageSize, @@ -40,39 +58,32 @@ enum class ErrorCode : uint32_t { Generic, }; -enum class MessageType : uint32_t { - Connection = 0, - ReadPointData, - RemoveSceneGraphNode, - Color, - ColorMap, - Opacity, - Size, - Visibility, - Disconnection, - Unknown -}; - -const std::map _messageTypeFromSIMPType { +const std::unordered_map _messageTypeFromSIMPType { {"CONN", MessageType::Connection}, - {"PDAT", MessageType::ReadPointData}, + {"PDAT", MessageType::PointData}, {"RSGN", MessageType::RemoveSceneGraphNode}, - {"UPCO", MessageType::Color}, - {"LCOL", MessageType::ColorMap}, - {"UPOP", MessageType::Opacity}, - {"UPSI", MessageType::Size}, + {"FCOL", MessageType::Color}, + {"LCOL", MessageType::Colormap}, + {"ATDA", MessageType::AttributeData}, + {"FOPA", MessageType::Opacity}, + {"FPSI", MessageType::Size}, {"TOVI", MessageType::Visibility}, {"DISC", MessageType::Disconnection}, }; -class SimpError : public ghoul::RuntimeError { -public: - ErrorCode errorCode; - explicit SimpError(const ErrorCode _errorCode, const std::string& msg); -}; +glm::vec4 readSingleColor(const std::vector& message, size_t& offset); bool isEndOfCurrentValue(const std::vector& message, size_t offset); +} // namespace utils + +class SimpError : public ghoul::RuntimeError { +public: + utils::ErrorCode errorCode; + explicit SimpError(const utils::ErrorCode _errorCode, const std::string& msg); +}; + + MessageType getMessageType(const std::string& type); std::string getSIMPType(const MessageType& type); @@ -85,8 +96,32 @@ std::string formatUpdateMessage(MessageType messageType, std::string_view value); std::string formatConnectionMessage(std::string_view value); + +std::string formatColorMessage(std::string_view identifier, glm::vec4 color); -std::string formatDisconnectionMessage(); +std::string formatPointDataCallbackMessage(std::string_view identifier); + +std::string floatToHex(const float f); + +float hexToFloat(const std::string& f); + +float readFloatValue(const std::vector& message, size_t& offset); + +int readIntValue(const std::vector& message, size_t& offset); + +std::string readString(const std::vector& message, size_t& offset); + +glm::vec4 readColor(const std::vector& message, size_t& offset); + +void readPointData( + const std::vector& message, size_t& offset, size_t nPoints, + size_t dimensionality, std::vector& pointData +); + +void readColormap( + const std::vector& message, size_t& offset, size_t nColors, + std::vector& colorMap +); } // namespace simp diff --git a/modules/softwareintegration/softwareintegrationmodule.cpp b/modules/softwareintegration/softwareintegrationmodule.cpp index e2860d30b7..16db218373 100644 --- a/modules/softwareintegration/softwareintegrationmodule.cpp +++ b/modules/softwareintegration/softwareintegrationmodule.cpp @@ -45,34 +45,24 @@ SoftwareIntegrationModule::SoftwareIntegrationModule() : OpenSpaceModule(Name) { // The Master node will handle all communication with the external software // and forward it to the Client nodes // 4700, is the defualt port where the tcp socket will be opened to the ext. software - _server = new NetworkEngine(); + _networkEngine = std::make_unique(); } } SoftwareIntegrationModule::~SoftwareIntegrationModule() { - if (global::windowDelegate->isMaster()) { - delete _server; - } + internalDeinitialize(); } -void SoftwareIntegrationModule::storeData(const std::string& key, - const std::vector data) { - _syncableFloatDataStorage.emplace(key, std::move(data)); +void SoftwareIntegrationModule::storeData(const std::string& key, const std::vector& data) { + _syncableFloatDataStorage.store(key, data); } -std::vector SoftwareIntegrationModule::fetchData(const std::string& key) { - auto it = _syncableFloatDataStorage.find(key); - if (it == _syncableFloatDataStorage.end()) { - LERROR(fmt::format( - "Could not find data with key '{}' in the temporary data storage", key - )); - return std::vector(); - } +const std::vector& SoftwareIntegrationModule::fetchData(const std::string& key) { + return _syncableFloatDataStorage.fetch(key); +} - std::vector data = it->second; - _syncableFloatDataStorage.erase(it); - - return std::move(data); +bool SoftwareIntegrationModule::isDataDirty(const std::string& key) { + return _syncableFloatDataStorage.isDirty(key); } void SoftwareIntegrationModule::internalInitialize(const ghoul::Dictionary&) { @@ -83,17 +73,17 @@ void SoftwareIntegrationModule::internalInitialize(const ghoul::Dictionary&) { fRenderable->registerClass("RenderablePointsCloud"); if (global::windowDelegate->isMaster()) { - _server->start(); + _networkEngine->start(); - global::callback::preSync->emplace_back([this]() { _server->update(); }); + global::callback::postSyncPreDraw->emplace_back([this]() { + if (!_networkEngine) return; + _networkEngine->postSync(); + }); } } void SoftwareIntegrationModule::internalDeinitialize() { global::syncEngine->removeSyncables(getSyncables()); - if (global::windowDelegate->isMaster()) { - _server->stop(); - } } std::vector diff --git a/modules/softwareintegration/softwareintegrationmodule.h b/modules/softwareintegration/softwareintegrationmodule.h index ec803333b1..bd96a29485 100644 --- a/modules/softwareintegration/softwareintegrationmodule.h +++ b/modules/softwareintegration/softwareintegrationmodule.h @@ -40,8 +40,9 @@ public: SoftwareIntegrationModule(); ~SoftwareIntegrationModule(); - void storeData(const std::string& key, const std::vector data); - std::vector fetchData(const std::string& key); + void storeData(const std::string& key, const std::vector& data); + const std::vector& fetchData(const std::string& key); + bool isDataDirty(const std::string& key); std::vector documentations() const override; @@ -51,10 +52,12 @@ private: std::vector getSyncables(); - NetworkEngine* _server; // Centralized storage for datasets SyncableFloatDataStorage _syncableFloatDataStorage; + + // Network engine + std::unique_ptr _networkEngine; }; } // namespace openspace diff --git a/modules/softwareintegration/syncablefloatdatastorage.cpp b/modules/softwareintegration/syncablefloatdatastorage.cpp index 965a1ac55b..ab09e211f3 100644 --- a/modules/softwareintegration/syncablefloatdatastorage.cpp +++ b/modules/softwareintegration/syncablefloatdatastorage.cpp @@ -41,15 +41,20 @@ void SyncableFloatDataStorage::encode(SyncBuffer* syncBuffer) { size_t nDataEntries = _storage.size(); syncBuffer->encode(nDataEntries); - for (const auto& [key, dataEntry] : _storage) { + for (auto& [key, value] : _storage) { syncBuffer->encode(key); - // Go trough all data in data entry. - // Sequentially structured (ex: x1, y1, z1, x2, y2, z2...) - size_t nItemsInDataEntry = dataEntry.size(); - syncBuffer->encode(nItemsInDataEntry); - for (auto value : dataEntry) { - syncBuffer->encode(value); + syncBuffer->encode(value.dirty); + + // Only encode data if it is dirty, to save bandwidth + if (value.dirty) { + // Go trough all data in data entry. + // Sequentially structured as: x1, y1, z1, x2, y2, z2... + size_t nItemsInDataEntry = value.data.size(); + syncBuffer->encode(nItemsInDataEntry); + for (auto val : value.data) { + syncBuffer->encode(val); + } } } } @@ -64,60 +69,112 @@ void SyncableFloatDataStorage::decode(SyncBuffer* syncBuffer) { std::string key; syncBuffer->decode(key); - size_t nItemsInDataEntry; - syncBuffer->decode(nItemsInDataEntry); - - // TODO: Change to a glm::fvec3 so we can use an overload - // of decode(glm::fvec3) instead of using a for-loop over floats? - std::vector dataEntry; - dataEntry.reserve(nItemsInDataEntry); - for (size_t j = 0; j < nItemsInDataEntry; ++j) { - float value; - syncBuffer->decode(value); - dataEntry.push_back(value); - } + bool dirty; + syncBuffer->decode(dirty); - _storage[key] = dataEntry; + if (dirty) { + size_t nItemsInDataEntry; + syncBuffer->decode(nItemsInDataEntry); + + std::vector dataEntry; + dataEntry.reserve(nItemsInDataEntry); + for (size_t j = 0; j < nItemsInDataEntry; ++j) { + float value; + syncBuffer->decode(value); + dataEntry.push_back(value); + } + + insertAssign(key, Value{ dataEntry, dirty }); + } } } void SyncableFloatDataStorage::postSync(bool isMaster) { if (isMaster) { - if (_storage.size() > 0) { - // LWARNING(fmt::format("SyncableFloatDataStorage.size() (MASTER): {}", _storage.size())); - } - } - else { - if (_storage.size() > 0) { - // LWARNING(fmt::format("SyncableFloatDataStorage.size() (CLIENT): {}", _storage.size())); + for (auto& [key, value] : _storage) { + if (value.dirty) { + value.dirty = false; + } } } } /* ================================================== */ +const SyncableFloatDataStorage::ValueData& SyncableFloatDataStorage::fetch(const Key& key) { + LDEBUG(fmt::format("Loading data from float data storage: {}", key)); + auto it = find(key); + if (it == end()) { + LERROR(fmt::format( + "Could not find data with key '{}' in the centralized data storage", key + )); + return std::move(ValueData{}); + } + + it->second.localDirty = false; + + return it->second.data; +} + +bool SyncableFloatDataStorage::isDirty(const Key& key) { + auto it = find(key); + if (it == end()) { + return false; + } + + return it->second.localDirty; +} + +void SyncableFloatDataStorage::store(const Key& key, const ValueData& data) { + LDEBUG(fmt::format("Storing data in float data storage: {}", key)); + Value value{ data, true, true }; + + auto old = find(key); + if (old != end()) { + glm::vec3 firstValueOld{}; + for (glm::vec3::length_type i = 0; i < glm::vec3::length(); ++i) { + firstValueOld[i] = old->second.data[i]; + } + + LDEBUG(fmt::format( + "First data point: old: {}", firstValueOld + )); + } + + insertAssign(key, std::move(value)); + + auto newVal = find(key); + if (newVal != end()) { + glm::vec3 firstValueNew{}; + for (glm::vec3::length_type i = 0; i < glm::vec3::length(); ++i) { + firstValueNew[i] = newVal->second.data[i]; + } + + LDEBUG(fmt::format( + "First data point: new {}", firstValueNew + )); + } +} + /* =============== Utility functions ================ */ -SyncableFloatDataStorage::Iterator SyncableFloatDataStorage::erase(SyncableFloatDataStorage::Iterator pos) { - return _storage.erase(pos); -} -SyncableFloatDataStorage::Iterator SyncableFloatDataStorage::erase(const SyncableFloatDataStorage::Iterator first, const SyncableFloatDataStorage::Iterator last) { - return _storage.erase(first, last); -} size_t SyncableFloatDataStorage::erase(const SyncableFloatDataStorage::Key& key) { return _storage.erase(key); } -std::pair SyncableFloatDataStorage::emplace(SyncableFloatDataStorage::Key key, SyncableFloatDataStorage::Value value) { - return _storage.emplace(key, value); +void SyncableFloatDataStorage::insertAssign(Key key, const Value& value) { + auto it = find(key); + if (it == end()) { + _storage.emplace(key, value); + } + else { + it->second = value; + } } -SyncableFloatDataStorage::Value& SyncableFloatDataStorage::at(const SyncableFloatDataStorage::Key& key) { - return _storage.at(key); -} -const SyncableFloatDataStorage::Value& SyncableFloatDataStorage::at(const SyncableFloatDataStorage::Key& key) const { +SyncableFloatDataStorage::Value& SyncableFloatDataStorage::at(const Key& key) { return _storage.at(key); } -SyncableFloatDataStorage::Iterator SyncableFloatDataStorage::find(const SyncableFloatDataStorage::Key& key) { +SyncableFloatDataStorage::Iterator SyncableFloatDataStorage::find(const Key& key) { return _storage.find(key); } /* ================================================== */ @@ -132,10 +189,4 @@ SyncableFloatDataStorage::Iterator SyncableFloatDataStorage::begin() noexcept { } /* ================================================== */ -/* =============== Operator overloads =============== */ -SyncableFloatDataStorage::Value& SyncableFloatDataStorage::operator[](SyncableFloatDataStorage::Key&& key) { - return _storage[key]; -} -/* ================================================== */ - } // namespace openspace diff --git a/modules/softwareintegration/syncablefloatdatastorage.h b/modules/softwareintegration/syncablefloatdatastorage.h index df84d46124..74006aadfb 100644 --- a/modules/softwareintegration/syncablefloatdatastorage.h +++ b/modules/softwareintegration/syncablefloatdatastorage.h @@ -25,48 +25,45 @@ #ifndef __OPENSPACE_MODULE_SOFTWAREINTEGRATION___SYNCABLEFLOATDATASTORAGE___H__ #define __OPENSPACE_MODULE_SOFTWAREINTEGRATION___SYNCABLEFLOATDATASTORAGE___H__ -#include #include +#include + +#include namespace openspace { -/** - * A double buffered implementation of the Syncable interface. - * Users are encouraged to used this class as a default way to synchronize different - * C++ data types using the SyncEngine. - * - * This class aims to handle the synchronization parts and yet act like a regular - * instance of T. Implicit casts are supported, however, when accessing member functions - * or variables, user may have to do explicit casts. - * - * ((T&) t).method(); - * - */ class SyncableFloatDataStorage : public Syncable { public: /* ====================== Types ===================== */ - typedef std::string Key; - typedef std::vector Value; // a dataset stored like x1, y1, z1, x2, y2 .... - typedef std::map Storage; - typedef Storage::iterator Iterator; + struct Value { + // a dataset stored like x1, y1, z1, x2, y2 .... + std::vector data; + bool dirty; + bool localDirty; + }; + using ValueData = decltype(Value::data); + using Key = std::string; + using Storage = std::unordered_map; + using Iterator = Storage::iterator; /* ================================================== */ /* ============== SyncEngine functions ============== */ - // virtual void preSync(bool isMaster) override; virtual void encode(SyncBuffer* syncBuffer) override; virtual void decode(SyncBuffer* syncBuffer) override; virtual void postSync(bool isMaster) override; /* ================================================== */ + const ValueData& fetch(const Key& key); + bool isDirty(const Key& key); + void store(const Key& key, const ValueData& data); + +private: /* =============== Utility functions ================ */ - Iterator erase(Iterator pos); - Iterator erase(const Iterator first, const Iterator last); size_t erase(const Key& key); - std::pair emplace(Key key, Value value); + void insertAssign(Key key, const Value& value); Value& at(const Key& key); - const Value& at(const Key& key) const; Iterator find(const Key& key); /* ================================================== */ @@ -77,16 +74,8 @@ public: Iterator begin() noexcept; /* ================================================== */ - /* =============== Operator overloads =============== */ - Value& operator[](Key&& key); - /* ================================================== */ - -private: std::mutex _mutex; Storage _storage; - - bool showMessageEncode = true; - bool showMessageDecode = true; }; } // namespace openspace