# HG changeset patch # User Sebastien Jodogne # Date 1603467556 -7200 # Node ID b782f78aed425923abde3db094586d45265f0243 # Parent 0d4b11ba86dfc1533f0740426ba713753c3bcd4e rendering osirix annotations diff -r 0d4b11ba86df -r b782f78aed42 Applications/StoneWebViewer/WebApplication/app.js --- a/Applications/StoneWebViewer/WebApplication/app.js Fri Oct 23 15:15:32 2020 +0200 +++ b/Applications/StoneWebViewer/WebApplication/app.js Fri Oct 23 17:39:16 2020 +0200 @@ -548,6 +548,31 @@ app.leftMode = 'full'; } } + + + + // TODO - TEST + axios.get('length.xml') + .then(function (response) { + stone.LoadOsiriXAnnotations(response.data, false); + }); + + axios.get('angle.xml') + .then(function (response) { + stone.LoadOsiriXAnnotations(response.data, false); + }); + + axios.get('arrow.xml') + .then(function (response) { + stone.LoadOsiriXAnnotations(response.data, false); + }); + + axios.get('text.xml') + .then(function (response) { + stone.LoadOsiriXAnnotations(response.data, false); + }); + + }); diff -r 0d4b11ba86df -r b782f78aed42 Applications/StoneWebViewer/WebAssembly/CMakeLists.txt --- a/Applications/StoneWebViewer/WebAssembly/CMakeLists.txt Fri Oct 23 15:15:32 2020 +0200 +++ b/Applications/StoneWebViewer/WebAssembly/CMakeLists.txt Fri Oct 23 17:39:16 2020 +0200 @@ -79,6 +79,20 @@ message(FATAL_ERROR "CMAKE_BUILD_TYPE must match either Debug, RelWithDebInfo or Release" ) endif() + +# We embed a font to be used for on-screen overlays +# --------------------------------------------------------------- + +DownloadPackage( + "a24b8136b8f3bb93f166baf97d9328de" + "http://orthanc.osimis.io/ThirdPartyDownloads/ubuntu-font-family-0.83.zip" + "${CMAKE_BINARY_DIR}/ubuntu-font-family-0.83") + +EmbedResources( + UBUNTU_FONT ${CMAKE_BINARY_DIR}/ubuntu-font-family-0.83/Ubuntu-R.ttf + ) + + ################################################################################ diff -r 0d4b11ba86df -r b782f78aed42 Applications/StoneWebViewer/WebAssembly/JavaScriptLibraries.cmake --- a/Applications/StoneWebViewer/WebAssembly/JavaScriptLibraries.cmake Fri Oct 23 15:15:32 2020 +0200 +++ b/Applications/StoneWebViewer/WebAssembly/JavaScriptLibraries.cmake Fri Oct 23 17:39:16 2020 +0200 @@ -48,6 +48,7 @@ FILES ${CMAKE_CURRENT_BINARY_DIR}/fontawesome-free-5.14.0-web/css/all.css ${CMAKE_CURRENT_BINARY_DIR}/bootstrap-3.4.1-dist/css/bootstrap.css + ${CMAKE_CURRENT_BINARY_DIR}/bootstrap-3.4.1-dist/css/bootstrap.css.map DESTINATION ${ORTHANC_STONE_INSTALL_PREFIX}/css ) diff -r 0d4b11ba86df -r b782f78aed42 Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp --- a/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp Fri Oct 23 15:15:32 2020 +0200 +++ b/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp Fri Oct 23 17:39:16 2020 +0200 @@ -19,6 +19,7 @@ **/ +#include #include @@ -72,10 +73,15 @@ #include #include #include +#include #include #include #include #include +#include +#include +#include +#include #include #include @@ -946,6 +952,22 @@ return false; } } + + + bool ProjectPoint(double& x, + double& y, + const OrthancStone::Vector& v) const + { + if (IsValid()) + { + coordinates_.ProjectPoint(x, y, v); + return true; + } + else + { + return false; + } + } }; @@ -969,6 +991,8 @@ private: static const int LAYER_TEXTURE = 0; static const int LAYER_REFERENCE_LINES = 1; + static const int LAYER_ANNOTATIONS = 2; + static const int LAYER_TEMP = 3; // TODO - REMOVE class ICommand : public Orthanc::IDynamicObject @@ -1053,6 +1077,7 @@ } }; + class SetLowQualityFrame : public ICommand { private: @@ -1282,6 +1307,8 @@ FrameGeometry currentFrameGeometry_; std::list prefetchQueue_; + boost::shared_ptr annotations_; + void ScheduleNextPrefetch() { while (!prefetchQueue_.empty()) @@ -1340,6 +1367,7 @@ } } + void DisplayCurrentFrame() { DisplayedFrameQuality quality = DisplayedFrameQuality_None; @@ -1401,6 +1429,7 @@ currentFrameGeometry_ = FrameGeometry(); } } + void ClearViewport() { @@ -1482,6 +1511,96 @@ pixelSpacingX, pixelSpacingY, frames_->GetFrameTags(index)); layer->SetPixelSpacing(pixelSpacingX, pixelSpacingY); + + /**** + * BEGINNING OF EXPERIMENTAL CODE + ****/ + + std::unique_ptr annotationsLayer; // TODO - Macro layer + std::unique_ptr tempLayer; // TODO - Macro layer + + if (annotations_) + { + std::set a; + annotations_->LookupSopInstanceUid(a, sopInstanceUid); + if (!a.empty()) + { + using namespace OrthancStone::OsiriX; + + std::unique_ptr layer(new OrthancStone::PolylineSceneLayer); + + for (std::set::const_iterator it = a.begin(); it != a.end(); ++it) + { + const Annotation& annotation = annotations_->GetAnnotation(*it); + + switch (annotation.GetType()) + { + case Annotation::Type_Line: + { + const LineAnnotation& line = dynamic_cast(annotation); + double x1, y1, x2, y2; + if (GetCurrentFrameGeometry().ProjectPoint(x1, y1, line.GetPoint1()) && + GetCurrentFrameGeometry().ProjectPoint(x2, y2, line.GetPoint2())) + { + OrthancStone::PolylineSceneLayer::Chain chain; + chain.push_back(OrthancStone::ScenePoint2D(x1, y1)); + chain.push_back(OrthancStone::ScenePoint2D(x2, y2)); + + // TODO - IsArrow + + layer->AddChain(chain, false, 0, 255, 0); + } + break; + } + + case Annotation::Type_Angle: + { + const AngleAnnotation& angle = dynamic_cast(annotation); + double x1, y1, x2, y2, x3, y3; + if (GetCurrentFrameGeometry().ProjectPoint(x1, y1, angle.GetA()) && + GetCurrentFrameGeometry().ProjectPoint(x2, y2, angle.GetCenter()) && + GetCurrentFrameGeometry().ProjectPoint(x3, y3, angle.GetB())) + { + OrthancStone::PolylineSceneLayer::Chain chain; + chain.push_back(OrthancStone::ScenePoint2D(x1, y1)); + chain.push_back(OrthancStone::ScenePoint2D(x2, y2)); + chain.push_back(OrthancStone::ScenePoint2D(x3, y3)); + layer->AddChain(chain, false, 0, 255, 0); + } + break; + } + + case Annotation::Type_Text: + { + const TextAnnotation& text = dynamic_cast(annotation); + double x, y; + OrthancStone::LinearAlgebra::Print(text.GetCenter()); + if (GetCurrentFrameGeometry().ProjectPoint(x, y, text.GetCenter())) + { + std::unique_ptr layer2(new OrthancStone::TextSceneLayer()); + layer2->SetPosition(x, y); + layer2->SetText(text.GetText()); + layer2->SetAnchor(OrthancStone::BitmapAnchor_Center); + layer2->SetColor(255, 0, 0); + tempLayer.reset(layer2.release()); + } + break; + } + + default: + LOG(ERROR) << "Annotation type not implemented: " << annotation.GetType(); + } + } + + annotationsLayer.reset(layer.release()); + } + } + + /**** + * END OF EXPERIMENTAL CODE + ****/ + + if (layer.get() == NULL) { return false; @@ -1494,6 +1613,24 @@ scene.SetLayer(LAYER_TEXTURE, layer.release()); + if (annotationsLayer.get() != NULL) + { + scene.SetLayer(LAYER_ANNOTATIONS, annotationsLayer.release()); + } + else + { + scene.DeleteLayer(LAYER_ANNOTATIONS); + } + + if (tempLayer.get() != NULL) // TODO - REMOVE + { + scene.SetLayer(LAYER_TEMP, tempLayer.release()); + } + else + { + scene.DeleteLayer(LAYER_TEMP); + } + if (fitNextContent_) { lock->RefreshCanvasSize(); @@ -1626,6 +1763,13 @@ LOG(INFO) << "Creating WebGL viewport in canvas: " << canvas; viewport_ = OrthancStone::WebGLViewport::Create(canvas); } + + { + std::unique_ptr lock(viewport_->Lock()); + std::string ttf; + Orthanc::EmbeddedResources::GetFileResource(ttf, Orthanc::EmbeddedResources::UBUNTU_FONT); + lock->GetCompositor().SetFont(0, ttf, 24 /* font size */, Orthanc::Encoding_Latin1); + } emscripten_set_wheel_callback(viewport_->GetCanvasCssSelector().c_str(), this, true, OnWheel); emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, this, false, OnKey); @@ -1775,6 +1919,17 @@ } } + + void UpdateCurrentFrame() + { + if (cursor_.get() != NULL) + { + unsigned int quality; + DisplayFrame(quality, cursor_->GetCurrentIndex()); + } + } + + // This method is used when the layout of the HTML page changes, // which does not trigger the "emscripten_set_resize_callback()" void UpdateSize(bool fitContent) @@ -1995,6 +2150,12 @@ { viewport_->FitForPrint(); } + + void SetAnnotations(boost::shared_ptr annotations) + { + annotations_ = annotations; + } + }; @@ -2004,6 +2165,7 @@ typedef std::map > Viewports; static Viewports allViewports_; static bool showReferenceLines_ = true; +static boost::shared_ptr annotations_; static void UpdateReferenceLines() @@ -2153,6 +2315,7 @@ ViewerViewport::Create(*lock, source_, canvas, cache_, softwareRendering_)); viewport->SetMouseButtonActions(leftButtonAction_, middleButtonAction_, rightButtonAction_); viewport->AcquireObserver(new WebAssemblyObserver); + viewport->SetAnnotations(annotations_); allViewports_[canvas] = viewport; return viewport; } @@ -2174,6 +2337,7 @@ context_.reset(new OrthancStone::WebAssemblyLoadersContext(1, 4, 1)); cache_.reset(new FramesCache); + annotations_.reset(new OrthancStone::OsiriX::CollectionOfAnnotations); DISPATCH_JAVASCRIPT_EVENT("StoneInitialized"); } @@ -2538,4 +2702,38 @@ } EXTERN_CATCH_EXCEPTIONS; } + + + EMSCRIPTEN_KEEPALIVE + int LoadOsiriXAnnotations(const char* xml, + int clearPreviousAnnotations) + { + try + { + if (clearPreviousAnnotations) + { + annotations_->Clear(); + } + + annotations_->LoadXml(xml); + + for (Viewports::iterator it = allViewports_.begin(); it != allViewports_.end(); ++it) + { + // TODO - Check if the viewport contains one of the SOP + // Instance UID from the loaded annotations => focus on this + // instance + + // TODO - If no viewport contains the instance => monitor the + // "ResourcesLoader" as new series get loaded + + assert(it->second != NULL); + it->second->UpdateCurrentFrame(); + } + + LOG(WARNING) << "Loaded " << annotations_->GetSize() << " annotations from OsiriX"; + return 1; + } + EXTERN_CATCH_EXCEPTIONS; + return 0; + } } diff -r 0d4b11ba86df -r b782f78aed42 OrthancStone/Sources/Toolbox/OsiriX/CollectionOfAnnotations.cpp --- a/OrthancStone/Sources/Toolbox/OsiriX/CollectionOfAnnotations.cpp Fri Oct 23 15:15:32 2020 +0200 +++ b/OrthancStone/Sources/Toolbox/OsiriX/CollectionOfAnnotations.cpp Fri Oct 23 17:39:16 2020 +0200 @@ -42,13 +42,16 @@ } - CollectionOfAnnotations::~CollectionOfAnnotations() + void CollectionOfAnnotations::Clear() { for (size_t i = 0; i < annotations_.size(); i++) { assert(annotations_[i] != NULL); delete annotations_[i]; } + + annotations_.clear(); + index_.clear(); } @@ -74,14 +77,44 @@ } else { + size_t pos = annotations_.size(); annotations_.push_back(annotation); + + SopInstanceUidIndex::iterator found = index_.find(annotation->GetSopInstanceUid()); + if (found == index_.end()) + { + std::set s; + s.insert(pos); + index_[annotation->GetSopInstanceUid()] = s; + } + else + { + found->second.insert(pos); + } } } - void CollectionOfAnnotations::ParseXml(const std::string& xml) + + void CollectionOfAnnotations::LookupSopInstanceUid(std::set& target, + const std::string& sopInstanceUid) const + { + SopInstanceUidIndex::const_iterator found = index_.find(sopInstanceUid); + if (found == index_.end()) + { + target.clear(); + } + else + { + target = found->second; + } + } + + + void CollectionOfAnnotations::LoadXml(const char* xml, + size_t size) { pugi::xml_document doc; - pugi::xml_parse_result result = doc.load_buffer(xml.empty() ? NULL : xml.c_str(), xml.size()); + pugi::xml_parse_result result = doc.load_buffer(xml, size); if (!result) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); diff -r 0d4b11ba86df -r b782f78aed42 OrthancStone/Sources/Toolbox/OsiriX/CollectionOfAnnotations.h --- a/OrthancStone/Sources/Toolbox/OsiriX/CollectionOfAnnotations.h Fri Oct 23 15:15:32 2020 +0200 +++ b/OrthancStone/Sources/Toolbox/OsiriX/CollectionOfAnnotations.h Fri Oct 23 17:39:16 2020 +0200 @@ -23,6 +23,7 @@ #include "Annotation.h" +#include #include namespace OrthancStone @@ -32,11 +33,19 @@ class CollectionOfAnnotations : public boost::noncopyable { private: + typedef std::map > SopInstanceUidIndex; + std::vector annotations_; + SopInstanceUidIndex index_; public: - ~CollectionOfAnnotations(); + ~CollectionOfAnnotations() + { + Clear(); + } + void Clear(); + size_t GetSize() const { return annotations_.size(); @@ -46,8 +55,17 @@ void AddAnnotation(Annotation* annotation); // takes ownership - // Parse an XML from OsiriX - void ParseXml(const std::string& xml); + void LookupSopInstanceUid(std::set& target, + const std::string& sopInstanceUid) const; + + // Load an XML from OsiriX + void LoadXml(const char* xml, + size_t size); + + void LoadXml(const std::string& xml) + { + LoadXml(xml.empty() ? NULL : xml.c_str(), xml.size()); + } }; } }