# HG changeset patch # User Sebastien Jodogne # Date 1580826243 -3600 # Node ID 68579a31eeb4b6fa8bd3c9f71a443b8ae1d7eade # Parent 7ec8fea061b91001cb4c5c61558d988a685545fd# Parent 1c7ae79c426d82ef7a73e212237cc32f9037d6e4 integration mainline->broker diff -r 1c7ae79c426d -r 68579a31eeb4 Applications/Generic/GuiAdapter.cpp --- a/Applications/Generic/GuiAdapter.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Applications/Generic/GuiAdapter.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -31,7 +31,7 @@ #endif #if ORTHANC_ENABLE_THREADS == 1 -# include "../../Framework/Messages/LockingEmitter.h" +# include "../../Framework/Deprecated/Messages/LockingEmitter.h" #endif namespace OrthancStone @@ -792,7 +792,7 @@ while (!stop) { { - LockingEmitter::WriterLock lock(lockingEmitter_); + Deprecated::LockingEmitter::WriterLock lock(lockingEmitter_); if(func != NULL) (*func)(cookie); OnAnimationFrame(); // in SDL we must call it @@ -802,7 +802,7 @@ while (!stop && SDL_PollEvent(&event)) { - LockingEmitter::WriterLock lock(lockingEmitter_); + Deprecated::LockingEmitter::WriterLock lock(lockingEmitter_); if (event.type == SDL_QUIT) { diff -r 1c7ae79c426d -r 68579a31eeb4 Applications/Generic/GuiAdapter.h --- a/Applications/Generic/GuiAdapter.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Applications/Generic/GuiAdapter.h Tue Feb 04 15:24:03 2020 +0100 @@ -43,8 +43,8 @@ #include "../../Framework/StoneException.h" -#if ORTHANC_ENABLE_THREADS != 1 -# include "../../Framework/Messages/LockingEmitter.h" +#if ORTHANC_ENABLE_THREADS == 1 +# include "../../Framework/Deprecated/Messages/LockingEmitter.h" #endif #include @@ -95,8 +95,6 @@ struct GuiAdapterWheelEvent; struct GuiAdapterKeyboardEvent; - class LockingEmitter; - #if 1 typedef bool (*OnMouseEventFunc)(std::string canvasId, const GuiAdapterMouseEvent* mouseEvent, void* userData); typedef bool (*OnMouseWheelFunc)(std::string canvasId, const GuiAdapterWheelEvent* wheelEvent, void* userData); @@ -225,7 +223,7 @@ { public: #if ORTHANC_ENABLE_THREADS == 1 - GuiAdapter(LockingEmitter& lockingEmitter) : lockingEmitter_(lockingEmitter) + GuiAdapter(Deprecated::LockingEmitter& lockingEmitter) : lockingEmitter_(lockingEmitter) #else GuiAdapter() #endif @@ -301,7 +299,7 @@ This object is used by the multithreaded Oracle to serialize access to shared data. We need to use it as soon as we access the state. */ - LockingEmitter& lockingEmitter_; + Deprecated::LockingEmitter& lockingEmitter_; #endif /** diff -r 1c7ae79c426d -r 68579a31eeb4 Applications/Generic/NativeStoneApplicationContext.cpp --- a/Applications/Generic/NativeStoneApplicationContext.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Applications/Generic/NativeStoneApplicationContext.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -24,10 +24,10 @@ namespace OrthancStone { - Deprecated::IWidget& NativeStoneApplicationContext::GlobalMutexLocker::SetCentralWidget(Deprecated::IWidget* widget) + void NativeStoneApplicationContext::GlobalMutexLocker::SetCentralWidget( + boost::shared_ptr widget) { that_.centralViewport_.SetCentralWidget(widget); - return *widget; } @@ -45,9 +45,7 @@ } - NativeStoneApplicationContext::NativeStoneApplicationContext(MessageBroker& broker) : - StoneApplicationContext(broker), - centralViewport_(broker), + NativeStoneApplicationContext::NativeStoneApplicationContext() : stopped_(true), updateDelayInMs_(100) // By default, 100ms between each refresh of the content { diff -r 1c7ae79c426d -r 68579a31eeb4 Applications/Generic/NativeStoneApplicationContext.h --- a/Applications/Generic/NativeStoneApplicationContext.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Applications/Generic/NativeStoneApplicationContext.h Tue Feb 04 15:24:03 2020 +0100 @@ -56,7 +56,7 @@ { } - Deprecated::IWidget& SetCentralWidget(Deprecated::IWidget* widget); // Takes ownership + void SetCentralWidget(boost::shared_ptr widget); Deprecated::IViewport& GetCentralViewport() { @@ -67,14 +67,9 @@ { that_.updateDelayInMs_ = delayInMs; } - - MessageBroker& GetMessageBroker() - { - return that_.GetMessageBroker(); - } }; - NativeStoneApplicationContext(MessageBroker& broker); + NativeStoneApplicationContext(); void Start(); diff -r 1c7ae79c426d -r 68579a31eeb4 Applications/Generic/NativeStoneApplicationRunner.cpp --- a/Applications/Generic/NativeStoneApplicationRunner.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Applications/Generic/NativeStoneApplicationRunner.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -97,7 +97,7 @@ DeclareCommandLineOptions(options); // application specific options - application_.DeclareStartupOptions(options); + application_->DeclareStartupOptions(options); boost::program_options::variables_map parameters; bool error = false; @@ -197,7 +197,7 @@ LogStatusBar statusBar; - NativeStoneApplicationContext context(broker_); + NativeStoneApplicationContext context; { // use multiple threads to execute asynchronous tasks like @@ -206,24 +206,23 @@ oracle.Start(); { - Deprecated::OracleWebService webService( - broker_, oracle, webServiceParameters, context); - + boost::shared_ptr webService + (new Deprecated::OracleWebService(oracle, webServiceParameters, context)); context.SetWebService(webService); context.SetOrthancBaseUrl(webServiceParameters.GetUrl()); - Deprecated::OracleDelayedCallExecutor delayedExecutor(broker_, oracle, context); + Deprecated::OracleDelayedCallExecutor delayedExecutor(oracle, context); context.SetDelayedCallExecutor(delayedExecutor); - application_.Initialize(&context, statusBar, parameters); + application_->Initialize(&context, statusBar, parameters); { NativeStoneApplicationContext::GlobalMutexLocker locker(context); - locker.SetCentralWidget(application_.GetCentralWidget()); + locker.SetCentralWidget(application_->GetCentralWidget()); locker.GetCentralViewport().SetStatusBar(statusBar); } - std::string title = application_.GetTitle(); + std::string title = application_->GetTitle(); if (title.empty()) { title = "Stone of Orthanc"; @@ -244,7 +243,7 @@ } LOG(WARNING) << "The application is stopping"; - application_.Finalize(); + application_->Finalize(); } catch (Orthanc::OrthancException& e) { diff -r 1c7ae79c426d -r 68579a31eeb4 Applications/Generic/NativeStoneApplicationRunner.h --- a/Applications/Generic/NativeStoneApplicationRunner.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Applications/Generic/NativeStoneApplicationRunner.h Tue Feb 04 15:24:03 2020 +0100 @@ -34,14 +34,11 @@ class NativeStoneApplicationRunner { protected: - MessageBroker& broker_; - IStoneApplication& application_; + boost::shared_ptr application_; + public: - - NativeStoneApplicationRunner(MessageBroker& broker, - IStoneApplication& application) - : broker_(broker), - application_(application) + NativeStoneApplicationRunner(boost::shared_ptr application) + : application_(application) { } int Execute(int argc, diff -r 1c7ae79c426d -r 68579a31eeb4 Applications/IStoneApplication.h --- a/Applications/IStoneApplication.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Applications/IStoneApplication.h Tue Feb 04 15:24:03 2020 +0100 @@ -64,7 +64,11 @@ #endif virtual std::string GetTitle() const = 0; - virtual Deprecated::IWidget* GetCentralWidget() = 0; + + virtual void SetCentralWidget(boost::shared_ptr widget) = 0; + + virtual boost::shared_ptr GetCentralWidget() = 0; + virtual void Finalize() = 0; }; } diff -r 1c7ae79c426d -r 68579a31eeb4 Applications/Qt/QCairoWidget.cpp diff -r 1c7ae79c426d -r 68579a31eeb4 Applications/Samples/SampleApplicationBase.h --- a/Applications/Samples/SampleApplicationBase.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Applications/Samples/SampleApplicationBase.h Tue Feb 04 15:24:03 2020 +0100 @@ -40,9 +40,8 @@ { class SampleApplicationBase : public IStoneApplication { - protected: - // ownership is transferred to the application context - Deprecated::WorldSceneWidget* mainWidget_; + private: + boost::shared_ptr mainWidget_; public: virtual void Initialize(StoneApplicationContext* context, @@ -64,7 +63,16 @@ virtual void Finalize() ORTHANC_OVERRIDE {} - virtual Deprecated::IWidget* GetCentralWidget() ORTHANC_OVERRIDE {return mainWidget_;} + + virtual void SetCentralWidget(boost::shared_ptr widget) ORTHANC_OVERRIDE + { + mainWidget_ = widget; + } + + virtual boost::shared_ptr GetCentralWidget() ORTHANC_OVERRIDE + { + return mainWidget_; + } #if ORTHANC_ENABLE_WASM==1 // default implementations for a single canvas named "canvas" in the HTML and an emtpy WasmApplicationAdapter diff -r 1c7ae79c426d -r 68579a31eeb4 Applications/Samples/SampleMainNative.cpp --- a/Applications/Samples/SampleMainNative.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Applications/Samples/SampleMainNative.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -26,19 +26,18 @@ #if ORTHANC_ENABLE_QT==1 #include "Qt/SampleQtApplicationRunner.h" #endif -#include "../../Framework/Messages/MessageBroker.h" int main(int argc, char* argv[]) { - OrthancStone::MessageBroker broker; - SampleApplication sampleStoneApplication(broker); + boost::shared_ptr sampleStoneApplication(new SampleApplication); #if ORTHANC_ENABLE_SDL==1 - OrthancStone::SdlStoneApplicationRunner sdlApplicationRunner(broker, sampleStoneApplication); + OrthancStone::SdlStoneApplicationRunner sdlApplicationRunner(sampleStoneApplication); return sdlApplicationRunner.Execute(argc, argv); #endif + #if ORTHANC_ENABLE_QT==1 - OrthancStone::Samples::SampleQtApplicationRunner qtAppRunner(broker, sampleStoneApplication); + OrthancStone::Samples::SampleQtApplicationRunner qtAppRunner(sampleStoneApplication); return qtAppRunner.Execute(argc, argv); #endif } diff -r 1c7ae79c426d -r 68579a31eeb4 Applications/Samples/SimpleViewerApplicationSingleFile.h --- a/Applications/Samples/SimpleViewerApplicationSingleFile.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Applications/Samples/SimpleViewerApplicationSingleFile.h Tue Feb 04 15:24:03 2020 +0100 @@ -44,7 +44,7 @@ { class SimpleViewerApplication : public SampleSingleCanvasWithButtonsApplicationBase, - public IObserver + public ObserverBase { private: class ThumbnailInteractor : public Deprecated::IWorldSceneInteractor @@ -199,8 +199,8 @@ SimpleViewerApplication& viewerApplication_; public: - SimpleViewerApplicationAdapter(MessageBroker& broker, SimpleViewerApplication& application) - : WasmPlatformApplicationAdapter(broker, application), + SimpleViewerApplicationAdapter(SimpleViewerApplication& application) + : WasmPlatformApplicationAdapter(application), viewerApplication_(application) { } @@ -243,7 +243,7 @@ std::auto_ptr thumbnailInteractor_; Deprecated::LayoutWidget* mainLayout_; Deprecated::LayoutWidget* thumbnailsLayout_; - std::vector thumbnails_; + std::vector > thumbnails_; std::map > instancesIdsPerSeriesId_; std::map seriesTags_; @@ -258,8 +258,7 @@ Orthanc::Font font_; public: - SimpleViewerApplication(MessageBroker& broker) : - IObserver(broker), + SimpleViewerApplication() : currentTool_(Tool_LineMeasure), mainLayout_(NULL), currentInstanceIndex_(0), @@ -297,26 +296,28 @@ mainLayout_->SetBackgroundColor(0, 0, 0); mainLayout_->SetHorizontal(); - thumbnailsLayout_ = new Deprecated::LayoutWidget("thumbnail-layout"); + boost::shared_ptr thumbnailsLayout_(new Deprecated::LayoutWidget("thumbnail-layout")); thumbnailsLayout_->SetPadding(10); thumbnailsLayout_->SetBackgroundCleared(true); thumbnailsLayout_->SetBackgroundColor(50, 50, 50); thumbnailsLayout_->SetVertical(); - mainWidget_ = new Deprecated::SliceViewerWidget(GetBroker(), "main-viewport"); + boost::shared_ptr widget + (new Deprecated::SliceViewerWidget("main-viewport")); + SetCentralWidget(widget); //mainWidget_->RegisterObserver(*this); // hierarchy mainLayout_->AddWidget(thumbnailsLayout_); - mainLayout_->AddWidget(mainWidget_); + mainLayout_->AddWidget(widget); // sources - smartLoader_.reset(new Deprecated::SmartLoader(GetBroker(), context->GetOrthancApiClient())); + smartLoader_.reset(new Deprecated::SmartLoader(context->GetOrthancApiClient())); smartLoader_->SetImageQuality(Deprecated::SliceImageQuality_FullPam); mainLayout_->SetTransmitMouseOver(true); mainWidgetInteractor_.reset(new MainWidgetInteractor(*this)); - mainWidget_->SetInteractor(*mainWidgetInteractor_); + widget->SetInteractor(*mainWidgetInteractor_); thumbnailInteractor_.reset(new ThumbnailInteractor(*this)); } @@ -327,10 +328,10 @@ if (parameters.count("studyId") < 1) { LOG(WARNING) << "The study ID is missing, will take the first studyId found in Orthanc"; - context->GetOrthancApiClient().GetJsonAsync( + context->GetOrthancApiClient()->GetJsonAsync( "/studies", - new Callable - (*this, &SimpleViewerApplication::OnStudyListReceived)); + new Deprecated::DeprecatedCallable + (GetSharedObserver(), &SimpleViewerApplication::OnStudyListReceived)); } else { @@ -357,10 +358,10 @@ { for (size_t i=0; i < response["Series"].size(); i++) { - context_->GetOrthancApiClient().GetJsonAsync( + context_->GetOrthancApiClient()->GetJsonAsync( "/series/" + response["Series"][(int)i].asString(), - new Callable - (*this, &SimpleViewerApplication::OnSeriesReceived)); + new Deprecated::DeprecatedCallable + (GetSharedObserver(), &SimpleViewerApplication::OnSeriesReceived)); } } } @@ -387,7 +388,7 @@ LoadThumbnailForSeries(seriesId, instancesIdsPerSeriesId_[seriesId][0]); // if this is the first thumbnail loaded, load the first instance in the mainWidget - Deprecated::SliceViewerWidget& widget = *dynamic_cast(mainWidget_); + Deprecated::SliceViewerWidget& widget = dynamic_cast(*GetCentralWidget()); if (widget.GetLayerCount() == 0) { smartLoader_->SetFrameInWidget(widget, 0, instancesIdsPerSeriesId_[seriesId][0], 0); @@ -398,10 +399,10 @@ void LoadThumbnailForSeries(const std::string& seriesId, const std::string& instanceId) { LOG(INFO) << "Loading thumbnail for series " << seriesId; - Deprecated::SliceViewerWidget* thumbnailWidget = new Deprecated::SliceViewerWidget(GetBroker(), "thumbnail-series-" + seriesId); + boost::shared_ptr thumbnailWidget(new Deprecated::SliceViewerWidget("thumbnail-series-" + seriesId)); thumbnails_.push_back(thumbnailWidget); thumbnailsLayout_->AddWidget(thumbnailWidget); - thumbnailWidget->RegisterObserverCallback(new Callable(*this, &SimpleViewerApplication::OnWidgetGeometryChanged)); + Register(*thumbnailWidget, &SimpleViewerApplication::OnWidgetGeometryChanged); smartLoader_->SetFrameInWidget(*thumbnailWidget, 0, instanceId, 0); thumbnailWidget->SetInteractor(*thumbnailInteractor_); } @@ -409,9 +410,9 @@ void SelectStudy(const std::string& studyId) { LOG(INFO) << "Selecting study: " << studyId; - context_->GetOrthancApiClient().GetJsonAsync( - "/studies/" + studyId, new Callable - (*this, &SimpleViewerApplication::OnStudyReceived)); + context_->GetOrthancApiClient()->GetJsonAsync( + "/studies/" + studyId, new Deprecated::DeprecatedCallable + (GetSharedObserver(), &SimpleViewerApplication::OnStudyReceived)); } void OnWidgetGeometryChanged(const Deprecated::SliceViewerWidget::GeometryChangedMessage& message) @@ -422,7 +423,7 @@ void SelectSeriesInMainViewport(const std::string& seriesId) { - Deprecated::SliceViewerWidget& widget = *dynamic_cast(mainWidget_); + Deprecated::SliceViewerWidget& widget = dynamic_cast(*GetCentralWidget()); smartLoader_->SetFrameInWidget(widget, 0, instancesIdsPerSeriesId_[seriesId][0], 0); } @@ -451,7 +452,7 @@ virtual void InitializeWasm() { AttachWidgetToWasmViewport("canvas", thumbnailsLayout_); - AttachWidgetToWasmViewport("canvas2", mainWidget_); + AttachWidgetToWasmViewport("canvas2", widget); } #endif diff -r 1c7ae79c426d -r 68579a31eeb4 Applications/Samples/SingleFrameApplication.h --- a/Applications/Samples/SingleFrameApplication.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Applications/Samples/SingleFrameApplication.h Tue Feb 04 15:24:03 2020 +0100 @@ -38,7 +38,7 @@ { class SingleFrameApplication : public SampleSingleCanvasApplicationBase, - public IObserver + public ObserverBase { private: class Interactor : public Deprecated::IWorldSceneInteractor @@ -127,7 +127,7 @@ void OffsetSlice(int offset) { - if (source_ != NULL) + if (source_) { int slice = static_cast(slice_) + offset; @@ -149,21 +149,15 @@ } - Deprecated::SliceViewerWidget& GetMainWidget() - { - return *dynamic_cast(mainWidget_); - } - - void SetSlice(size_t index) { - if (source_ != NULL && + if (source_ && index < source_->GetSlicesCount()) { slice_ = static_cast(index); #if 1 - GetMainWidget().SetSlice(source_->GetSlice(slice_).GetGeometry()); + widget_->SetSlice(source_->GetSlice(slice_).GetGeometry()); #else // TEST for scene extents - Rotate the axes double a = 15.0 / 180.0 * boost::math::constants::pi(); @@ -189,22 +183,22 @@ // Once the geometry of the series is downloaded from Orthanc, // display its middle slice, and adapt the viewport to fit this // slice - if (source_ == &message.GetOrigin()) + if (source_ && + source_.get() == &message.GetOrigin()) { SetSlice(source_->GetSlicesCount() / 2); } - GetMainWidget().FitContent(); + widget_->FitContent(); } - + + boost::shared_ptr widget_; std::auto_ptr mainWidgetInteractor_; - const Deprecated::DicomSeriesVolumeSlicer* source_; + boost::shared_ptr source_; unsigned int slice_; public: - SingleFrameApplication(MessageBroker& broker) : - IObserver(broker), - source_(NULL), + SingleFrameApplication() : slice_(0) { } @@ -243,13 +237,15 @@ std::string instance = parameters["instance"].as(); int frame = parameters["frame"].as(); - mainWidget_ = new Deprecated::SliceViewerWidget(GetBroker(), "main-widget"); + widget_.reset(new Deprecated::SliceViewerWidget("main-widget")); + SetCentralWidget(widget_); - std::auto_ptr layer(new Deprecated::DicomSeriesVolumeSlicer(GetBroker(), context->GetOrthancApiClient())); - source_ = layer.get(); + boost::shared_ptr layer(new Deprecated::DicomSeriesVolumeSlicer); + layer->Connect(context->GetOrthancApiClient()); + source_ = layer; layer->LoadFrame(instance, frame); - layer->RegisterObserverCallback(new Callable(*this, &SingleFrameApplication::OnMainWidgetGeometryReady)); - GetMainWidget().AddLayer(layer.release()); + Register(*layer, &SingleFrameApplication::OnMainWidgetGeometryReady); + widget_->AddLayer(layer); Deprecated::RenderStyle s; @@ -258,11 +254,11 @@ s.interpolation_ = ImageInterpolation_Bilinear; } - GetMainWidget().SetLayerStyle(0, s); - GetMainWidget().SetTransmitMouseOver(true); + widget_->SetLayerStyle(0, s); + widget_->SetTransmitMouseOver(true); mainWidgetInteractor_.reset(new Interactor(*this)); - GetMainWidget().SetInteractor(*mainWidgetInteractor_); + widget_->SetInteractor(*mainWidgetInteractor_); } }; diff -r 1c7ae79c426d -r 68579a31eeb4 Applications/Samples/SingleFrameEditorApplication.h --- a/Applications/Samples/SingleFrameEditorApplication.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Applications/Samples/SingleFrameEditorApplication.h Tue Feb 04 15:24:03 2020 +0100 @@ -28,13 +28,13 @@ #include "../../Framework/Radiography/RadiographyLayerMoveTracker.h" #include "../../Framework/Radiography/RadiographyLayerResizeTracker.h" #include "../../Framework/Radiography/RadiographyLayerRotateTracker.h" +#include "../../Framework/Radiography/RadiographyMaskLayer.h" #include "../../Framework/Radiography/RadiographyScene.h" #include "../../Framework/Radiography/RadiographySceneCommand.h" +#include "../../Framework/Radiography/RadiographySceneReader.h" +#include "../../Framework/Radiography/RadiographySceneWriter.h" #include "../../Framework/Radiography/RadiographyWidget.h" #include "../../Framework/Radiography/RadiographyWindowingTracker.h" -#include "../../Framework/Radiography/RadiographySceneWriter.h" -#include "../../Framework/Radiography/RadiographySceneReader.h" -#include "../../Framework/Radiography/RadiographyMaskLayer.h" #include "../../Framework/Toolbox/TextRenderer.h" #include @@ -55,7 +55,7 @@ { class RadiographyEditorInteractor : public Deprecated::IWorldSceneInteractor, - public IObserver + public ObserverBase { private: enum Tool @@ -82,8 +82,7 @@ public: - RadiographyEditorInteractor(MessageBroker& broker) : - IObserver(broker), + RadiographyEditorInteractor() : context_(NULL), tool_(Tool_Move), maskLayer_(NULL) @@ -315,8 +314,8 @@ LOG(INFO) << "JSON export was successful: " << snapshot.toStyledString(); - boost::shared_ptr scene(new RadiographyScene(GetBroker())); - RadiographySceneReader reader(*scene, context_->GetOrthancApiClient()); + boost::shared_ptr scene(new RadiographyScene); + RadiographySceneReader reader(*scene, *context_->GetOrthancApiClient()); reader.Read(snapshot); widget.SetScene(scene); @@ -346,7 +345,7 @@ if (context_ != NULL) { - widget.GetScene().ExportDicom(context_->GetOrthancApiClient(), + widget.GetScene().ExportDicom(*context_->GetOrthancApiClient(), tags, std::string(), 0.1, 0.1, widget.IsInverted(), widget.GetInterpolation(), EXPORT_USING_PAM); } @@ -426,16 +425,9 @@ private: boost::shared_ptr scene_; RadiographyEditorInteractor interactor_; - Orthanc::FontRegistry fontRegistry_; RadiographyMaskLayer* maskLayer_; public: - SingleFrameEditorApplication(MessageBroker& broker) : - IObserver(broker), - interactor_(broker) - { - } - virtual ~SingleFrameEditorApplication() { LOG(WARNING) << "Destroying the application"; @@ -487,9 +479,9 @@ std::string instance = parameters["instance"].as(); //int frame = parameters["frame"].as(); - scene_.reset(new RadiographyScene(GetBroker())); + scene_.reset(new RadiographyScene); - RadiographyLayer& dicomLayer = scene_->LoadDicomFrame(context->GetOrthancApiClient(), instance, 0, false, NULL); + RadiographyLayer& dicomLayer = scene_->LoadDicomFrame(*context->GetOrthancApiClient(), instance, 0, false, NULL); //scene_->LoadDicomFrame(instance, frame, false); //.SetPan(200, 0); // = scene_->LoadDicomFrame(context->GetOrthancApiClient(), "61f3143e-96f34791-ad6bbb8d-62559e75-45943e1b", 0, false, NULL); @@ -527,10 +519,10 @@ layer.SetPan(0, 200); } - - mainWidget_ = new RadiographyWidget(GetBroker(), scene_, "main-widget"); - mainWidget_->SetTransmitMouseOver(true); - mainWidget_->SetInteractor(interactor_); + boost::shared_ptr widget(new RadiographyWidget(scene_, "main-widget")); + widget->SetTransmitMouseOver(true); + widget->SetInteractor(interactor_); + SetCentralWidget(widget); //scene_->SetWindowing(128, 256); } diff -r 1c7ae79c426d -r 68579a31eeb4 Applications/Sdl/SdlCairoSurface.h --- a/Applications/Sdl/SdlCairoSurface.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Applications/Sdl/SdlCairoSurface.h Tue Feb 04 15:24:03 2020 +0100 @@ -27,6 +27,8 @@ #include "../../Framework/Wrappers/CairoSurface.h" #include "../../Framework/Deprecated/Viewport/IViewport.h" +#include + #include namespace OrthancStone diff -r 1c7ae79c426d -r 68579a31eeb4 Applications/Sdl/SdlEngine.cpp --- a/Applications/Sdl/SdlEngine.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Applications/Sdl/SdlEngine.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -99,9 +99,7 @@ SdlEngine::SdlEngine(SdlWindow& window, - NativeStoneApplicationContext& context, - MessageBroker& broker) : - IObserver(broker), + NativeStoneApplicationContext& context) : window_(window), context_(context), surface_(window), diff -r 1c7ae79c426d -r 68579a31eeb4 Applications/Sdl/SdlEngine.h --- a/Applications/Sdl/SdlEngine.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Applications/Sdl/SdlEngine.h Tue Feb 04 15:24:03 2020 +0100 @@ -23,12 +23,13 @@ #if ORTHANC_ENABLE_SDL == 1 +#include "../../Framework/Messages/ObserverBase.h" +#include "../Generic/NativeStoneApplicationContext.h" #include "SdlCairoSurface.h" -#include "../Generic/NativeStoneApplicationContext.h" namespace OrthancStone { - class SdlEngine : public IObserver + class SdlEngine : public ObserverBase { private: SdlWindow& window_; @@ -46,8 +47,7 @@ public: SdlEngine(SdlWindow& window, - NativeStoneApplicationContext& context, - MessageBroker& broker); + NativeStoneApplicationContext& context); void OnViewportChanged(const Deprecated::IViewport::ViewportChangedMessage& message) { diff -r 1c7ae79c426d -r 68579a31eeb4 Applications/Sdl/SdlOrthancSurface.cpp --- a/Applications/Sdl/SdlOrthancSurface.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Applications/Sdl/SdlOrthancSurface.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -27,6 +27,8 @@ #include #include +#include + namespace OrthancStone { SdlOrthancSurface::SdlOrthancSurface(SdlWindow& window) : diff -r 1c7ae79c426d -r 68579a31eeb4 Applications/Sdl/SdlStoneApplicationRunner.cpp --- a/Applications/Sdl/SdlStoneApplicationRunner.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Applications/Sdl/SdlStoneApplicationRunner.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -103,20 +103,19 @@ LOG(WARNING) << "Starting the application"; SdlWindow window(title.c_str(), width_, height_, enableOpenGl_); - SdlEngine sdl(window, context, broker_); + boost::shared_ptr sdl(new SdlEngine(window, context)); { NativeStoneApplicationContext::GlobalMutexLocker locker(context); - locker.GetCentralViewport().RegisterObserverCallback( - new Callable - (sdl, &SdlEngine::OnViewportChanged)); + sdl->Register + (locker.GetCentralViewport(), &SdlEngine::OnViewportChanged); //context.GetCentralViewport().Register(sdl); // (*) } context.Start(); - sdl.Run(); + sdl->Run(); LOG(WARNING) << "Stopping the application"; diff -r 1c7ae79c426d -r 68579a31eeb4 Applications/Sdl/SdlStoneApplicationRunner.h --- a/Applications/Sdl/SdlStoneApplicationRunner.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Applications/Sdl/SdlStoneApplicationRunner.h Tue Feb 04 15:24:03 2020 +0100 @@ -39,9 +39,8 @@ bool enableOpenGl_; public: - SdlStoneApplicationRunner(MessageBroker& broker, - IStoneApplication& application) : - NativeStoneApplicationRunner(broker, application) + SdlStoneApplicationRunner(boost::shared_ptr application) : + NativeStoneApplicationRunner(application) { } diff -r 1c7ae79c426d -r 68579a31eeb4 Applications/StoneApplicationContext.cpp --- a/Applications/StoneApplicationContext.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Applications/StoneApplicationContext.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -32,35 +32,35 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } - orthanc_.reset(new Deprecated::OrthancApiClient(broker_, *webService_, orthancBaseUrl_)); + orthanc_.reset(new Deprecated::OrthancApiClient(*webService_, orthancBaseUrl_)); } - Deprecated::IWebService& StoneApplicationContext::GetWebService() + boost::shared_ptr StoneApplicationContext::GetWebService() { if (webService_ == NULL) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } - return *webService_; + return webService_; } - Deprecated::OrthancApiClient& StoneApplicationContext::GetOrthancApiClient() + boost::shared_ptr StoneApplicationContext::GetOrthancApiClient() { if (orthanc_.get() == NULL) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } - return *orthanc_; + return orthanc_; } - void StoneApplicationContext::SetWebService(Deprecated::IWebService& webService) + void StoneApplicationContext::SetWebService(boost::shared_ptr webService) { - webService_ = &webService; + webService_ = webService; InitializeOrthanc(); } diff -r 1c7ae79c426d -r 68579a31eeb4 Applications/StoneApplicationContext.h --- a/Applications/StoneApplicationContext.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Applications/StoneApplicationContext.h Tue Feb 04 15:24:03 2020 +0100 @@ -59,18 +59,15 @@ class StoneApplicationContext : public boost::noncopyable { private: - MessageBroker& broker_; - Deprecated::IWebService* webService_; - Deprecated::IDelayedCallExecutor* delayedCallExecutor_; - std::auto_ptr orthanc_; + boost::shared_ptr webService_; + Deprecated::IDelayedCallExecutor* delayedCallExecutor_; // TODO => shared_ptr ?? + boost::shared_ptr orthanc_; std::string orthancBaseUrl_; void InitializeOrthanc(); public: - StoneApplicationContext(MessageBroker& broker) : - broker_(broker), - webService_(NULL), + StoneApplicationContext() : delayedCallExecutor_(NULL) { } @@ -79,21 +76,11 @@ { } - MessageBroker& GetMessageBroker() - { - return broker_; - } + boost::shared_ptr GetWebService(); - bool HasWebService() const - { - return webService_ != NULL; - } + boost::shared_ptr GetOrthancApiClient(); - Deprecated::IWebService& GetWebService(); - - Deprecated::OrthancApiClient& GetOrthancApiClient(); - - void SetWebService(Deprecated::IWebService& webService); + void SetWebService(boost::shared_ptr webService); void SetOrthancBaseUrl(const std::string& baseUrl); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Layers/DicomSeriesVolumeSlicer.cpp --- a/Framework/Deprecated/Layers/DicomSeriesVolumeSlicer.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Deprecated/Layers/DicomSeriesVolumeSlicer.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -87,59 +87,73 @@ } - DicomSeriesVolumeSlicer::DicomSeriesVolumeSlicer(OrthancStone::MessageBroker& broker, - OrthancApiClient& orthanc) : - IVolumeSlicer(broker), - IObserver(broker), - loader_(broker, orthanc), + DicomSeriesVolumeSlicer::DicomSeriesVolumeSlicer() : quality_(SliceImageQuality_FullPng) { - loader_.RegisterObserverCallback( - new OrthancStone::Callable - (*this, &DicomSeriesVolumeSlicer::OnSliceGeometryReady)); - - loader_.RegisterObserverCallback( - new OrthancStone::Callable - (*this, &DicomSeriesVolumeSlicer::OnSliceGeometryError)); + } - loader_.RegisterObserverCallback( - new OrthancStone::Callable - (*this, &DicomSeriesVolumeSlicer::OnSliceImageReady)); - - loader_.RegisterObserverCallback( - new OrthancStone::Callable - (*this, &DicomSeriesVolumeSlicer::OnSliceImageError)); + void DicomSeriesVolumeSlicer::Connect(boost::shared_ptr orthanc) + { + loader_.reset(new OrthancSlicesLoader(orthanc)); + Register(*loader_, &DicomSeriesVolumeSlicer::OnSliceGeometryReady); + Register(*loader_, &DicomSeriesVolumeSlicer::OnSliceGeometryError); + Register(*loader_, &DicomSeriesVolumeSlicer::OnSliceImageReady); + Register(*loader_, &DicomSeriesVolumeSlicer::OnSliceImageError); } void DicomSeriesVolumeSlicer::LoadSeries(const std::string& seriesId) { - loader_.ScheduleLoadSeries(seriesId); + if (loader_.get() == NULL) + { + // Should have called "Connect()" + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + loader_->ScheduleLoadSeries(seriesId); } void DicomSeriesVolumeSlicer::LoadInstance(const std::string& instanceId) { - loader_.ScheduleLoadInstance(instanceId); + if (loader_.get() == NULL) + { + // Should have called "Connect()" + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + loader_->ScheduleLoadInstance(instanceId); } void DicomSeriesVolumeSlicer::LoadFrame(const std::string& instanceId, unsigned int frame) { - loader_.ScheduleLoadFrame(instanceId, frame); + if (loader_.get() == NULL) + { + // Should have called "Connect()" + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + loader_->ScheduleLoadFrame(instanceId, frame); } bool DicomSeriesVolumeSlicer::GetExtent(std::vector& points, const OrthancStone::CoordinateSystem3D& viewportSlice) { + if (loader_.get() == NULL) + { + // Should have called "Connect()" + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + size_t index; - if (loader_.IsGeometryReady() && - loader_.LookupSlice(index, viewportSlice)) + if (loader_->IsGeometryReady() && + loader_->LookupSlice(index, viewportSlice)) { - loader_.GetSlice(index).GetExtent(points); + loader_->GetSlice(index).GetExtent(points); return true; } else @@ -151,12 +165,18 @@ void DicomSeriesVolumeSlicer::ScheduleLayerCreation(const OrthancStone::CoordinateSystem3D& viewportSlice) { + if (loader_.get() == NULL) + { + // Should have called "Connect()" + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + size_t index; - if (loader_.IsGeometryReady() && - loader_.LookupSlice(index, viewportSlice)) + if (loader_->IsGeometryReady() && + loader_->LookupSlice(index, viewportSlice)) { - loader_.ScheduleLoadSliceImage(index, quality_); + loader_->ScheduleLoadSliceImage(index, quality_); } } } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Layers/DicomSeriesVolumeSlicer.h --- a/Framework/Deprecated/Layers/DicomSeriesVolumeSlicer.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Deprecated/Layers/DicomSeriesVolumeSlicer.h Tue Feb 04 15:24:03 2020 +0100 @@ -22,6 +22,7 @@ #pragma once #include "IVolumeSlicer.h" +#include "../../Messages/ObserverBase.h" #include "../Toolbox/IWebService.h" #include "../Toolbox/OrthancSlicesLoader.h" #include "../Toolbox/OrthancApiClient.h" @@ -33,7 +34,7 @@ // messages are sent to observers so they can use it class DicomSeriesVolumeSlicer : public IVolumeSlicer, - public OrthancStone::IObserver + public OrthancStone::ObserverBase //private OrthancSlicesLoader::ISliceLoaderObserver { public: @@ -79,13 +80,14 @@ private: class RendererFactory; - OrthancSlicesLoader loader_; + boost::shared_ptr loader_; SliceImageQuality quality_; public: - DicomSeriesVolumeSlicer(OrthancStone::MessageBroker& broker, - OrthancApiClient& orthanc); + DicomSeriesVolumeSlicer(); + void Connect(boost::shared_ptr orthanc); + void LoadSeries(const std::string& seriesId); void LoadInstance(const std::string& instanceId); @@ -105,12 +107,12 @@ size_t GetSlicesCount() const { - return loader_.GetSlicesCount(); + return loader_->GetSlicesCount(); } const Slice& GetSlice(size_t slice) const { - return loader_.GetSlice(slice); + return loader_->GetSlice(slice); } virtual bool GetExtent(std::vector& points, diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Layers/DicomStructureSetSlicer.cpp --- a/Framework/Deprecated/Layers/DicomStructureSetSlicer.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Deprecated/Layers/DicomStructureSetSlicer.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -20,6 +20,8 @@ #include "DicomStructureSetSlicer.h" +#include "../../Toolbox/DicomStructureSet.h" + namespace Deprecated { class DicomStructureSetSlicer::Renderer : public ILayerRenderer @@ -28,13 +30,18 @@ class Structure { private: - bool visible_; - uint8_t red_; - uint8_t green_; - uint8_t blue_; - std::string name_; + bool visible_; + uint8_t red_; + uint8_t green_; + uint8_t blue_; + std::string name_; + +#if USE_BOOST_UNION_FOR_POLYGONS == 1 + std::vector< std::vector > polygons_; +#else std::vector< std::pair > segments_; - +#endif + public: Structure(OrthancStone::DicomStructureSet& structureSet, const OrthancStone::CoordinateSystem3D& plane, @@ -42,7 +49,12 @@ name_(structureSet.GetStructureName(index)) { structureSet.GetStructureColor(red_, green_, blue_, index); + +#if USE_BOOST_UNION_FOR_POLYGONS == 1 + visible_ = structureSet.ProjectStructure(polygons_, index, plane); +#else visible_ = structureSet.ProjectStructure(segments_, index, plane); +#endif } void Render(OrthancStone::CairoContext& context) @@ -53,12 +65,25 @@ context.SetSourceColor(red_, green_, blue_); +#if USE_BOOST_UNION_FOR_POLYGONS == 1 + for (size_t i = 0; i < polygons_.size(); i++) + { + cairo_move_to(cr, polygons_[i][0].x, polygons_[i][0].y); + for (size_t j = 0; j < polygons_[i].size(); j++) + { + cairo_line_to(cr, polygons_[i][j].x, polygons_[i][j].y); + } + cairo_line_to(cr, polygons_[i][0].x, polygons_[i][0].y); + cairo_stroke(cr); + } +#else for (size_t i = 0; i < segments_.size(); i++) { cairo_move_to(cr, segments_[i].first.x, segments_[i].first.y); - cairo_move_to(cr, segments_[i].second.x, segments_[i].second.y); + cairo_line_to(cr, segments_[i].second.x, segments_[i].second.y); cairo_stroke(cr); } +#endif } } }; @@ -140,15 +165,10 @@ }; - DicomStructureSetSlicer::DicomStructureSetSlicer(OrthancStone::MessageBroker& broker, - StructureSetLoader& loader) : - IVolumeSlicer(broker), - IObserver(broker), + DicomStructureSetSlicer::DicomStructureSetSlicer(StructureSetLoader& loader) : loader_(loader) { - loader_.RegisterObserverCallback( - new OrthancStone::Callable - (*this, &DicomStructureSetSlicer::OnStructureSetLoaded)); + Register(loader_, &DicomStructureSetSlicer::OnStructureSetLoaded); } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Layers/DicomStructureSetSlicer.h --- a/Framework/Deprecated/Layers/DicomStructureSetSlicer.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Deprecated/Layers/DicomStructureSetSlicer.h Tue Feb 04 15:24:03 2020 +0100 @@ -28,7 +28,7 @@ { class DicomStructureSetSlicer : public IVolumeSlicer, - public OrthancStone::IObserver + public OrthancStone::ObserverBase { private: class Renderer; @@ -42,8 +42,7 @@ } public: - DicomStructureSetSlicer(OrthancStone::MessageBroker& broker, - StructureSetLoader& loader); + DicomStructureSetSlicer(StructureSetLoader& loader); virtual bool GetExtent(std::vector& points, const OrthancStone::CoordinateSystem3D& viewportPlane) diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Layers/IVolumeSlicer.h --- a/Framework/Deprecated/Layers/IVolumeSlicer.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Deprecated/Layers/IVolumeSlicer.h Tue Feb 04 15:24:03 2020 +0100 @@ -122,11 +122,6 @@ }; - IVolumeSlicer(OrthancStone::MessageBroker& broker) : - IObservable(broker) - { - } - virtual ~IVolumeSlicer() { } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Loaders/DicomStructureSetLoader.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Deprecated/Loaders/DicomStructureSetLoader.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,417 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "DicomStructureSetLoader.h" + +#include "../../Scene2D/PolylineSceneLayer.h" +#include "../../StoneException.h" +#include "../../Toolbox/GeometryToolbox.h" + +#include + +#include + +#if 0 +bool logbgo233 = false; +bool logbgo115 = false; +#endif + +namespace Deprecated +{ + +#if 0 + void DumpDicomMap(std::ostream& o, const Orthanc::DicomMap& dicomMap) + { + using namespace std; + //ios_base::fmtflags state = o.flags(); + //o.flags(ios::right | ios::hex); + //o << "(" << setfill('0') << setw(4) << tag.GetGroup() + // << "," << setw(4) << tag.GetElement() << ")"; + //o.flags(state); + Json::Value val; + dicomMap.Serialize(val); + o << val; + //return o; + } +#endif + + + class DicomStructureSetLoader::AddReferencedInstance : public LoaderStateMachine::State + { + private: + std::string instanceId_; + + public: + AddReferencedInstance(DicomStructureSetLoader& that, + const std::string& instanceId) : + State(that), + instanceId_(instanceId) + { + } + + virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) + { + Json::Value tags; + message.ParseJsonBody(tags); + + Orthanc::DicomMap dicom; + dicom.FromDicomAsJson(tags); + + DicomStructureSetLoader& loader = GetLoader(); + + loader.content_->AddReferencedSlice(dicom); + + loader.countProcessedInstances_ ++; + assert(loader.countProcessedInstances_ <= loader.countReferencedInstances_); + + if (loader.countProcessedInstances_ == loader.countReferencedInstances_) + { + // All the referenced instances have been loaded, finalize the RT-STRUCT + loader.content_->CheckReferencedSlices(); + loader.revision_++; + loader.SetStructuresReady(); + } + } + }; + + + // State that converts a "SOP Instance UID" to an Orthanc identifier + class DicomStructureSetLoader::LookupInstance : public LoaderStateMachine::State + { + private: + std::string sopInstanceUid_; + + public: + LookupInstance(DicomStructureSetLoader& that, + const std::string& sopInstanceUid) : + State(that), + sopInstanceUid_(sopInstanceUid) + { + } + + virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) + { +#if 0 + LOG(TRACE) << "DicomStructureSetLoader::LookupInstance::Handle() (SUCCESS)"; +#endif + DicomStructureSetLoader& loader = GetLoader(); + + Json::Value lookup; + message.ParseJsonBody(lookup); + + if (lookup.type() != Json::arrayValue || + lookup.size() != 1 || + !lookup[0].isMember("Type") || + !lookup[0].isMember("Path") || + lookup[0]["Type"].type() != Json::stringValue || + lookup[0]["ID"].type() != Json::stringValue || + lookup[0]["Type"].asString() != "Instance") + { + std::stringstream msg; + msg << "Unknown resource! message.GetAnswer() = " << message.GetAnswer() << " message.GetAnswerHeaders() = "; + for (OrthancStone::OrthancRestApiCommand::HttpHeaders::const_iterator it = message.GetAnswerHeaders().begin(); + it != message.GetAnswerHeaders().end(); ++it) + { + msg << "\nkey: \"" << it->first << "\" value: \"" << it->second << "\"\n"; + } + const std::string msgStr = msg.str(); + LOG(ERROR) << msgStr; + throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource); + } + + const std::string instanceId = lookup[0]["ID"].asString(); + + { + std::auto_ptr command(new OrthancStone::OrthancRestApiCommand); + command->SetHttpHeader("Accept-Encoding", "gzip"); + std::string uri = "/instances/" + instanceId + "/tags"; + command->SetUri(uri); + command->AcquirePayload(new AddReferencedInstance(loader, instanceId)); + Schedule(command.release()); + } + } + }; + + + class DicomStructureSetLoader::LoadStructure : public LoaderStateMachine::State + { + public: + LoadStructure(DicomStructureSetLoader& that) : + State(that) + { + } + + virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) + { +#if 0 + if (logbgo115) + LOG(TRACE) << "DicomStructureSetLoader::LoadStructure::Handle() (SUCCESS)"; +#endif + DicomStructureSetLoader& loader = GetLoader(); + + { + OrthancPlugins::FullOrthancDataset dicom(message.GetAnswer()); + loader.content_.reset(new OrthancStone::DicomStructureSet(dicom)); + size_t structureCount = loader.content_->GetStructuresCount(); + loader.structureVisibility_.resize(structureCount); + bool everythingVisible = false; + if ((loader.initiallyVisibleStructures_.size() == 1) + && (loader.initiallyVisibleStructures_[0].size() == 1) + && (loader.initiallyVisibleStructures_[0][0] == '*')) + { + everythingVisible = true; + } + + for (size_t i = 0; i < structureCount; ++i) + { + // if a single "*" string is supplied, this means we want everything + // to be visible... + if(everythingVisible) + { + loader.structureVisibility_.at(i) = true; + } + else + { + // otherwise, we only enable visibility for those structures whose + // names are mentioned in the initiallyVisibleStructures_ array + const std::string& structureName = loader.content_->GetStructureName(i); + + std::vector::iterator foundIt = + std::find( + loader.initiallyVisibleStructures_.begin(), + loader.initiallyVisibleStructures_.end(), + structureName); + std::vector::iterator endIt = loader.initiallyVisibleStructures_.end(); + if (foundIt != endIt) + loader.structureVisibility_.at(i) = true; + else + loader.structureVisibility_.at(i) = false; + } + } + } + + // Some (admittedly invalid) Dicom files have empty values in the + // 0008,1155 tag. We try our best to cope with this. + std::set instances; + std::set nonEmptyInstances; + loader.content_->GetReferencedInstances(instances); + for (std::set::const_iterator + it = instances.begin(); it != instances.end(); ++it) + { + std::string instance = Orthanc::Toolbox::StripSpaces(*it); + if(instance != "") + nonEmptyInstances.insert(instance); + } + + loader.countReferencedInstances_ = + static_cast(nonEmptyInstances.size()); + + for (std::set::const_iterator + it = nonEmptyInstances.begin(); it != nonEmptyInstances.end(); ++it) + { + std::auto_ptr command(new OrthancStone::OrthancRestApiCommand); + command->SetUri("/tools/lookup"); + command->SetMethod(Orthanc::HttpMethod_Post); + command->SetBody(*it); + command->AcquirePayload(new LookupInstance(loader, *it)); + Schedule(command.release()); + } + } + }; + + + class DicomStructureSetLoader::Slice : public IExtractedSlice + { + private: + const OrthancStone::DicomStructureSet& content_; + uint64_t revision_; + bool isValid_; + std::vector visibility_; + + public: + /** + The visibility vector must either: + - be empty + or + - contain the same number of items as the number of structures in the + structure set. + In the first case (empty vector), all the structures are displayed. + In the second case, the visibility of each structure is defined by the + content of the vector at the corresponding index. + */ + Slice(const OrthancStone::DicomStructureSet& content, + uint64_t revision, + const OrthancStone::CoordinateSystem3D& cuttingPlane, + std::vector visibility = std::vector()) + : content_(content) + , revision_(revision) + , visibility_(visibility) + { + ORTHANC_ASSERT((visibility_.size() == content_.GetStructuresCount()) + || (visibility_.size() == 0u)); + + bool opposite; + + const OrthancStone::Vector normal = content.GetNormal(); + isValid_ = ( + OrthancStone::GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetNormal()) || + OrthancStone::GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisX()) || + OrthancStone::GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisY())); + } + + virtual bool IsValid() + { + return isValid_; + } + + virtual uint64_t GetRevision() + { + return revision_; + } + + virtual OrthancStone::ISceneLayer* CreateSceneLayer( + const OrthancStone::ILayerStyleConfigurator* configurator, + const OrthancStone::CoordinateSystem3D& cuttingPlane) + { + assert(isValid_); + + std::auto_ptr layer(new OrthancStone::PolylineSceneLayer); + layer->SetThickness(2); + + for (size_t i = 0; i < content_.GetStructuresCount(); i++) + { + if ((visibility_.size() == 0) || visibility_.at(i)) + { + const OrthancStone::Color& color = content_.GetStructureColor(i); + +#ifdef USE_BOOST_UNION_FOR_POLYGONS + std::vector< std::vector > polygons; + + if (content_.ProjectStructure(polygons, i, cuttingPlane)) + { + for (size_t j = 0; j < polygons.size(); j++) + { + PolylineSceneLayer::Chain chain; + chain.resize(polygons[j].size()); + + for (size_t k = 0; k < polygons[j].size(); k++) + { + chain[k] = ScenePoint2D(polygons[j][k].x, polygons[j][k].y); + } + + layer->AddChain(chain, true /* closed */, color); + } + } +#else + std::vector< std::pair > segments; + + if (content_.ProjectStructure(segments, i, cuttingPlane)) + { + for (size_t j = 0; j < segments.size(); j++) + { + OrthancStone::PolylineSceneLayer::Chain chain; + chain.resize(2); + + chain[0] = OrthancStone::ScenePoint2D(segments[j].first.x, segments[j].first.y); + chain[1] = OrthancStone::ScenePoint2D(segments[j].second.x, segments[j].second.y); + + layer->AddChain(chain, false /* NOT closed */, color); + } + } +#endif + } + } + + return layer.release(); + } + }; + + + DicomStructureSetLoader::DicomStructureSetLoader(OrthancStone::IOracle& oracle, + OrthancStone::IObservable& oracleObservable) : + LoaderStateMachine(oracle, oracleObservable), + revision_(0), + countProcessedInstances_(0), + countReferencedInstances_(0), + structuresReady_(false) + { + } + + + void DicomStructureSetLoader::SetStructureDisplayState(size_t structureIndex, bool display) + { + structureVisibility_.at(structureIndex) = display; + revision_++; + } + + DicomStructureSetLoader::~DicomStructureSetLoader() + { + LOG(TRACE) << "DicomStructureSetLoader::~DicomStructureSetLoader()"; + } + + void DicomStructureSetLoader::LoadInstance( + const std::string& instanceId, + const std::vector& initiallyVisibleStructures) + { + Start(); + + instanceId_ = instanceId; + initiallyVisibleStructures_ = initiallyVisibleStructures; + + { + std::auto_ptr command(new OrthancStone::OrthancRestApiCommand); + command->SetHttpHeader("Accept-Encoding", "gzip"); + + std::string uri = "/instances/" + instanceId + "/tags?ignore-length=3006-0050"; + + command->SetUri(uri); + command->AcquirePayload(new LoadStructure(*this)); + Schedule(command.release()); + } + } + + + OrthancStone::IVolumeSlicer::IExtractedSlice* DicomStructureSetLoader::ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane) + { + if (content_.get() == NULL) + { + // Geometry is not available yet + return new OrthancStone::IVolumeSlicer::InvalidSlice; + } + else + { + return new Slice(*content_, revision_, cuttingPlane, structureVisibility_); + } + } + + void DicomStructureSetLoader::SetStructuresReady() + { + ORTHANC_ASSERT(!structuresReady_); + structuresReady_ = true; + BroadcastMessage(DicomStructureSetLoader::StructuresReady(*this)); + } + + bool DicomStructureSetLoader::AreStructuresReady() const + { + return structuresReady_; + } + +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Loaders/DicomStructureSetLoader.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Deprecated/Loaders/DicomStructureSetLoader.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,100 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../../Toolbox/DicomStructureSet.h" +#include "../../Volumes/IVolumeSlicer.h" +#include "LoaderStateMachine.h" + +#include + +namespace Deprecated +{ + class DicomStructureSetLoader : + public LoaderStateMachine, + public OrthancStone::IVolumeSlicer, + public OrthancStone::IObservable + { + private: + class Slice; + + // States of LoaderStateMachine + class AddReferencedInstance; // 3rd state + class LookupInstance; // 2nd state + class LoadStructure; // 1st state + + std::auto_ptr content_; + uint64_t revision_; + std::string instanceId_; + unsigned int countProcessedInstances_; + unsigned int countReferencedInstances_; + + // will be set to true once the loading is finished + bool structuresReady_; + + /** + At load time, these strings are used to initialize the structureVisibility_ + vector. + + As a special case, if initiallyVisibleStructures_ contains a single string + that is '*', ALL structures will be made visible. + */ + std::vector initiallyVisibleStructures_; + + /** + Contains the "Should this structure be displayed?" flag for all structures. + Only filled when structures are loaded. + + Changing this value directly affects the rendering + */ + std::vector structureVisibility_; + + public: + ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, StructuresReady, DicomStructureSetLoader); + + DicomStructureSetLoader(OrthancStone::IOracle& oracle, + OrthancStone::IObservable& oracleObservable); + + OrthancStone::DicomStructureSet* GetContent() + { + return content_.get(); + } + + void SetStructureDisplayState(size_t structureIndex, bool display); + + bool GetStructureDisplayState(size_t structureIndex) const + { + return structureVisibility_.at(structureIndex); + } + + ~DicomStructureSetLoader(); + + void LoadInstance(const std::string& instanceId, + const std::vector& initiallyVisibleStructures = std::vector()); + + virtual IExtractedSlice* ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE; + + void SetStructuresReady(); + + bool AreStructuresReady() const; + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Loaders/DicomStructureSetLoader2.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Deprecated/Loaders/DicomStructureSetLoader2.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,125 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + +#include "DicomStructureSetLoader2.h" + +#include "../Messages/IObservable.h" +#include "../Oracle/IOracle.h" +#include "../Oracle/OracleCommandExceptionMessage.h" + +namespace Deprecated +{ + + DicomStructureSetLoader2::DicomStructureSetLoader2( + DicomStructureSet2& structureSet + , IOracle& oracle + , IObservable& oracleObservable) + : IObserver(oracleObservable.GetBroker()) + , IObservable(oracleObservable.GetBroker()) + , structureSet_(structureSet) + , oracle_(oracle) + , oracleObservable_(oracleObservable) + , structuresReady_(false) + { + LOG(TRACE) << "DicomStructureSetLoader2(" << std::hex << this << std::dec << ")::DicomStructureSetLoader2()"; + + oracleObservable.RegisterObserverCallback( + new Callable + (*this, &DicomStructureSetLoader2::HandleSuccessMessage)); + + oracleObservable.RegisterObserverCallback( + new Callable + (*this, &DicomStructureSetLoader2::HandleExceptionMessage)); + } + + DicomStructureSetLoader2::~DicomStructureSetLoader2() + { + LOG(TRACE) << "DicomStructureSetLoader2(" << std::hex << this << std::dec << ")::~DicomStructureSetLoader2()"; + oracleObservable_.Unregister(this); + } + + void DicomStructureSetLoader2::LoadInstanceFromString(const std::string& body) + { + OrthancPlugins::FullOrthancDataset dicom(body); + //loader.content_.reset(new DicomStructureSet(dicom)); + structureSet_.Clear(); + structureSet_.SetContents(dicom); + SetStructuresReady(); + } + + void DicomStructureSetLoader2::HandleSuccessMessage(const OrthancRestApiCommand::SuccessMessage& message) + { + const std::string& body = message.GetAnswer(); + LoadInstanceFromString(body); + } + + void DicomStructureSetLoader2::HandleExceptionMessage(const OracleCommandExceptionMessage& message) + { + LOG(ERROR) << "DicomStructureSetLoader2::HandleExceptionMessage: error when trying to load data. " + << "Error: " << message.GetException().What() << " Details: " + << message.GetException().GetDetails(); + } + + void DicomStructureSetLoader2::LoadInstance(const std::string& instanceId) + { + std::auto_ptr command(new OrthancRestApiCommand); + command->SetHttpHeader("Accept-Encoding", "gzip"); + + std::string uri = "/instances/" + instanceId + "/tags?ignore-length=3006-0050"; + + command->SetUri(uri); + oracle_.Schedule(*this, command.release()); + } + + void DicomStructureSetLoader2::SetStructuresReady() + { + structuresReady_ = true; + } + + bool DicomStructureSetLoader2::AreStructuresReady() const + { + return structuresReady_; + } + + /* + + void LoaderStateMachine::HandleExceptionMessage(const OracleCommandExceptionMessage& message) + { + LOG(ERROR) << "LoaderStateMachine::HandleExceptionMessage: error in the state machine, stopping all processing"; + LOG(ERROR) << "Error: " << message.GetException().What() << " Details: " << + message.GetException().GetDetails(); + Clear(); + } + + LoaderStateMachine::~LoaderStateMachine() + { + Clear(); + } + + + */ + +} + +#endif +// BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Loaders/DicomStructureSetLoader2.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Deprecated/Loaders/DicomStructureSetLoader2.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,87 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + +#pragma once + +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + +#include "../Toolbox/DicomStructureSet2.h" +#include "../Messages/IMessage.h" +#include "../Messages/IObserver.h" +#include "../Messages/IObservable.h" +#include "../Oracle/OrthancRestApiCommand.h" + +#include + +namespace Deprecated +{ + class IOracle; + class IObservable; + class OrthancRestApiCommand; + class OracleCommandExceptionMessage; + + class DicomStructureSetLoader2 : public IObserver, public IObservable + { + public: + ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, StructuresReady, DicomStructureSetLoader2); + + /** + Warning: the structureSet, oracle and oracleObservable objects must live + at least as long as this object (TODO: shared_ptr?) + */ + DicomStructureSetLoader2(DicomStructureSet2& structureSet, IOracle& oracle, IObservable& oracleObservable); + + ~DicomStructureSetLoader2(); + + void LoadInstance(const std::string& instanceId); + + /** Internal use */ + void LoadInstanceFromString(const std::string& body); + + void SetStructuresReady(); + bool AreStructuresReady() const; + + private: + /** + Called back by the oracle when data is ready! + */ + void HandleSuccessMessage(const OrthancRestApiCommand::SuccessMessage& message); + + /** + Called back by the oracle when shit hits the fan + */ + void HandleExceptionMessage(const OracleCommandExceptionMessage& message); + + /** + The structure set that will be (cleared and) filled with data from the + loader + */ + DicomStructureSet2& structureSet_; + + IOracle& oracle_; + IObservable& oracleObservable_; + bool structuresReady_; + }; +} + +#endif +// BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + + diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Loaders/LoaderCache.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Deprecated/Loaders/LoaderCache.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,412 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + +#include "LoaderCache.h" + +#include "../../StoneException.h" +#include "OrthancSeriesVolumeProgressiveLoader.h" +#include "OrthancMultiframeVolumeLoader.h" +#include "DicomStructureSetLoader.h" + +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 +#include "DicomStructureSetLoader2.h" +#endif + //BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + + +#if ORTHANC_ENABLE_WASM == 1 +# include +# include "../../Oracle/WebAssemblyOracle.h" +#else +# include "../../Oracle/ThreadedOracle.h" +#endif + +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 +#include "../../Toolbox/DicomStructureSet2.h" +#endif +//BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + +#include "../../Volumes/DicomVolumeImage.h" +#include "../../Volumes/DicomVolumeImageMPRSlicer.h" + +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 +#include "../../Volumes/DicomStructureSetSlicer2.h" +#endif +//BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + +#include +#include + +namespace Deprecated +{ +#if ORTHANC_ENABLE_WASM == 1 + LoaderCache::LoaderCache(OrthancStone::WebAssemblyOracle& oracle) + : oracle_(oracle) + { + + } +#else + LoaderCache::LoaderCache(OrthancStone::ThreadedOracle& oracle, + LockingEmitter& lockingEmitter) + : oracle_(oracle) + , lockingEmitter_(lockingEmitter) + { + } +#endif + + boost::shared_ptr + LoaderCache::GetSeriesVolumeProgressiveLoader(std::string seriesUuid) + { + try + { + + // normalize keys a little + seriesUuid = Orthanc::Toolbox::StripSpaces(seriesUuid); + Orthanc::Toolbox::ToLowerCase(seriesUuid); + + // find in cache + if (seriesVolumeProgressiveLoaders_.find(seriesUuid) == seriesVolumeProgressiveLoaders_.end()) + { +// LOG(TRACE) << "LoaderCache::GetSeriesVolumeProgressiveLoader : CACHEMISS --> need to load seriesUUid = " << seriesUuid; +#if ORTHANC_ENABLE_WASM == 1 +// LOG(TRACE) << "Performing request for series " << seriesUuid << " sbrk(0) = " << sbrk(0); +#else +// LOG(TRACE) << "Performing request for series " << seriesUuid; +#endif + boost::shared_ptr volumeImage(new OrthancStone::DicomVolumeImage); + boost::shared_ptr loader; +// LOG(TRACE) << "volumeImage = " << volumeImage.get(); + { +#if ORTHANC_ENABLE_WASM == 1 + loader.reset(new OrthancSeriesVolumeProgressiveLoader(volumeImage, oracle_, oracle_)); +#else + LockingEmitter::WriterLock lock(lockingEmitter_); + loader.reset(new OrthancSeriesVolumeProgressiveLoader(volumeImage, oracle_, lock.GetOracleObservable())); +#endif +// LOG(TRACE) << "LoaderCache::GetSeriesVolumeProgressiveLoader : loader = " << loader.get(); + loader->LoadSeries(seriesUuid); +// LOG(TRACE) << "LoaderCache::GetSeriesVolumeProgressiveLoader : loader->LoadSeries successful"; + } + seriesVolumeProgressiveLoaders_[seriesUuid] = loader; + } + else + { +// LOG(TRACE) << "LoaderCache::GetSeriesVolumeProgressiveLoader : returning cached loader for seriesUUid = " << seriesUuid; + } + return seriesVolumeProgressiveLoaders_[seriesUuid]; + } + catch (const Orthanc::OrthancException& e) + { + if (e.HasDetails()) + { + LOG(ERROR) << "OrthancException in LoaderCache: " << e.What() << " Details: " << e.GetDetails(); + } + else + { + LOG(ERROR) << "OrthancException in LoaderCache: " << e.What(); + } + throw; + } + catch (const std::exception& e) + { + LOG(ERROR) << "std::exception in LoaderCache: " << e.what(); + throw; + } + catch (...) + { + LOG(ERROR) << "Unknown exception in LoaderCache"; + throw; + } + } + + boost::shared_ptr LoaderCache::GetMultiframeVolumeLoader(std::string instanceUuid) + { + // if the loader is not available, let's trigger its creation + if(multiframeVolumeLoaders_.find(instanceUuid) == multiframeVolumeLoaders_.end()) + { + GetMultiframeDicomVolumeImageMPRSlicer(instanceUuid); + } + ORTHANC_ASSERT(multiframeVolumeLoaders_.find(instanceUuid) != multiframeVolumeLoaders_.end()); + + return multiframeVolumeLoaders_[instanceUuid]; + } + + boost::shared_ptr LoaderCache::GetMultiframeDicomVolumeImageMPRSlicer(std::string instanceUuid) + { + try + { + // normalize keys a little + instanceUuid = Orthanc::Toolbox::StripSpaces(instanceUuid); + Orthanc::Toolbox::ToLowerCase(instanceUuid); + + // find in cache + if (dicomVolumeImageMPRSlicers_.find(instanceUuid) == dicomVolumeImageMPRSlicers_.end()) + { + boost::shared_ptr volumeImage(new OrthancStone::DicomVolumeImage); + boost::shared_ptr loader; + + { +#if ORTHANC_ENABLE_WASM == 1 + loader.reset(new OrthancMultiframeVolumeLoader(volumeImage, oracle_, oracle_)); +#else + LockingEmitter::WriterLock lock(lockingEmitter_); + loader.reset(new OrthancMultiframeVolumeLoader(volumeImage, oracle_, lock.GetOracleObservable())); +#endif + loader->LoadInstance(instanceUuid); + } + multiframeVolumeLoaders_[instanceUuid] = loader; + boost::shared_ptr mprSlicer(new OrthancStone::DicomVolumeImageMPRSlicer(volumeImage)); + dicomVolumeImageMPRSlicers_[instanceUuid] = mprSlicer; + } + return dicomVolumeImageMPRSlicers_[instanceUuid]; + } + catch (const Orthanc::OrthancException& e) + { + if (e.HasDetails()) + { + LOG(ERROR) << "OrthancException in LoaderCache: " << e.What() << " Details: " << e.GetDetails(); + } + else + { + LOG(ERROR) << "OrthancException in LoaderCache: " << e.What(); + } + throw; + } + catch (const std::exception& e) + { + LOG(ERROR) << "std::exception in LoaderCache: " << e.what(); + throw; + } + catch (...) + { + LOG(ERROR) << "Unknown exception in LoaderCache"; + throw; + } + } + +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + + boost::shared_ptr LoaderCache::GetDicomStructureSetSlicer2(std::string instanceUuid) + { + // if the loader is not available, let's trigger its creation + if (dicomStructureSetSlicers2_.find(instanceUuid) == dicomStructureSetSlicers2_.end()) + { + GetDicomStructureSetLoader2(instanceUuid); + } + ORTHANC_ASSERT(dicomStructureSetSlicers2_.find(instanceUuid) != dicomStructureSetSlicers2_.end()); + + return dicomStructureSetSlicers2_[instanceUuid]; + } +#endif +//BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + + + /** + This method allows to convert a list of string into a string by + sorting the strings then joining them + */ + static std::string SortAndJoin(const std::vector& stringList) + { + if (stringList.size() == 0) + { + return ""; + } + else + { + std::vector sortedStringList = stringList; + std::sort(sortedStringList.begin(), sortedStringList.end()); + std::stringstream s; + s << sortedStringList[0]; + for (size_t i = 1; i < sortedStringList.size(); ++i) + { + s << "-" << sortedStringList[i]; + } + return s.str(); + } + } + + boost::shared_ptr + LoaderCache::GetDicomStructureSetLoader( + std::string inInstanceUuid, + const std::vector& initiallyVisibleStructures) + { + try + { + // normalize keys a little + inInstanceUuid = Orthanc::Toolbox::StripSpaces(inInstanceUuid); + Orthanc::Toolbox::ToLowerCase(inInstanceUuid); + + std::string initiallyVisibleStructuresKey = + SortAndJoin(initiallyVisibleStructures); + + std::string entryKey = inInstanceUuid + "_" + initiallyVisibleStructuresKey; + + // find in cache + if (dicomStructureSetLoaders_.find(entryKey) == dicomStructureSetLoaders_.end()) + { + boost::shared_ptr loader; + + { +#if ORTHANC_ENABLE_WASM == 1 + loader.reset(new DicomStructureSetLoader(oracle_, oracle_)); +#else + LockingEmitter::WriterLock lock(lockingEmitter_); + loader.reset(new DicomStructureSetLoader(oracle_, lock.GetOracleObservable())); +#endif + loader->LoadInstance(inInstanceUuid, initiallyVisibleStructures); + } + dicomStructureSetLoaders_[entryKey] = loader; + } + return dicomStructureSetLoaders_[entryKey]; + } + catch (const Orthanc::OrthancException& e) + { + if (e.HasDetails()) + { + LOG(ERROR) << "OrthancException in LoaderCache: " << e.What() << " Details: " << e.GetDetails(); + } + else + { + LOG(ERROR) << "OrthancException in LoaderCache: " << e.What(); + } + throw; + } + catch (const std::exception& e) + { + LOG(ERROR) << "std::exception in LoaderCache: " << e.what(); + throw; + } + catch (...) + { + LOG(ERROR) << "Unknown exception in LoaderCache"; + throw; + } + } + +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + + boost::shared_ptr LoaderCache::GetDicomStructureSetLoader2(std::string instanceUuid) + { + try + { + // normalize keys a little + instanceUuid = Orthanc::Toolbox::StripSpaces(instanceUuid); + Orthanc::Toolbox::ToLowerCase(instanceUuid); + + // find in cache + if (dicomStructureSetLoaders2_.find(instanceUuid) == dicomStructureSetLoaders2_.end()) + { + boost::shared_ptr loader; + boost::shared_ptr structureSet(new DicomStructureSet2()); + boost::shared_ptr rtSlicer(new DicomStructureSetSlicer2(structureSet)); + dicomStructureSetSlicers2_[instanceUuid] = rtSlicer; + dicomStructureSets2_[instanceUuid] = structureSet; // to prevent it from being deleted + { +#if ORTHANC_ENABLE_WASM == 1 + loader.reset(new DicomStructureSetLoader2(*(structureSet.get()), oracle_, oracle_)); +#else + LockingEmitter::WriterLock lock(lockingEmitter_); + // TODO: clarify lifetimes... this is DANGEROUS! + loader.reset(new DicomStructureSetLoader2(*(structureSet.get()), oracle_, lock.GetOracleObservable())); +#endif + loader->LoadInstance(instanceUuid); + } + dicomStructureSetLoaders2_[instanceUuid] = loader; + } + return dicomStructureSetLoaders2_[instanceUuid]; + } + catch (const Orthanc::OrthancException& e) + { + if (e.HasDetails()) + { + LOG(ERROR) << "OrthancException in GetDicomStructureSetLoader2: " << e.What() << " Details: " << e.GetDetails(); + } + else + { + LOG(ERROR) << "OrthancException in GetDicomStructureSetLoader2: " << e.What(); + } + throw; + } + catch (const std::exception& e) + { + LOG(ERROR) << "std::exception in GetDicomStructureSetLoader2: " << e.what(); + throw; + } + catch (...) + { + LOG(ERROR) << "Unknown exception in GetDicomStructureSetLoader2"; + throw; + } + } + +#endif +// BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + + + void LoaderCache::ClearCache() + { +#if ORTHANC_ENABLE_WASM != 1 + LockingEmitter::WriterLock lock(lockingEmitter_); +#endif + +//#ifndef NDEBUG + // ISO way of checking for debug builds + DebugDisplayObjRefCounts(); +//#endif + seriesVolumeProgressiveLoaders_.clear(); + multiframeVolumeLoaders_.clear(); + dicomVolumeImageMPRSlicers_.clear(); + dicomStructureSetLoaders_.clear(); + +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + // order is important! + dicomStructureSetLoaders2_.clear(); + dicomStructureSetSlicers2_.clear(); + dicomStructureSets2_.clear(); +#endif +// BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + } + + template void DebugDisplayObjRefCountsInMap( + const std::string& name, const std::map >& myMap) + { + LOG(TRACE) << "Map \"" << name << "\" ref counts:"; + size_t i = 0; + for (typename std::map >::const_iterator + it = myMap.begin(); it != myMap.end(); ++it) + { + LOG(TRACE) << " element #" << i << ": ref count = " << it->second.use_count(); + i++; + } + } + + void LoaderCache::DebugDisplayObjRefCounts() + { + DebugDisplayObjRefCountsInMap("seriesVolumeProgressiveLoaders_", seriesVolumeProgressiveLoaders_); + DebugDisplayObjRefCountsInMap("multiframeVolumeLoaders_", multiframeVolumeLoaders_); + DebugDisplayObjRefCountsInMap("dicomVolumeImageMPRSlicers_", dicomVolumeImageMPRSlicers_); + DebugDisplayObjRefCountsInMap("dicomStructureSetLoaders_", dicomStructureSetLoaders_); +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + DebugDisplayObjRefCountsInMap("dicomStructureSetLoaders2_", dicomStructureSetLoaders2_); + DebugDisplayObjRefCountsInMap("dicomStructureSetSlicers2_", dicomStructureSetSlicers2_); +#endif +//BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + } +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Loaders/LoaderCache.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Deprecated/Loaders/LoaderCache.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,109 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + +#pragma once + +#include "../Messages/LockingEmitter.h" +#include "../../Volumes/DicomVolumeImageMPRSlicer.h" +#include "OrthancSeriesVolumeProgressiveLoader.h" +#include "OrthancMultiframeVolumeLoader.h" +#include "DicomStructureSetLoader.h" + +#include + +#include +#include +#include + +namespace OrthancStone +{ +#if ORTHANC_ENABLE_WASM == 1 + class WebAssemblyOracle; +#else + class ThreadedOracle; +#endif +} + +namespace Deprecated +{ + class LoaderCache + { + public: +#if ORTHANC_ENABLE_WASM == 1 + LoaderCache(OrthancStone::WebAssemblyOracle& oracle); +#else + LoaderCache(OrthancStone::ThreadedOracle& oracle, LockingEmitter& lockingEmitter); +#endif + + boost::shared_ptr + GetSeriesVolumeProgressiveLoader (std::string seriesUuid); + + boost::shared_ptr + GetMultiframeDicomVolumeImageMPRSlicer(std::string instanceUuid); + + boost::shared_ptr + GetMultiframeVolumeLoader(std::string instanceUuid); + + boost::shared_ptr + GetDicomStructureSetLoader( + std::string instanceUuid, + const std::vector& initiallyVisibleStructures); + +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + boost::shared_ptr + GetDicomStructureSetLoader2(std::string instanceUuid); + + boost::shared_ptr + GetDicomStructureSetSlicer2(std::string instanceUuid); +#endif + //BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + + void ClearCache(); + + private: + + void DebugDisplayObjRefCounts(); +#if ORTHANC_ENABLE_WASM == 1 + OrthancStone::WebAssemblyOracle& oracle_; +#else + OrthancStone::ThreadedOracle& oracle_; + LockingEmitter& lockingEmitter_; +#endif + + std::map > + seriesVolumeProgressiveLoaders_; + std::map > + multiframeVolumeLoaders_; + std::map > + dicomVolumeImageMPRSlicers_; + std::map > + dicomStructureSetLoaders_; +#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + std::map > + dicomStructureSetLoaders2_; + std::map > + dicomStructureSets2_; + std::map > + dicomStructureSetSlicers2_; +#endif + //BGO_ENABLE_DICOMSTRUCTURESETLOADER2 + }; +} + diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Loaders/LoaderStateMachine.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Deprecated/Loaders/LoaderStateMachine.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,198 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "LoaderStateMachine.h" + +#include + +namespace Deprecated +{ + void LoaderStateMachine::State::Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + + void LoaderStateMachine::State::Handle(const OrthancStone::GetOrthancImageCommand::SuccessMessage& message) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + + void LoaderStateMachine::State::Handle(const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + + void LoaderStateMachine::Schedule(OrthancStone::OracleCommandBase* command) + { + LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::Schedule()"; + + std::auto_ptr protection(command); + + if (command == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + if (!command->HasPayload()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, + "The payload must contain the next state"); + } + pendingCommands_.push_back(protection.release()); + + Step(); + } + + + void LoaderStateMachine::Start() + { + LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::Start()"; + + if (active_) + { + LOG(TRACE) << "LoaderStateMachine::Start() called while active_ is true"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + active_ = true; + + for (size_t i = 0; i < simultaneousDownloads_; i++) + { + Step(); + } + } + + + void LoaderStateMachine::Step() + { + if (!pendingCommands_.empty() && + activeCommands_ < simultaneousDownloads_) + { + + OrthancStone::IOracleCommand* nextCommand = pendingCommands_.front(); + + LOG(TRACE) << " LoaderStateMachine(" << std::hex << this << std::dec << + ")::Step(): activeCommands_ (" << activeCommands_ << + ") < simultaneousDownloads_ (" << simultaneousDownloads_ << + ") --> will Schedule command addr " << std::hex << nextCommand << std::dec; + + boost::shared_ptr observer(GetSharedObserver()); + oracle_.Schedule(observer, nextCommand); + pendingCommands_.pop_front(); + + activeCommands_++; + } + else + { + LOG(TRACE) << " LoaderStateMachine(" << std::hex << this << std::dec << + ")::Step(): activeCommands_ (" << activeCommands_ << + ") >= simultaneousDownloads_ (" << simultaneousDownloads_ << + ") --> will NOT Schedule command"; + } + } + + + void LoaderStateMachine::Clear() + { + LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::Clear()"; + for (PendingCommands::iterator it = pendingCommands_.begin(); + it != pendingCommands_.end(); ++it) + { + delete *it; + } + + pendingCommands_.clear(); + } + + + void LoaderStateMachine::HandleExceptionMessage(const OrthancStone::OracleCommandExceptionMessage& message) + { + LOG(ERROR) << "LoaderStateMachine::HandleExceptionMessage: error in the state machine, stopping all processing"; + LOG(ERROR) << "Error: " << message.GetException().What() << " Details: " << + message.GetException().GetDetails(); + Clear(); + } + + template + void LoaderStateMachine::HandleSuccessMessage(const T& message) + { + if (activeCommands_ <= 0) { + LOG(ERROR) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::HandleSuccessMessage : activeCommands_ should be > 0 but is: " << activeCommands_; + } + else { + activeCommands_--; + try + { + dynamic_cast(message.GetOrigin().GetPayload()).Handle(message); + Step(); + } + catch (Orthanc::OrthancException& e) + { + LOG(ERROR) << "Error in the state machine, stopping all processing: " << + e.What() << " Details: " << e.GetDetails(); + Clear(); + } + } + } + + + LoaderStateMachine::LoaderStateMachine(OrthancStone::IOracle& oracle, + OrthancStone::IObservable& oracleObservable) : + oracle_(oracle), + active_(false), + simultaneousDownloads_(4), + activeCommands_(0) + { + LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::LoaderStateMachine()"; + + // TODO => Move this out of constructor + Register(oracleObservable, &LoaderStateMachine::HandleSuccessMessage); + Register(oracleObservable, &LoaderStateMachine::HandleSuccessMessage); + Register(oracleObservable, &LoaderStateMachine::HandleSuccessMessage); + Register(oracleObservable, &LoaderStateMachine::HandleExceptionMessage); + } + + LoaderStateMachine::~LoaderStateMachine() + { + LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::~LoaderStateMachine()"; + Clear(); + } + + void LoaderStateMachine::SetSimultaneousDownloads(unsigned int count) + { + if (active_) + { + LOG(ERROR) << "LoaderStateMachine::SetSimultaneousDownloads called while active_ is true"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else if (count == 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + simultaneousDownloads_ = count; + } + } +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Loaders/LoaderStateMachine.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Deprecated/Loaders/LoaderStateMachine.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,116 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../../Messages/IObservable.h" +#include "../../Messages/ObserverBase.h" +#include "../../Oracle/GetOrthancImageCommand.h" +#include "../../Oracle/GetOrthancWebViewerJpegCommand.h" +#include "../../Oracle/IOracle.h" +#include "../../Oracle/OracleCommandExceptionMessage.h" +#include "../../Oracle/OrthancRestApiCommand.h" + +#include + +#include + +namespace Deprecated +{ + /** + This class is supplied with Oracle commands and will schedule up to + simultaneousDownloads_ of them at the same time, then will schedule the + rest once slots become available. It is used, a.o., by the + OrtancMultiframeVolumeLoader class. + */ + class LoaderStateMachine : public OrthancStone::ObserverBase + { + protected: + class State : public Orthanc::IDynamicObject + { + private: + LoaderStateMachine& that_; + + public: + State(LoaderStateMachine& that) : + that_(that) + { + } + + State(const State& currentState) : + that_(currentState.that_) + { + } + + void Schedule(OrthancStone::OracleCommandBase* command) const + { + that_.Schedule(command); + } + + template + T& GetLoader() const + { + return dynamic_cast(that_); + } + + virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message); + + virtual void Handle(const OrthancStone::GetOrthancImageCommand::SuccessMessage& message); + + virtual void Handle(const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message); + }; + + void Schedule(OrthancStone::OracleCommandBase* command); + + void Start(); + + private: + void Step(); + + void Clear(); + + void HandleExceptionMessage(const OrthancStone::OracleCommandExceptionMessage& message); + + template + void HandleSuccessMessage(const T& message); + + typedef std::list PendingCommands; + + OrthancStone::IOracle& oracle_; + bool active_; + unsigned int simultaneousDownloads_; + PendingCommands pendingCommands_; + unsigned int activeCommands_; + + public: + LoaderStateMachine(OrthancStone::IOracle& oracle, + OrthancStone::IObservable& oracleObservable); + + virtual ~LoaderStateMachine(); + + bool IsActive() const + { + return active_; + } + + void SetSimultaneousDownloads(unsigned int count); + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,580 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "OrthancMultiframeVolumeLoader.h" + +#include +#include + +namespace Deprecated +{ + class OrthancMultiframeVolumeLoader::LoadRTDoseGeometry : public LoaderStateMachine::State + { + private: + std::auto_ptr dicom_; + + public: + LoadRTDoseGeometry(OrthancMultiframeVolumeLoader& that, + Orthanc::DicomMap* dicom) : + State(that), + dicom_(dicom) + { + if (dicom == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + } + + virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) + { + // Complete the DICOM tags with just-received "Grid Frame Offset Vector" + std::string s = Orthanc::Toolbox::StripSpaces(message.GetAnswer()); + dicom_->SetValue(Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR, s, false); + + GetLoader().SetGeometry(*dicom_); + } + }; + + + static std::string GetSopClassUid(const Orthanc::DicomMap& dicom) + { + std::string s; + if (!dicom.LookupStringValue(s, Orthanc::DICOM_TAG_SOP_CLASS_UID, false)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "DICOM file without SOP class UID"); + } + else + { + return s; + } + } + + + class OrthancMultiframeVolumeLoader::LoadGeometry : public State + { + public: + LoadGeometry(OrthancMultiframeVolumeLoader& that) : + State(that) + { + } + + virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) + { + OrthancMultiframeVolumeLoader& loader = GetLoader(); + + Json::Value body; + message.ParseJsonBody(body); + + if (body.type() != Json::objectValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + + std::auto_ptr dicom(new Orthanc::DicomMap); + dicom->FromDicomAsJson(body); + + if (OrthancStone::StringToSopClassUid(GetSopClassUid(*dicom)) == OrthancStone::SopClassUid_RTDose) + { + // Download the "Grid Frame Offset Vector" DICOM tag, that is + // mandatory for RT-DOSE, but is too long to be returned by default + + std::auto_ptr command(new OrthancStone::OrthancRestApiCommand); + command->SetUri("/instances/" + loader.GetInstanceId() + "/content/" + + Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR.Format()); + command->AcquirePayload(new LoadRTDoseGeometry(loader, dicom.release())); + + Schedule(command.release()); + } + else + { + loader.SetGeometry(*dicom); + } + } + }; + + class OrthancMultiframeVolumeLoader::LoadTransferSyntax : public State + { + public: + LoadTransferSyntax(OrthancMultiframeVolumeLoader& that) : + State(that) + { + } + + virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) + { + GetLoader().SetTransferSyntax(message.GetAnswer()); + } + }; + + class OrthancMultiframeVolumeLoader::LoadUncompressedPixelData : public State + { + public: + LoadUncompressedPixelData(OrthancMultiframeVolumeLoader& that) : + State(that) + { + } + + virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) + { + GetLoader().SetUncompressedPixelData(message.GetAnswer()); + } + }; + + const std::string& OrthancMultiframeVolumeLoader::GetInstanceId() const + { + if (IsActive()) + { + return instanceId_; + } + else + { + LOG(ERROR) << "OrthancMultiframeVolumeLoader::GetInstanceId(): (!IsActive())"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + void OrthancMultiframeVolumeLoader::ScheduleFrameDownloads() + { + if (transferSyntaxUid_.empty() || + !volume_->HasGeometry()) + { + return; + } + /* + 1.2.840.10008.1.2 Implicit VR Endian: Default Transfer Syntax for DICOM + 1.2.840.10008.1.2.1 Explicit VR Little Endian + 1.2.840.10008.1.2.2 Explicit VR Big Endian + + See https://www.dicomlibrary.com/dicom/transfer-syntax/ + */ + if (transferSyntaxUid_ == "1.2.840.10008.1.2" || + transferSyntaxUid_ == "1.2.840.10008.1.2.1" || + transferSyntaxUid_ == "1.2.840.10008.1.2.2") + { + std::auto_ptr command(new OrthancStone::OrthancRestApiCommand); + command->SetHttpHeader("Accept-Encoding", "gzip"); + command->SetUri("/instances/" + instanceId_ + "/content/" + + Orthanc::DICOM_TAG_PIXEL_DATA.Format() + "/0"); + command->AcquirePayload(new LoadUncompressedPixelData(*this)); + Schedule(command.release()); + } + else + { + throw Orthanc::OrthancException( + Orthanc::ErrorCode_NotImplemented, + "No support for multiframe instances with transfer syntax: " + transferSyntaxUid_); + } + } + + void OrthancMultiframeVolumeLoader::SetTransferSyntax(const std::string& transferSyntax) + { + transferSyntaxUid_ = Orthanc::Toolbox::StripSpaces(transferSyntax); + ScheduleFrameDownloads(); + } + + void OrthancMultiframeVolumeLoader::SetGeometry(const Orthanc::DicomMap& dicom) + { + OrthancStone::DicomInstanceParameters parameters(dicom); + volume_->SetDicomParameters(parameters); + + Orthanc::PixelFormat format; + if (!parameters.GetImageInformation().ExtractPixelFormat(format, true)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + double spacingZ; + switch (parameters.GetSopClassUid()) + { + case OrthancStone::SopClassUid_RTDose: + spacingZ = parameters.GetThickness(); + break; + + default: + throw Orthanc::OrthancException( + Orthanc::ErrorCode_NotImplemented, + "No support for multiframe instances with SOP class UID: " + GetSopClassUid(dicom)); + } + + const unsigned int width = parameters.GetImageInformation().GetWidth(); + const unsigned int height = parameters.GetImageInformation().GetHeight(); + const unsigned int depth = parameters.GetImageInformation().GetNumberOfFrames(); + + { + OrthancStone::VolumeImageGeometry geometry; + geometry.SetSizeInVoxels(width, height, depth); + geometry.SetAxialGeometry(parameters.GetGeometry()); + geometry.SetVoxelDimensions(parameters.GetPixelSpacingX(), + parameters.GetPixelSpacingY(), spacingZ); + volume_->Initialize(geometry, format, true /* Do compute range */); + } + + volume_->GetPixelData().Clear(); + + ScheduleFrameDownloads(); + + + + BroadcastMessage(OrthancStone::DicomVolumeImage::GeometryReadyMessage(*volume_)); + } + + + ORTHANC_FORCE_INLINE + static void CopyPixel(uint32_t& target, const void* source) + { + // TODO - check alignement? + target = le32toh(*reinterpret_cast(source)); + } + + ORTHANC_FORCE_INLINE + static void CopyPixel(uint16_t& target, const void* source) + { + // TODO - check alignement? + target = le16toh(*reinterpret_cast(source)); + } + + ORTHANC_FORCE_INLINE + static void CopyPixel(int16_t& target, const void* source) + { + // byte swapping is the same for unsigned and signed integers + // (the sign bit is always stored with the MSByte) + uint16_t* targetUp = reinterpret_cast(&target); + CopyPixel(*targetUp, source); + } + + template + void OrthancMultiframeVolumeLoader::CopyPixelDataAndComputeDistribution( + const std::string& pixelData, std::map& distribution) + { + OrthancStone::ImageBuffer3D& target = volume_->GetPixelData(); + + const unsigned int bpp = target.GetBytesPerPixel(); + const unsigned int width = target.GetWidth(); + const unsigned int height = target.GetHeight(); + const unsigned int depth = target.GetDepth(); + + if (pixelData.size() != bpp * width * height * depth) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "The pixel data has not the proper size"); + } + + if (pixelData.empty()) + { + return; + } + + // first pass to initialize map + { + const uint8_t* source = reinterpret_cast(pixelData.c_str()); + + for (unsigned int z = 0; z < depth; z++) + { + for (unsigned int y = 0; y < height; y++) + { + for (unsigned int x = 0; x < width; x++) + { + T value; + CopyPixel(value, source); + distribution[value] = 0; + source += bpp; + } + } + } + } + + { + const uint8_t* source = reinterpret_cast(pixelData.c_str()); + + for (unsigned int z = 0; z < depth; z++) + { + OrthancStone::ImageBuffer3D::SliceWriter writer(target, OrthancStone::VolumeProjection_Axial, z); + + assert(writer.GetAccessor().GetWidth() == width && + writer.GetAccessor().GetHeight() == height); + + for (unsigned int y = 0; y < height; y++) + { + assert(sizeof(T) == Orthanc::GetBytesPerPixel(target.GetFormat())); + + T* target = reinterpret_cast(writer.GetAccessor().GetRow(y)); + + for (unsigned int x = 0; x < width; x++) + { + CopyPixel(*target, source); + + distribution[*target] += 1; + + target++; + source += bpp; + } + } + } + } + } + + template + void OrthancMultiframeVolumeLoader::ComputeMinMaxWithOutlierRejection( + const std::map& distribution) + { + if (distribution.size() == 0) + { + LOG(ERROR) << "ComputeMinMaxWithOutlierRejection -- Volume image empty."; + } + else + { + OrthancStone::ImageBuffer3D& target = volume_->GetPixelData(); + + const uint64_t bpp = target.GetBytesPerPixel(); + const uint64_t width = target.GetWidth(); + const uint64_t height = target.GetHeight(); + const uint64_t depth = target.GetDepth(); + const uint64_t voxelCount = width * height * depth; + + // now that we have distribution[pixelValue] == numberOfPixelsWithValue + // compute number of values and check (assertion) that it is equal to + // width * height * depth + { + typename std::map::const_iterator it = distribution.begin(); + uint64_t totalCount = 0; + distributionRawMin_ = static_cast(it->first); + + while (it != distribution.end()) + { + T pixelValue = it->first; + uint64_t count = it->second; + totalCount += count; + it++; + if (it == distribution.end()) + distributionRawMax_ = static_cast(pixelValue); + } + LOG(INFO) << "Volume image. First distribution value = " + << static_cast(distributionRawMin_) + << " | Last distribution value = " + << static_cast(distributionRawMax_); + + if (totalCount != voxelCount) + { + LOG(ERROR) << "Internal error in dose distribution computation. TC (" + << totalCount << ") != VoxC (" << voxelCount; + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + + // compute the number of voxels to reject at each end of the distribution + uint64_t endRejectionCount = static_cast( + outliersHalfRejectionRate_ * voxelCount); + + if (endRejectionCount > voxelCount) + { + LOG(ERROR) << "Internal error in dose distribution computation." + << " endRejectionCount = " << endRejectionCount + << " | voxelCount = " << voxelCount; + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + // this will contain the actual distribution minimum after outlier + // rejection + T resultMin = 0; + + // then start from start and remove pixel values up to + // endRejectionCount voxels rejected + { + typename std::map::const_iterator it = distribution.begin(); + + uint64_t currentCount = 0; + + while (it != distribution.end()) + { + T pixelValue = it->first; + uint64_t count = it->second; + + // if this pixelValue crosses the rejection threshold, let's set it + // and exit the loop + if ((currentCount <= endRejectionCount) && + (currentCount + count > endRejectionCount)) + { + resultMin = pixelValue; + break; + } + else + { + currentCount += count; + } + // and continue walking along the distribution + it++; + } + } + + // this will contain the actual distribution maximum after outlier + // rejection + T resultMax = 0; + // now start from END and remove pixel values up to + // endRejectionCount voxels rejected + { + typename std::map::const_reverse_iterator it = distribution.rbegin(); + + uint64_t currentCount = 0; + + while (it != distribution.rend()) + { + T pixelValue = it->first; + uint64_t count = it->second; + + if ((currentCount <= endRejectionCount) && + (currentCount + count > endRejectionCount)) + { + resultMax = pixelValue; + break; + } + else + { + currentCount += count; + } + // and continue walking along the distribution + it++; + } + } + if (resultMin > resultMax) + { + LOG(ERROR) << "Internal error in dose distribution computation! " << + "resultMin (" << resultMin << ") > resultMax (" << resultMax << ")"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + computedDistributionMin_ = static_cast(resultMin); + computedDistributionMax_ = static_cast(resultMax); + } + } + + template + void OrthancMultiframeVolumeLoader::CopyPixelDataAndComputeMinMax( + const std::string& pixelData) + { + std::map distribution; + CopyPixelDataAndComputeDistribution(pixelData, distribution); + ComputeMinMaxWithOutlierRejection(distribution); + } + + void OrthancMultiframeVolumeLoader::SetUncompressedPixelData(const std::string& pixelData) + { + switch (volume_->GetPixelData().GetFormat()) + { + case Orthanc::PixelFormat_Grayscale32: + CopyPixelDataAndComputeMinMax(pixelData); + break; + case Orthanc::PixelFormat_Grayscale16: + CopyPixelDataAndComputeMinMax(pixelData); + break; + case Orthanc::PixelFormat_SignedGrayscale16: + CopyPixelDataAndComputeMinMax(pixelData); + break; + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + volume_->IncrementRevision(); + + pixelDataLoaded_ = true; + BroadcastMessage(OrthancStone::DicomVolumeImage::ContentUpdatedMessage(*volume_)); + } + + bool OrthancMultiframeVolumeLoader::HasGeometry() const + { + return volume_->HasGeometry(); + } + + const OrthancStone::VolumeImageGeometry& OrthancMultiframeVolumeLoader::GetImageGeometry() const + { + return volume_->GetGeometry(); + } + + OrthancMultiframeVolumeLoader::OrthancMultiframeVolumeLoader( + boost::shared_ptr volume, + OrthancStone::IOracle& oracle, + OrthancStone::IObservable& oracleObservable, + float outliersHalfRejectionRate) : + LoaderStateMachine(oracle, oracleObservable), + volume_(volume), + pixelDataLoaded_(false), + outliersHalfRejectionRate_(outliersHalfRejectionRate), + distributionRawMin_(0), + distributionRawMax_(0), + computedDistributionMin_(0), + computedDistributionMax_(0) + { + if (volume.get() == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + } + + OrthancMultiframeVolumeLoader::~OrthancMultiframeVolumeLoader() + { + LOG(TRACE) << "OrthancMultiframeVolumeLoader::~OrthancMultiframeVolumeLoader()"; + } + + + void OrthancMultiframeVolumeLoader::GetDistributionMinMax + (float& minValue, float& maxValue) const + { + if (distributionRawMin_ == 0 && distributionRawMax_ == 0) + { + LOG(WARNING) << "GetDistributionMinMaxWithOutliersRejection called before computation!"; + } + minValue = distributionRawMin_; + maxValue = distributionRawMax_; + } + + void OrthancMultiframeVolumeLoader::GetDistributionMinMaxWithOutliersRejection + (float& minValue, float& maxValue) const + { + if (computedDistributionMin_ == 0 && computedDistributionMax_ == 0) + { + LOG(WARNING) << "GetDistributionMinMaxWithOutliersRejection called before computation!"; + } + minValue = computedDistributionMin_; + maxValue = computedDistributionMax_; + } + + void OrthancMultiframeVolumeLoader::LoadInstance(const std::string& instanceId) + { + Start(); + + instanceId_ = instanceId; + + { + std::auto_ptr command(new OrthancStone::OrthancRestApiCommand); + command->SetHttpHeader("Accept-Encoding", "gzip"); + command->SetUri("/instances/" + instanceId + "/tags"); + command->AcquirePayload(new LoadGeometry(*this)); + Schedule(command.release()); + } + + { + std::auto_ptr command(new OrthancStone::OrthancRestApiCommand); + command->SetUri("/instances/" + instanceId + "/metadata/TransferSyntax"); + command->AcquirePayload(new LoadTransferSyntax(*this)); + Schedule(command.release()); + } + } +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,115 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "LoaderStateMachine.h" +#include "../../Volumes/DicomVolumeImage.h" + +#include + +namespace Deprecated +{ + class OrthancMultiframeVolumeLoader : + public LoaderStateMachine, + public OrthancStone::IObservable + { + private: + class LoadRTDoseGeometry; + class LoadGeometry; + class LoadTransferSyntax; + class LoadUncompressedPixelData; + + boost::shared_ptr volume_; + std::string instanceId_; + std::string transferSyntaxUid_; + bool pixelDataLoaded_; + float outliersHalfRejectionRate_; + float distributionRawMin_; + float distributionRawMax_; + float computedDistributionMin_; + float computedDistributionMax_; + + const std::string& GetInstanceId() const; + + void ScheduleFrameDownloads(); + + void SetTransferSyntax(const std::string& transferSyntax); + + void SetGeometry(const Orthanc::DicomMap& dicom); + + + /** + This method will : + + - copy the pixel values from the response to the volume image + - compute the maximum and minimum value while discarding the + outliersHalfRejectionRate_ fraction of the outliers from both the start + and the end of the distribution. + + In English, this means that, if the volume dataset contains a few extreme + values very different from the rest (outliers) that we want to get rid of, + this method allows to do so. + + If you supply 0.005, for instance, it means 1% of the extreme values will + be rejected (0.5% on each side of the distribution) + */ + template + void CopyPixelDataAndComputeMinMax(const std::string& pixelData); + + /** Service method for CopyPixelDataAndComputeMinMax*/ + template + void CopyPixelDataAndComputeDistribution( + const std::string& pixelData, + std::map& distribution); + + /** Service method for CopyPixelDataAndComputeMinMax*/ + template + void ComputeMinMaxWithOutlierRejection( + const std::map& distribution); + + void SetUncompressedPixelData(const std::string& pixelData); + + bool HasGeometry() const; + const OrthancStone::VolumeImageGeometry& GetImageGeometry() const; + + public: + OrthancMultiframeVolumeLoader(boost::shared_ptr volume, + OrthancStone::IOracle& oracle, + OrthancStone::IObservable& oracleObservable, + float outliersHalfRejectionRate = 0.0005); + + virtual ~OrthancMultiframeVolumeLoader(); + + bool IsPixelDataLoaded() const + { + return pixelDataLoaded_; + } + + void GetDistributionMinMax + (float& minValue, float& maxValue) const; + + void GetDistributionMinMaxWithOutliersRejection + (float& minValue, float& maxValue) const; + + void LoadInstance(const std::string& instanceId); + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Deprecated/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,513 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "OrthancSeriesVolumeProgressiveLoader.h" + +#include "../../Loaders/BasicFetchingItemsSorter.h" +#include "../../Loaders/BasicFetchingStrategy.h" +#include "../../Toolbox/GeometryToolbox.h" +#include "../../Volumes/DicomVolumeImageMPRSlicer.h" + +#include +#include + +namespace Deprecated +{ + class OrthancSeriesVolumeProgressiveLoader::ExtractedSlice : public OrthancStone::DicomVolumeImageMPRSlicer::Slice + { + private: + const OrthancSeriesVolumeProgressiveLoader& that_; + + public: + ExtractedSlice(const OrthancSeriesVolumeProgressiveLoader& that, + const OrthancStone::CoordinateSystem3D& plane) : + OrthancStone::DicomVolumeImageMPRSlicer::Slice(*that.volume_, plane), + that_(that) + { + if (IsValid()) + { + if (GetProjection() == OrthancStone::VolumeProjection_Axial) + { + // For coronal and sagittal projections, we take the global + // revision of the volume because even if a single slice changes, + // this means the projection will yield a different result --> + // we must increase the revision as soon as any slice changes + SetRevision(that_.seriesGeometry_.GetSliceRevision(GetSliceIndex())); + } + + if (that_.strategy_.get() != NULL && + GetProjection() == OrthancStone::VolumeProjection_Axial) + { + that_.strategy_->SetCurrent(GetSliceIndex()); + } + } + } + }; + + + + void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSlice(size_t index, + const OrthancStone::DicomInstanceParameters& reference) const + { + const OrthancStone::DicomInstanceParameters& slice = *slices_[index]; + + if (!OrthancStone::GeometryToolbox::IsParallel( + reference.GetGeometry().GetNormal(), + slice.GetGeometry().GetNormal())) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, + "A slice in the volume image is not parallel to the others"); + } + + if (reference.GetExpectedPixelFormat() != slice.GetExpectedPixelFormat()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat, + "The pixel format changes across the slices of the volume image"); + } + + if (reference.GetImageInformation().GetWidth() != slice.GetImageInformation().GetWidth() || + reference.GetImageInformation().GetHeight() != slice.GetImageInformation().GetHeight()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize, + "The width/height of slices are not constant in the volume image"); + } + + if (!OrthancStone::LinearAlgebra::IsNear(reference.GetPixelSpacingX(), slice.GetPixelSpacingX()) || + !OrthancStone::LinearAlgebra::IsNear(reference.GetPixelSpacingY(), slice.GetPixelSpacingY())) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, + "The pixel spacing of the slices change across the volume image"); + } + } + + + void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckVolume() const + { + for (size_t i = 0; i < slices_.size(); i++) + { + assert(slices_[i] != NULL); + if (slices_[i]->GetImageInformation().GetNumberOfFrames() != 1) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, + "This class does not support multi-frame images"); + } + } + + if (slices_.size() != 0) + { + const OrthancStone::DicomInstanceParameters& reference = *slices_[0]; + + for (size_t i = 1; i < slices_.size(); i++) + { + CheckSlice(i, reference); + } + } + } + + + void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::Clear() + { + for (size_t i = 0; i < slices_.size(); i++) + { + assert(slices_[i] != NULL); + delete slices_[i]; + } + + slices_.clear(); + slicesRevision_.clear(); + } + + + void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSliceIndex(size_t index) const + { + if (!HasGeometry()) + { + LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSliceIndex(size_t index): (!HasGeometry())"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else if (index >= slices_.size()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + assert(slices_.size() == GetImageGeometry().GetDepth() && + slices_.size() == slicesRevision_.size()); + } + } + + + // WARNING: The payload of "slices" must be of class "DicomInstanceParameters" + // (called with the slices created in LoadGeometry) + void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::ComputeGeometry(OrthancStone::SlicesSorter& slices) + { + Clear(); + + if (!slices.Sort()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, + "Cannot sort the 3D slices of a DICOM series"); + } + + if (slices.GetSlicesCount() == 0) + { + geometry_.reset(new OrthancStone::VolumeImageGeometry); + } + else + { + slices_.reserve(slices.GetSlicesCount()); + slicesRevision_.resize(slices.GetSlicesCount(), 0); + + for (size_t i = 0; i < slices.GetSlicesCount(); i++) + { + const OrthancStone::DicomInstanceParameters& slice = + dynamic_cast(slices.GetSlicePayload(i)); + slices_.push_back(new OrthancStone::DicomInstanceParameters(slice)); + } + + CheckVolume(); + + double spacingZ; + + if (slices.ComputeSpacingBetweenSlices(spacingZ)) + { + LOG(INFO) << "Computed spacing between slices: " << spacingZ << "mm"; + + const OrthancStone::DicomInstanceParameters& parameters = *slices_[0]; + + geometry_.reset(new OrthancStone::VolumeImageGeometry); + geometry_->SetSizeInVoxels(parameters.GetImageInformation().GetWidth(), + parameters.GetImageInformation().GetHeight(), + static_cast(slices.GetSlicesCount())); + geometry_->SetAxialGeometry(slices.GetSliceGeometry(0)); + geometry_->SetVoxelDimensions(parameters.GetPixelSpacingX(), + parameters.GetPixelSpacingY(), spacingZ); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, + "The origins of the slices of a volume image are not regularly spaced"); + } + } + } + + + const OrthancStone::VolumeImageGeometry& OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetImageGeometry() const + { + if (!HasGeometry()) + { + LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetImageGeometry(): (!HasGeometry())"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + assert(slices_.size() == geometry_->GetDepth()); + return *geometry_; + } + } + + + const OrthancStone::DicomInstanceParameters& OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetSliceParameters(size_t index) const + { + CheckSliceIndex(index); + return *slices_[index]; + } + + + uint64_t OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetSliceRevision(size_t index) const + { + CheckSliceIndex(index); + return slicesRevision_[index]; + } + + + void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::IncrementSliceRevision(size_t index) + { + CheckSliceIndex(index); + slicesRevision_[index] ++; + } + + + static unsigned int GetSliceIndexPayload(const OrthancStone::OracleCommandBase& command) + { + assert(command.HasPayload()); + return dynamic_cast< const Orthanc::SingleValueObject& >(command.GetPayload()).GetValue(); + } + + + void OrthancSeriesVolumeProgressiveLoader::ScheduleNextSliceDownload() + { + assert(strategy_.get() != NULL); + + unsigned int sliceIndex, quality; + + if (strategy_->GetNext(sliceIndex, quality)) + { + assert(quality <= BEST_QUALITY); + + const OrthancStone::DicomInstanceParameters& slice = seriesGeometry_.GetSliceParameters(sliceIndex); + + const std::string& instance = slice.GetOrthancInstanceIdentifier(); + if (instance.empty()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + std::auto_ptr command; + + if (quality == BEST_QUALITY) + { + std::auto_ptr tmp(new OrthancStone::GetOrthancImageCommand); + // TODO: review the following comment. + // - Commented out by bgo on 2019-07-19 | reason: Alain has seen cases + // where gzipping the uint16 image took 11 sec to produce 5mb. + // The unzipped request was much much faster. + // - Re-enabled on 2019-07-30. Reason: in Web Assembly, the browser + // does not use the Accept-Encoding header and always requests + // compression. Furthermore, NOT + tmp->SetHttpHeader("Accept-Encoding", "gzip"); + tmp->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam))); + tmp->SetInstanceUri(instance, slice.GetExpectedPixelFormat()); + tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat()); + command.reset(tmp.release()); + } + else + { + std::auto_ptr tmp(new OrthancStone::GetOrthancWebViewerJpegCommand); + // TODO: review the following comment. Commented out by bgo on 2019-07-19 + // (gzip for jpeg seems overkill) + //tmp->SetHttpHeader("Accept-Encoding", "gzip"); + tmp->SetInstance(instance); + tmp->SetQuality((quality == 0 ? 50 : 90)); + tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat()); + command.reset(tmp.release()); + } + + command->AcquirePayload(new Orthanc::SingleValueObject(sliceIndex)); + + boost::shared_ptr observer(GetSharedObserver()); + oracle_.Schedule(observer, command.release()); + } + else + { + // loading is finished! + volumeImageReadyInHighQuality_ = true; + BroadcastMessage(OrthancSeriesVolumeProgressiveLoader::VolumeImageReadyInHighQuality(*this)); + } + } + +/** + This is called in response to GET "/series/XXXXXXXXXXXXX/instances-tags" +*/ + void OrthancSeriesVolumeProgressiveLoader::LoadGeometry(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message) + { + Json::Value body; + message.ParseJsonBody(body); + + if (body.type() != Json::objectValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + + { + Json::Value::Members instances = body.getMemberNames(); + + OrthancStone::SlicesSorter slices; + + for (size_t i = 0; i < instances.size(); i++) + { + Orthanc::DicomMap dicom; + dicom.FromDicomAsJson(body[instances[i]]); + + std::auto_ptr instance(new OrthancStone::DicomInstanceParameters(dicom)); + instance->SetOrthancInstanceIdentifier(instances[i]); + + // the 3D plane corresponding to the slice + OrthancStone::CoordinateSystem3D geometry = instance->GetGeometry(); + slices.AddSlice(geometry, instance.release()); + } + + seriesGeometry_.ComputeGeometry(slices); + } + + size_t slicesCount = seriesGeometry_.GetImageGeometry().GetDepth(); + + if (slicesCount == 0) + { + volume_->Initialize(seriesGeometry_.GetImageGeometry(), Orthanc::PixelFormat_Grayscale8); + } + else + { + const OrthancStone::DicomInstanceParameters& parameters = seriesGeometry_.GetSliceParameters(0); + + volume_->Initialize(seriesGeometry_.GetImageGeometry(), parameters.GetExpectedPixelFormat()); + volume_->SetDicomParameters(parameters); + volume_->GetPixelData().Clear(); + + strategy_.reset(new OrthancStone::BasicFetchingStrategy(sorter_->CreateSorter(static_cast(slicesCount)), BEST_QUALITY)); + + assert(simultaneousDownloads_ != 0); + for (unsigned int i = 0; i < simultaneousDownloads_; i++) + { + ScheduleNextSliceDownload(); + } + } + + slicesQuality_.resize(slicesCount, 0); + + BroadcastMessage(OrthancStone::DicomVolumeImage::GeometryReadyMessage(*volume_)); + } + + + void OrthancSeriesVolumeProgressiveLoader::SetSliceContent(unsigned int sliceIndex, + const Orthanc::ImageAccessor& image, + unsigned int quality) + { + assert(sliceIndex < slicesQuality_.size() && + slicesQuality_.size() == volume_->GetPixelData().GetDepth()); + + if (quality >= slicesQuality_[sliceIndex]) + { + { + OrthancStone::ImageBuffer3D::SliceWriter writer(volume_->GetPixelData(), OrthancStone::VolumeProjection_Axial, sliceIndex); + Orthanc::ImageProcessing::Copy(writer.GetAccessor(), image); + } + + volume_->IncrementRevision(); + seriesGeometry_.IncrementSliceRevision(sliceIndex); + slicesQuality_[sliceIndex] = quality; + + BroadcastMessage(OrthancStone::DicomVolumeImage::ContentUpdatedMessage(*volume_)); + } + + ScheduleNextSliceDownload(); + } + + + void OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent(const OrthancStone::GetOrthancImageCommand::SuccessMessage& message) + { + SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), BEST_QUALITY); + } + + + void OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent(const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message) + { + unsigned int quality; + + switch (dynamic_cast(message.GetOrigin()).GetQuality()) + { + case 50: + quality = LOW_QUALITY; + break; + + case 90: + quality = MIDDLE_QUALITY; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), quality); + } + + + OrthancSeriesVolumeProgressiveLoader::OrthancSeriesVolumeProgressiveLoader(const boost::shared_ptr& volume, + OrthancStone::IOracle& oracle, + OrthancStone::IObservable& oracleObservable) : + oracle_(oracle), + active_(false), + simultaneousDownloads_(4), + volume_(volume), + sorter_(new OrthancStone::BasicFetchingItemsSorter::Factory), + volumeImageReadyInHighQuality_(false) + { + // TODO => Move this out of constructor + Register + (oracleObservable, &OrthancSeriesVolumeProgressiveLoader::LoadGeometry); + + Register + (oracleObservable, &OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent); + + Register + (oracleObservable, &OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent); + } + + OrthancSeriesVolumeProgressiveLoader::~OrthancSeriesVolumeProgressiveLoader() + { + LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::~OrthancSeriesVolumeProgressiveLoader()"; + } + + void OrthancSeriesVolumeProgressiveLoader::SetSimultaneousDownloads(unsigned int count) + { + if (active_) + { + LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::SetSimultaneousDownloads(): (active_)"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else if (count == 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + simultaneousDownloads_ = count; + } + } + + + void OrthancSeriesVolumeProgressiveLoader::LoadSeries(const std::string& seriesId) + { +// LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries seriesId=" << seriesId; + if (active_) + { +// LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries NOT ACTIVE! --> ERROR"; + LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries(const std::string& seriesId): (active_)"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + active_ = true; + + std::auto_ptr command(new OrthancStone::OrthancRestApiCommand); + command->SetUri("/series/" + seriesId + "/instances-tags"); + +// LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries about to call oracle_.Schedule"; + boost::shared_ptr observer(GetSharedObserver()); + oracle_.Schedule(observer, command.release()); +// LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries called oracle_.Schedule"; + } + } + + + OrthancStone::IVolumeSlicer::IExtractedSlice* + OrthancSeriesVolumeProgressiveLoader::ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane) + { + if (volume_->HasGeometry()) + { + return new ExtractedSlice(*this, cuttingPlane); + } + else + { + return new IVolumeSlicer::InvalidSlice; + } + } +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Loaders/OrthancSeriesVolumeProgressiveLoader.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Deprecated/Loaders/OrthancSeriesVolumeProgressiveLoader.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,163 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../../Loaders/IFetchingItemsSorter.h" +#include "../../Loaders/IFetchingStrategy.h" +#include "../../Messages/IObservable.h" +#include "../../Messages/ObserverBase.h" +#include "../../Oracle/GetOrthancImageCommand.h" +#include "../../Oracle/GetOrthancWebViewerJpegCommand.h" +#include "../../Oracle/IOracle.h" +#include "../../Oracle/OrthancRestApiCommand.h" +#include "../../Toolbox/SlicesSorter.h" +#include "../../Volumes/DicomVolumeImage.h" +#include "../../Volumes/IVolumeSlicer.h" + +#include + +namespace Deprecated +{ + /** + This class is used to manage the progressive loading of a volume that + is stored in a Dicom series. + */ + class OrthancSeriesVolumeProgressiveLoader : + public OrthancStone::ObserverBase, + public OrthancStone::IObservable, + public OrthancStone::IVolumeSlicer + { + private: + static const unsigned int LOW_QUALITY = 0; + static const unsigned int MIDDLE_QUALITY = 1; + static const unsigned int BEST_QUALITY = 2; + + class ExtractedSlice; + + /** Helper class internal to OrthancSeriesVolumeProgressiveLoader */ + class SeriesGeometry : public boost::noncopyable + { + private: + void CheckSlice(size_t index, + const OrthancStone::DicomInstanceParameters& reference) const; + + void CheckVolume() const; + + void Clear(); + + void CheckSliceIndex(size_t index) const; + + std::auto_ptr geometry_; + std::vector slices_; + std::vector slicesRevision_; + + public: + ~SeriesGeometry() + { + Clear(); + } + + void ComputeGeometry(OrthancStone::SlicesSorter& slices); + + virtual bool HasGeometry() const + { + return geometry_.get() != NULL; + } + + virtual const OrthancStone::VolumeImageGeometry& GetImageGeometry() const; + + const OrthancStone::DicomInstanceParameters& GetSliceParameters(size_t index) const; + + uint64_t GetSliceRevision(size_t index) const; + + void IncrementSliceRevision(size_t index); + }; + + void ScheduleNextSliceDownload(); + + void LoadGeometry(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message); + + void SetSliceContent(unsigned int sliceIndex, + const Orthanc::ImageAccessor& image, + unsigned int quality); + + void LoadBestQualitySliceContent(const OrthancStone::GetOrthancImageCommand::SuccessMessage& message); + + void LoadJpegSliceContent(const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message); + + OrthancStone::IOracle& oracle_; + bool active_; + unsigned int simultaneousDownloads_; + SeriesGeometry seriesGeometry_; + boost::shared_ptr volume_; + std::auto_ptr sorter_; + std::auto_ptr strategy_; + std::vector slicesQuality_; + bool volumeImageReadyInHighQuality_; + + + public: + ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, VolumeImageReadyInHighQuality, OrthancSeriesVolumeProgressiveLoader); + + + OrthancSeriesVolumeProgressiveLoader(const boost::shared_ptr& volume, + OrthancStone::IOracle& oracle, + OrthancStone::IObservable& oracleObservable); + + virtual ~OrthancSeriesVolumeProgressiveLoader(); + + void SetSimultaneousDownloads(unsigned int count); + + bool IsVolumeImageReadyInHighQuality() const + { + return volumeImageReadyInHighQuality_; + } + + void LoadSeries(const std::string& seriesId); + + /** + This getter is used by clients that do not receive the geometry through + subscribing, for instance if they are created or listening only AFTER the + "geometry loaded" message is broadcast + */ + bool HasGeometry() const + { + return seriesGeometry_.HasGeometry(); + } + + /** + Same remark as HasGeometry + */ + const OrthancStone::VolumeImageGeometry& GetImageGeometry() const + { + return seriesGeometry_.GetImageGeometry(); + } + + /** + When a slice is requested, the strategy algorithm (that defines the + sequence of resources to be loaded from the server) is modified to + take into account this request (this is done in the ExtractedSlice ctor) + */ + virtual IExtractedSlice* + ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE; + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Messages/LockingEmitter.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Deprecated/Messages/LockingEmitter.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,40 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + +#include "LockingEmitter.h" + +#include + +namespace Deprecated +{ + void LockingEmitter::EmitMessage(boost::weak_ptr observer, + const OrthancStone::IMessage& message) + { + try + { + boost::unique_lock lock(mutex_); + oracleObservable_.EmitMessage(observer, message); + } + catch (Orthanc::OrthancException& e) + { + LOG(ERROR) << "Exception while emitting a message: " << e.What(); + } + } +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Messages/LockingEmitter.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Deprecated/Messages/LockingEmitter.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,89 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + +#pragma once + +#include +#include + +#include "../../Messages/IMessageEmitter.h" +#include "../../Messages/IObservable.h" + +#include // For ORTHANC_OVERRIDE + +#include + +namespace Deprecated +{ + /** + * This class is used when using the ThreadedOracle : since messages + * can be sent from multiple Oracle threads, this IMessageEmitter + * implementation serializes the callbacks. + * + * The internal mutex used in Oracle messaging can also be used to + * protect the application data. Thus, this class can be used as a single + * application-wide mutex. + */ + class LockingEmitter : public OrthancStone::IMessageEmitter + { + private: + boost::shared_mutex mutex_; + OrthancStone::IObservable oracleObservable_; + + public: + virtual void EmitMessage(boost::weak_ptr observer, + const OrthancStone::IMessage& message) ORTHANC_OVERRIDE; + + + class ReaderLock : public boost::noncopyable + { + private: + LockingEmitter& that_; + boost::shared_lock lock_; + + public: + ReaderLock(LockingEmitter& that) : + that_(that), + lock_(that.mutex_) + { + } + }; + + + class WriterLock : public boost::noncopyable + { + private: + LockingEmitter& that_; + boost::unique_lock lock_; + + public: + WriterLock(LockingEmitter& that) : + that_(that), + lock_(that.mutex_) + { + } + + OrthancStone::IObservable& GetOracleObservable() + { + return that_.oracleObservable_; + } + }; + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/SmartLoader.cpp --- a/Framework/Deprecated/SmartLoader.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Deprecated/SmartLoader.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -21,7 +21,6 @@ #include "SmartLoader.h" -#include "../Messages/MessageForwarder.h" #include "../StoneException.h" #include "Core/Images/Image.h" #include "Core/Logging.h" @@ -68,11 +67,6 @@ CachedSliceStatus status_; public: - CachedSlice(OrthancStone::MessageBroker& broker) : - IVolumeSlicer(broker) - { - } - virtual ~CachedSlice() { } @@ -106,7 +100,7 @@ CachedSlice* Clone() const { - CachedSlice* output = new CachedSlice(GetBroker()); + CachedSlice* output = new CachedSlice; output->sliceIndex_ = sliceIndex_; output->slice_.reset(slice_->Clone()); output->image_ = image_; @@ -119,10 +113,7 @@ }; - SmartLoader::SmartLoader(OrthancStone::MessageBroker& broker, - OrthancApiClient& orthancApiClient) : - IObservable(broker), - IObserver(broker), + SmartLoader::SmartLoader(boost::shared_ptr orthancApiClient) : imageQuality_(SliceImageQuality_FullPam), orthancApiClient_(orthancApiClient) { @@ -140,7 +131,7 @@ // the messages to its observables // in both cases, we must be carefull about objects lifecycle !!! - std::auto_ptr layerSource; + boost::shared_ptr layerSource; std::string sliceKeyId = instanceId + ":" + boost::lexical_cast(frame); SmartLoader::CachedSlice* cachedSlice = NULL; @@ -151,22 +142,23 @@ } else { - layerSource.reset(new DicomSeriesVolumeSlicer(IObserver::GetBroker(), orthancApiClient_)); + layerSource.reset(new DicomSeriesVolumeSlicer); + dynamic_cast(layerSource.get())->Connect(orthancApiClient_); dynamic_cast(layerSource.get())->SetImageQuality(imageQuality_); - layerSource->RegisterObserverCallback(new OrthancStone::Callable(*this, &SmartLoader::OnLayerGeometryReady)); - layerSource->RegisterObserverCallback(new OrthancStone::Callable(*this, &SmartLoader::OnFrameReady)); - layerSource->RegisterObserverCallback(new OrthancStone::Callable(*this, &SmartLoader::OnLayerReady)); + Register(*layerSource, &SmartLoader::OnLayerGeometryReady); + Register(*layerSource, &SmartLoader::OnFrameReady); + Register(*layerSource, &SmartLoader::OnLayerReady); dynamic_cast(layerSource.get())->LoadFrame(instanceId, frame); } // make sure that the widget registers the events before we trigger them if (sliceViewer.GetLayerCount() == layerIndex) { - sliceViewer.AddLayer(layerSource.release()); + sliceViewer.AddLayer(layerSource); } else if (sliceViewer.GetLayerCount() > layerIndex) { - sliceViewer.ReplaceLayer(layerIndex, layerSource.release()); + sliceViewer.ReplaceLayer(layerIndex, layerSource); } else { @@ -190,7 +182,7 @@ // create the slice in the cache with "empty" data - boost::shared_ptr cachedSlice(new CachedSlice(IObserver::GetBroker())); + boost::shared_ptr cachedSlice(new CachedSlice); cachedSlice->slice_.reset(new Slice(instanceId, frame)); cachedSlice->status_ = CachedSliceStatus_ScheduledToLoad; std::string sliceKeyId = instanceId + ":" + boost::lexical_cast(frame); @@ -199,12 +191,12 @@ cachedSlices_[sliceKeyId] = boost::shared_ptr(cachedSlice); - std::auto_ptr layerSource(new DicomSeriesVolumeSlicer(IObserver::GetBroker(), orthancApiClient_)); - + std::auto_ptr layerSource(new DicomSeriesVolumeSlicer); + dynamic_cast(layerSource.get())->Connect(orthancApiClient_); dynamic_cast(layerSource.get())->SetImageQuality(imageQuality_); - layerSource->RegisterObserverCallback(new OrthancStone::Callable(*this, &SmartLoader::OnLayerGeometryReady)); - layerSource->RegisterObserverCallback(new OrthancStone::Callable(*this, &SmartLoader::OnFrameReady)); - layerSource->RegisterObserverCallback(new OrthancStone::Callable(*this, &SmartLoader::OnLayerReady)); + Register(*layerSource, &SmartLoader::OnLayerGeometryReady); + Register(*layerSource, &SmartLoader::OnFrameReady); + Register(*layerSource, &SmartLoader::OnLayerReady); dynamic_cast(layerSource.get())->LoadFrame(instanceId, frame); // keep a ref to the VolumeSlicer until the slice is fully loaded and saved to cache @@ -235,7 +227,7 @@ LOG(WARNING) << "Geometry ready: " << sliceKeyId; - boost::shared_ptr cachedSlice(new CachedSlice(IObserver::GetBroker())); + boost::shared_ptr cachedSlice(new CachedSlice); cachedSlice->slice_.reset(slice.Clone()); cachedSlice->effectiveQuality_ = source.GetImageQuality(); cachedSlice->status_ = CachedSliceStatus_GeometryLoaded; @@ -256,7 +248,7 @@ LOG(WARNING) << "Image ready: " << sliceKeyId; - boost::shared_ptr cachedSlice(new CachedSlice(IObserver::GetBroker())); + boost::shared_ptr cachedSlice(new CachedSlice); cachedSlice->image_.reset(Orthanc::Image::Clone(message.GetFrame())); cachedSlice->effectiveQuality_ = message.GetImageQuality(); cachedSlice->slice_.reset(message.GetSlice().Clone()); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/SmartLoader.h --- a/Framework/Deprecated/SmartLoader.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Deprecated/SmartLoader.h Tue Feb 04 15:24:03 2020 +0100 @@ -30,7 +30,7 @@ { class SliceViewerWidget; - class SmartLoader : public OrthancStone::IObservable, public OrthancStone::IObserver + class SmartLoader : public OrthancStone::IObservable, public OrthancStone::ObserverBase { class CachedSlice; @@ -42,10 +42,10 @@ PreloadingInstances preloadingInstances_; SliceImageQuality imageQuality_; - OrthancApiClient& orthancApiClient_; + boost::shared_ptr orthancApiClient_; public: - SmartLoader(OrthancStone::MessageBroker& broker, OrthancApiClient& orthancApiClient); // TODO: add maxPreloadStorageSizeInBytes + SmartLoader(boost::shared_ptr orthancApiClient); // TODO: add maxPreloadStorageSizeInBytes // void PreloadStudy(const std::string studyId); // void PreloadSeries(const std::string seriesId); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Toolbox/BaseWebService.cpp --- a/Framework/Deprecated/Toolbox/BaseWebService.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Deprecated/Toolbox/BaseWebService.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -37,13 +37,13 @@ class BaseWebService::BaseWebServicePayload : public Orthanc::IDynamicObject { private: - std::auto_ptr< OrthancStone::MessageHandler > userSuccessHandler_; - std::auto_ptr< OrthancStone::MessageHandler > userFailureHandler_; + std::auto_ptr< MessageHandler > userSuccessHandler_; + std::auto_ptr< MessageHandler > userFailureHandler_; std::auto_ptr< Orthanc::IDynamicObject> userPayload_; public: - BaseWebServicePayload(OrthancStone::MessageHandler* userSuccessHandler, - OrthancStone::MessageHandler* userFailureHandler, + BaseWebServicePayload(MessageHandler* userSuccessHandler, + MessageHandler* userFailureHandler, Orthanc::IDynamicObject* userPayload) : userSuccessHandler_(userSuccessHandler), userFailureHandler_(userFailureHandler), @@ -88,18 +88,18 @@ void BaseWebService::GetAsync(const std::string& uri, const HttpHeaders& headers, Orthanc::IDynamicObject* payload /* takes ownership */, - OrthancStone::MessageHandler* successCallback, - OrthancStone::MessageHandler* failureCallback, + MessageHandler* successCallback, + MessageHandler* failureCallback, unsigned int timeoutInSeconds) { if (!cacheEnabled_ || cache_.find(uri) == cache_.end()) { GetAsyncInternal(uri, headers, new BaseWebService::BaseWebServicePayload(successCallback, failureCallback, payload), // ownership is transfered - new OrthancStone::Callable - (*this, &BaseWebService::CacheAndNotifyHttpSuccess), - new OrthancStone::Callable - (*this, &BaseWebService::NotifyHttpError), + new DeprecatedCallable + (GetSharedObserver(), &BaseWebService::CacheAndNotifyHttpSuccess), + new DeprecatedCallable + (GetSharedObserver(), &BaseWebService::NotifyHttpError), timeoutInSeconds); } else diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Toolbox/BaseWebService.h --- a/Framework/Deprecated/Toolbox/BaseWebService.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Deprecated/Toolbox/BaseWebService.h Tue Feb 04 15:24:03 2020 +0100 @@ -22,6 +22,7 @@ #pragma once #include "IWebService.h" +#include "../../Messages/ObserverBase.h" #include #include @@ -31,7 +32,7 @@ { // This is an intermediate of IWebService that implements some caching on // the HTTP GET requests - class BaseWebService : public IWebService, public OrthancStone::IObserver + class BaseWebService : public IWebService, public OrthancStone::ObserverBase { public: class CachedHttpRequestSuccessMessage @@ -90,10 +91,7 @@ std::deque orderedCacheKeys_; public: - - BaseWebService(OrthancStone::MessageBroker& broker) : - IWebService(broker), - IObserver(broker), + BaseWebService() : cacheEnabled_(false), cacheCurrentSize_(0), cacheMaxSize_(100*1024*1024) @@ -112,21 +110,21 @@ virtual void GetAsync(const std::string& uri, const HttpHeaders& headers, Orthanc::IDynamicObject* payload /* takes ownership */, - OrthancStone::MessageHandler* successCallback, - OrthancStone::MessageHandler* failureCallback = NULL, + MessageHandler* successCallback, + MessageHandler* failureCallback = NULL, unsigned int timeoutInSeconds = 60); protected: virtual void GetAsyncInternal(const std::string& uri, const HttpHeaders& headers, Orthanc::IDynamicObject* payload /* takes ownership */, - OrthancStone::MessageHandler* successCallback, - OrthancStone::MessageHandler* failureCallback = NULL, + MessageHandler* successCallback, + MessageHandler* failureCallback = NULL, unsigned int timeoutInSeconds = 60) = 0; virtual void NotifyHttpSuccessLater(boost::shared_ptr cachedHttpMessage, Orthanc::IDynamicObject* payload, // takes ownership - OrthancStone::MessageHandler* successCallback) = 0; + MessageHandler* successCallback) = 0; private: void NotifyHttpSuccess(const IWebService::HttpRequestSuccessMessage& message); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Toolbox/IDelayedCallExecutor.h --- a/Framework/Deprecated/Toolbox/IDelayedCallExecutor.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Deprecated/Toolbox/IDelayedCallExecutor.h Tue Feb 04 15:24:03 2020 +0100 @@ -21,6 +21,7 @@ #pragma once +#include "IWebService.h" #include "../../Messages/IObserver.h" #include "../../Messages/ICallable.h" @@ -35,24 +36,14 @@ // The IDelayedCall executes a callback after a delay (equivalent to timeout() function in javascript). class IDelayedCallExecutor : public boost::noncopyable { - protected: - OrthancStone::MessageBroker& broker_; - public: ORTHANC_STONE_DEFINE_EMPTY_MESSAGE(__FILE__, __LINE__, TimeoutMessage); - IDelayedCallExecutor(OrthancStone::MessageBroker& broker) : - broker_(broker) - { - } - - virtual ~IDelayedCallExecutor() { } - - virtual void Schedule(OrthancStone::MessageHandler* callback, + virtual void Schedule(MessageHandler* callback, unsigned int timeoutInMs = 1000) = 0; }; } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Toolbox/IWebService.h --- a/Framework/Deprecated/Toolbox/IWebService.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Deprecated/Toolbox/IWebService.h Tue Feb 04 15:24:03 2020 +0100 @@ -33,6 +33,53 @@ namespace Deprecated { + template + class MessageHandler : public OrthancStone::ICallable + { + }; + + + template + class DeprecatedCallable : public MessageHandler + { + private: + typedef void (TObserver::* MemberMethod) (const TMessage&); + + boost::weak_ptr observer_; + MemberMethod function_; + + public: + DeprecatedCallable(boost::shared_ptr observer, + MemberMethod function) : + observer_(observer), + function_(function) + { + } + + virtual void Apply(const OrthancStone::IMessage& message) + { + boost::shared_ptr lock(observer_); + if (lock) + { + TObserver& observer = dynamic_cast(*lock); + const TMessage& typedMessage = dynamic_cast(message); + (observer.*function_) (typedMessage); + } + } + + virtual const OrthancStone::MessageIdentifier& GetMessageIdentifier() + { + return TMessage::GetStaticIdentifier(); + } + + virtual boost::weak_ptr GetObserver() const + { + return observer_; + } + }; + + // The IWebService performs HTTP requests. // Since applications can run in native or WASM environment and, since // in a WASM environment, the WebService is asynchronous, the IWebservice @@ -40,9 +87,6 @@ // and you'll be notified when the response/error is ready. class IWebService : public boost::noncopyable { - protected: - OrthancStone::MessageBroker& broker_; - public: typedef std::map HttpHeaders; @@ -138,12 +182,6 @@ }; - IWebService(OrthancStone::MessageBroker& broker) : - broker_(broker) - { - } - - virtual ~IWebService() { } @@ -153,23 +191,23 @@ virtual void GetAsync(const std::string& uri, const HttpHeaders& headers, Orthanc::IDynamicObject* payload /* takes ownership */, - OrthancStone::MessageHandler* successCallback, - OrthancStone::MessageHandler* failureCallback = NULL, + MessageHandler* successCallback, + MessageHandler* failureCallback = NULL, unsigned int timeoutInSeconds = 60) = 0; virtual void PostAsync(const std::string& uri, const HttpHeaders& headers, const std::string& body, Orthanc::IDynamicObject* payload /* takes ownership */, - OrthancStone::MessageHandler* successCallback, - OrthancStone::MessageHandler* failureCallback = NULL, + MessageHandler* successCallback, + MessageHandler* failureCallback = NULL, unsigned int timeoutInSeconds = 60) = 0; virtual void DeleteAsync(const std::string& uri, const HttpHeaders& headers, Orthanc::IDynamicObject* payload /* takes ownership */, - OrthancStone::MessageHandler* successCallback, - OrthancStone::MessageHandler* failureCallback = NULL, + MessageHandler* successCallback, + MessageHandler* failureCallback = NULL, unsigned int timeoutInSeconds = 60) = 0; }; } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Toolbox/OrthancApiClient.cpp --- a/Framework/Deprecated/Toolbox/OrthancApiClient.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Deprecated/Toolbox/OrthancApiClient.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -68,12 +68,11 @@ class OrthancApiClient::WebServicePayload : public Orthanc::IDynamicObject { private: - std::auto_ptr< OrthancStone::MessageHandler > emptyHandler_; - std::auto_ptr< OrthancStone::MessageHandler > jsonHandler_; - std::auto_ptr< OrthancStone::MessageHandler > binaryHandler_; - std::auto_ptr< OrthancStone::MessageHandler > failureHandler_; + std::auto_ptr< MessageHandler > emptyHandler_; + std::auto_ptr< MessageHandler > jsonHandler_; + std::auto_ptr< MessageHandler > binaryHandler_; + std::auto_ptr< MessageHandler > failureHandler_; std::auto_ptr< Orthanc::IDynamicObject > userPayload_; - OrthancStone::MessageBroker& broker_; void NotifyConversionError(const IWebService::HttpRequestSuccessMessage& message) const { if (failureHandler_.get() != NULL) @@ -84,14 +83,12 @@ } public: - WebServicePayload(OrthancStone::MessageBroker& broker, - OrthancStone::MessageHandler* handler, - OrthancStone::MessageHandler* failureHandler, + WebServicePayload(MessageHandler* handler, + MessageHandler* failureHandler, Orthanc::IDynamicObject* userPayload) : emptyHandler_(handler), failureHandler_(failureHandler), - userPayload_(userPayload), - broker_(broker) + userPayload_(userPayload) { if (handler == NULL) @@ -100,14 +97,12 @@ } } - WebServicePayload(OrthancStone::MessageBroker& broker, - OrthancStone::MessageHandler* handler, - OrthancStone::MessageHandler* failureHandler, + WebServicePayload(MessageHandler* handler, + MessageHandler* failureHandler, Orthanc::IDynamicObject* userPayload) : binaryHandler_(handler), failureHandler_(failureHandler), - userPayload_(userPayload), - broker_(broker) + userPayload_(userPayload) { if (handler == NULL) { @@ -115,14 +110,12 @@ } } - WebServicePayload(OrthancStone::MessageBroker& broker, - OrthancStone::MessageHandler* handler, - OrthancStone::MessageHandler* failureHandler, + WebServicePayload(MessageHandler* handler, + MessageHandler* failureHandler, Orthanc::IDynamicObject* userPayload) : jsonHandler_(handler), failureHandler_(failureHandler), - userPayload_(userPayload), - broker_(broker) + userPayload_(userPayload) { if (handler == NULL) { @@ -134,35 +127,26 @@ { if (emptyHandler_.get() != NULL) { - if (broker_.IsActive(*(emptyHandler_->GetObserver()))) - { - emptyHandler_->Apply(OrthancApiClient::EmptyResponseReadyMessage - (message.GetUri(), userPayload_.get())); - } + emptyHandler_->Apply(OrthancApiClient::EmptyResponseReadyMessage + (message.GetUri(), userPayload_.get())); } else if (binaryHandler_.get() != NULL) { - if (broker_.IsActive(*(binaryHandler_->GetObserver()))) - { - binaryHandler_->Apply(OrthancApiClient::BinaryResponseReadyMessage - (message.GetUri(), message.GetAnswer(), - message.GetAnswerSize(), userPayload_.get())); - } + binaryHandler_->Apply(OrthancApiClient::BinaryResponseReadyMessage + (message.GetUri(), message.GetAnswer(), + message.GetAnswerSize(), userPayload_.get())); } else if (jsonHandler_.get() != NULL) { - if (broker_.IsActive(*(jsonHandler_->GetObserver()))) + Json::Value response; + if (MessagingToolbox::ParseJson(response, message.GetAnswer(), message.GetAnswerSize())) { - Json::Value response; - if (MessagingToolbox::ParseJson(response, message.GetAnswer(), message.GetAnswerSize())) - { - jsonHandler_->Apply(OrthancApiClient::JsonResponseReadyMessage - (message.GetUri(), response, userPayload_.get())); - } - else - { - NotifyConversionError(message); - } + jsonHandler_->Apply(OrthancApiClient::JsonResponseReadyMessage + (message.GetUri(), response, userPayload_.get())); + } + else + { + NotifyConversionError(message); } } else @@ -182,11 +166,8 @@ }; - OrthancApiClient::OrthancApiClient(OrthancStone::MessageBroker& broker, - IWebService& web, + OrthancApiClient::OrthancApiClient(IWebService& web, const std::string& baseUrl) : - IObservable(broker), - IObserver(broker), web_(web), baseUrl_(baseUrl) { @@ -195,26 +176,26 @@ void OrthancApiClient::GetJsonAsync( const std::string& uri, - OrthancStone::MessageHandler* successCallback, - OrthancStone::MessageHandler* failureCallback, + MessageHandler* successCallback, + MessageHandler* failureCallback, Orthanc::IDynamicObject* payload) { IWebService::HttpHeaders emptyHeaders; web_.GetAsync(baseUrl_ + uri, emptyHeaders, - new WebServicePayload(IObservable::GetBroker(), successCallback, failureCallback, payload), - new OrthancStone::Callable - (*this, &OrthancApiClient::NotifyHttpSuccess), - new OrthancStone::Callable - (*this, &OrthancApiClient::NotifyHttpError)); + new WebServicePayload(successCallback, failureCallback, payload), + new DeprecatedCallable + (GetSharedObserver(), &OrthancApiClient::NotifyHttpSuccess), + new DeprecatedCallable + (GetSharedObserver(), &OrthancApiClient::NotifyHttpError)); } void OrthancApiClient::GetBinaryAsync( const std::string& uri, const std::string& contentType, - OrthancStone::MessageHandler* successCallback, - OrthancStone::MessageHandler* failureCallback, + MessageHandler* successCallback, + MessageHandler* failureCallback, Orthanc::IDynamicObject* payload) { IWebService::HttpHeaders headers; @@ -225,34 +206,34 @@ void OrthancApiClient::GetBinaryAsync( const std::string& uri, const IWebService::HttpHeaders& headers, - OrthancStone::MessageHandler* successCallback, - OrthancStone::MessageHandler* failureCallback, + MessageHandler* successCallback, + MessageHandler* failureCallback, Orthanc::IDynamicObject* payload) { // printf("GET [%s] [%s]\n", baseUrl_.c_str(), uri.c_str()); web_.GetAsync(baseUrl_ + uri, headers, - new WebServicePayload(IObservable::GetBroker(), successCallback, failureCallback, payload), - new OrthancStone::Callable - (*this, &OrthancApiClient::NotifyHttpSuccess), - new OrthancStone::Callable - (*this, &OrthancApiClient::NotifyHttpError)); + new WebServicePayload(successCallback, failureCallback, payload), + new DeprecatedCallable + (GetSharedObserver(), &OrthancApiClient::NotifyHttpSuccess), + new DeprecatedCallable + (GetSharedObserver(), &OrthancApiClient::NotifyHttpError)); } void OrthancApiClient::PostBinaryAsyncExpectJson( const std::string& uri, const std::string& body, - OrthancStone::MessageHandler* successCallback, - OrthancStone::MessageHandler* failureCallback, + MessageHandler* successCallback, + MessageHandler* failureCallback, Orthanc::IDynamicObject* payload) { web_.PostAsync(baseUrl_ + uri, IWebService::HttpHeaders(), body, - new WebServicePayload(IObservable::GetBroker(), successCallback, failureCallback, payload), - new OrthancStone::Callable - (*this, &OrthancApiClient::NotifyHttpSuccess), - new OrthancStone::Callable - (*this, &OrthancApiClient::NotifyHttpError)); + new WebServicePayload(successCallback, failureCallback, payload), + new DeprecatedCallable + (GetSharedObserver(), &OrthancApiClient::NotifyHttpSuccess), + new DeprecatedCallable + (GetSharedObserver(), &OrthancApiClient::NotifyHttpError)); } @@ -266,23 +247,23 @@ void OrthancApiClient::PostBinaryAsync( const std::string& uri, const std::string& body, - OrthancStone::MessageHandler* successCallback, - OrthancStone::MessageHandler* failureCallback, + MessageHandler* successCallback, + MessageHandler* failureCallback, Orthanc::IDynamicObject* payload /* takes ownership */) { web_.PostAsync(baseUrl_ + uri, IWebService::HttpHeaders(), body, - new WebServicePayload(IObservable::GetBroker(), successCallback, failureCallback, payload), - new OrthancStone::Callable - (*this, &OrthancApiClient::NotifyHttpSuccess), - new OrthancStone::Callable - (*this, &OrthancApiClient::NotifyHttpError)); + new WebServicePayload(successCallback, failureCallback, payload), + new DeprecatedCallable + (GetSharedObserver(), &OrthancApiClient::NotifyHttpSuccess), + new DeprecatedCallable + (GetSharedObserver(), &OrthancApiClient::NotifyHttpError)); } void OrthancApiClient::PostJsonAsyncExpectJson( const std::string& uri, const Json::Value& data, - OrthancStone::MessageHandler* successCallback, - OrthancStone::MessageHandler* failureCallback, + MessageHandler* successCallback, + MessageHandler* failureCallback, Orthanc::IDynamicObject* payload) { std::string body; @@ -302,8 +283,8 @@ void OrthancApiClient::PostJsonAsync( const std::string& uri, const Json::Value& data, - OrthancStone::MessageHandler* successCallback, - OrthancStone::MessageHandler* failureCallback, + MessageHandler* successCallback, + MessageHandler* failureCallback, Orthanc::IDynamicObject* payload /* takes ownership */) { std::string body; @@ -313,16 +294,16 @@ void OrthancApiClient::DeleteAsync( const std::string& uri, - OrthancStone::MessageHandler* successCallback, - OrthancStone::MessageHandler* failureCallback, + MessageHandler* successCallback, + MessageHandler* failureCallback, Orthanc::IDynamicObject* payload) { web_.DeleteAsync(baseUrl_ + uri, IWebService::HttpHeaders(), - new WebServicePayload(IObservable::GetBroker(), successCallback, failureCallback, payload), - new OrthancStone::Callable - (*this, &OrthancApiClient::NotifyHttpSuccess), - new OrthancStone::Callable - (*this, &OrthancApiClient::NotifyHttpError)); + new WebServicePayload(successCallback, failureCallback, payload), + new DeprecatedCallable + (GetSharedObserver(), &OrthancApiClient::NotifyHttpSuccess), + new DeprecatedCallable + (GetSharedObserver(), &OrthancApiClient::NotifyHttpError)); } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Toolbox/OrthancApiClient.h --- a/Framework/Deprecated/Toolbox/OrthancApiClient.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Deprecated/Toolbox/OrthancApiClient.h Tue Feb 04 15:24:03 2020 +0100 @@ -25,13 +25,24 @@ #include #include "IWebService.h" -#include "../../Messages/IObservable.h" +#include "../../Messages/ObserverBase.h" namespace Deprecated { + enum SliceImageQuality + { + SliceImageQuality_FullPng, // smaller to transmit but longer to generate on Orthanc side (better choice when on low bandwidth) + SliceImageQuality_FullPam, // bigger to transmit but faster to generate on Orthanc side (better choice when on localhost or LAN) + SliceImageQuality_Jpeg50, + SliceImageQuality_Jpeg90, + SliceImageQuality_Jpeg95, + + SliceImageQuality_InternalRaw // downloads the raw pixels data as they are stored in the DICOM file (internal use only) + }; + class OrthancApiClient : public OrthancStone::IObservable, - public OrthancStone::IObserver + public OrthancStone::ObserverBase { public: class JsonResponseReadyMessage : public OrthancStone::IMessage @@ -157,8 +168,7 @@ std::string baseUrl_; public: - OrthancApiClient(OrthancStone::MessageBroker& broker, - IWebService& web, + OrthancApiClient(IWebService& web, const std::string& baseUrl); virtual ~OrthancApiClient() @@ -169,36 +179,36 @@ // schedule a GET request expecting a JSON response. void GetJsonAsync(const std::string& uri, - OrthancStone::MessageHandler* successCallback, - OrthancStone::MessageHandler* failureCallback = NULL, + MessageHandler* successCallback, + MessageHandler* failureCallback = NULL, Orthanc::IDynamicObject* payload = NULL /* takes ownership */); // schedule a GET request expecting a binary response. void GetBinaryAsync(const std::string& uri, const std::string& contentType, - OrthancStone::MessageHandler* successCallback, - OrthancStone::MessageHandler* failureCallback = NULL, + MessageHandler* successCallback, + MessageHandler* failureCallback = NULL, Orthanc::IDynamicObject* payload = NULL /* takes ownership */); // schedule a GET request expecting a binary response. void GetBinaryAsync(const std::string& uri, const IWebService::HttpHeaders& headers, - OrthancStone::MessageHandler* successCallback, - OrthancStone::MessageHandler* failureCallback = NULL, + MessageHandler* successCallback, + MessageHandler* failureCallback = NULL, Orthanc::IDynamicObject* payload = NULL /* takes ownership */); // schedule a POST request expecting a JSON response. void PostBinaryAsyncExpectJson(const std::string& uri, const std::string& body, - OrthancStone::MessageHandler* successCallback, - OrthancStone::MessageHandler* failureCallback = NULL, + MessageHandler* successCallback, + MessageHandler* failureCallback = NULL, Orthanc::IDynamicObject* payload = NULL /* takes ownership */); // schedule a POST request expecting a JSON response. void PostJsonAsyncExpectJson(const std::string& uri, const Json::Value& data, - OrthancStone::MessageHandler* successCallback, - OrthancStone::MessageHandler* failureCallback = NULL, + MessageHandler* successCallback, + MessageHandler* failureCallback = NULL, Orthanc::IDynamicObject* payload = NULL /* takes ownership */); // schedule a POST request and don't mind the response. @@ -208,8 +218,8 @@ // schedule a POST request and don't expect any response. void PostJsonAsync(const std::string& uri, const Json::Value& data, - OrthancStone::MessageHandler* successCallback, - OrthancStone::MessageHandler* failureCallback = NULL, + MessageHandler* successCallback, + MessageHandler* failureCallback = NULL, Orthanc::IDynamicObject* payload = NULL /* takes ownership */); @@ -220,14 +230,14 @@ // schedule a POST request and don't expect any response. void PostBinaryAsync(const std::string& uri, const std::string& body, - OrthancStone::MessageHandler* successCallback, - OrthancStone::MessageHandler* failureCallback = NULL, + MessageHandler* successCallback, + MessageHandler* failureCallback = NULL, Orthanc::IDynamicObject* payload = NULL /* takes ownership */); // schedule a DELETE request expecting an empty response. void DeleteAsync(const std::string& uri, - OrthancStone::MessageHandler* successCallback, - OrthancStone::MessageHandler* failureCallback = NULL, + MessageHandler* successCallback, + MessageHandler* failureCallback = NULL, Orthanc::IDynamicObject* payload = NULL /* takes ownership */); void NotifyHttpSuccess(const IWebService::HttpRequestSuccessMessage& message); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Toolbox/OrthancSlicesLoader.cpp --- a/Framework/Deprecated/Toolbox/OrthancSlicesLoader.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Deprecated/Toolbox/OrthancSlicesLoader.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -639,10 +639,7 @@ } - OrthancSlicesLoader::OrthancSlicesLoader(OrthancStone::MessageBroker& broker, - OrthancApiClient& orthanc) : - OrthancStone::IObservable(broker), - OrthancStone::IObserver(broker), + OrthancSlicesLoader::OrthancSlicesLoader(boost::shared_ptr orthanc) : orthanc_(orthanc), state_(State_Initialization) { @@ -658,10 +655,10 @@ else { state_ = State_LoadingGeometry; - orthanc_.GetJsonAsync("/series/" + seriesId + "/instances-tags", - new OrthancStone::Callable(*this, &OrthancSlicesLoader::ParseSeriesGeometry), - new OrthancStone::Callable(*this, &OrthancSlicesLoader::OnGeometryError), - NULL); + orthanc_->GetJsonAsync("/series/" + seriesId + "/instances-tags", + new DeprecatedCallable(GetSharedObserver(), &OrthancSlicesLoader::ParseSeriesGeometry), + new DeprecatedCallable(GetSharedObserver(), &OrthancSlicesLoader::OnGeometryError), + NULL); } } @@ -677,10 +674,10 @@ // Tag "3004-000c" is "Grid Frame Offset Vector", which is // mandatory to read RT DOSE, but is too long to be returned by default - orthanc_.GetJsonAsync("/instances/" + instanceId + "/tags?ignore-length=3004-000c", - new OrthancStone::Callable(*this, &OrthancSlicesLoader::ParseInstanceGeometry), - new OrthancStone::Callable(*this, &OrthancSlicesLoader::OnGeometryError), - Operation::DownloadInstanceGeometry(instanceId)); + orthanc_->GetJsonAsync("/instances/" + instanceId + "/tags?ignore-length=3004-000c", + new DeprecatedCallable(GetSharedObserver(), &OrthancSlicesLoader::ParseInstanceGeometry), + new DeprecatedCallable(GetSharedObserver(), &OrthancSlicesLoader::OnGeometryError), + Operation::DownloadInstanceGeometry(instanceId)); } } @@ -696,10 +693,10 @@ { state_ = State_LoadingGeometry; - orthanc_.GetJsonAsync("/instances/" + instanceId + "/tags", - new OrthancStone::Callable(*this, &OrthancSlicesLoader::ParseFrameGeometry), - new OrthancStone::Callable(*this, &OrthancSlicesLoader::OnGeometryError), - Operation::DownloadFrameGeometry(instanceId, frame)); + orthanc_->GetJsonAsync("/instances/" + instanceId + "/tags", + new DeprecatedCallable(GetSharedObserver(), &OrthancSlicesLoader::ParseFrameGeometry), + new DeprecatedCallable(GetSharedObserver(), &OrthancSlicesLoader::OnGeometryError), + Operation::DownloadFrameGeometry(instanceId, frame)); } } @@ -770,23 +767,23 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } - orthanc_.GetBinaryAsync(uri, "image/png", - new OrthancStone::Callable - (*this, &OrthancSlicesLoader::ParseSliceImagePng), - new OrthancStone::Callable - (*this, &OrthancSlicesLoader::OnSliceImageError), - Operation::DownloadSliceImage( - static_cast(index), slice, SliceImageQuality_FullPng)); -} + orthanc_->GetBinaryAsync(uri, "image/png", + new DeprecatedCallable + (GetSharedObserver(), &OrthancSlicesLoader::ParseSliceImagePng), + new DeprecatedCallable + (GetSharedObserver(), &OrthancSlicesLoader::OnSliceImageError), + Operation::DownloadSliceImage( + static_cast(index), slice, SliceImageQuality_FullPng)); + } void OrthancSlicesLoader::ScheduleSliceImagePam(const Slice& slice, size_t index) { std::string uri = ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" + - boost::lexical_cast(slice.GetFrame())); + boost::lexical_cast(slice.GetFrame())); switch (slice.GetConverter().GetExpectedPixelFormat()) { @@ -806,15 +803,15 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } - orthanc_.GetBinaryAsync(uri, "image/x-portable-arbitrarymap", - new OrthancStone::Callable - (*this, &OrthancSlicesLoader::ParseSliceImagePam), - new OrthancStone::Callable - (*this, &OrthancSlicesLoader::OnSliceImageError), - Operation::DownloadSliceImage(static_cast(index), - slice, SliceImageQuality_FullPam)); + orthanc_->GetBinaryAsync(uri, "image/x-portable-arbitrarymap", + new DeprecatedCallable + (GetSharedObserver(), &OrthancSlicesLoader::ParseSliceImagePam), + new DeprecatedCallable + (GetSharedObserver(), &OrthancSlicesLoader::OnSliceImageError), + Operation::DownloadSliceImage(static_cast(index), + slice, SliceImageQuality_FullPam)); } @@ -849,15 +846,15 @@ "-" + slice.GetOrthancInstanceId() + "_" + boost::lexical_cast(slice.GetFrame())); - orthanc_.GetJsonAsync(uri, - new OrthancStone::Callable - (*this, &OrthancSlicesLoader::ParseSliceImageJpeg), - new OrthancStone::Callable - (*this, &OrthancSlicesLoader::OnSliceImageError), - Operation::DownloadSliceImage( - static_cast(index), slice, quality)); + orthanc_->GetJsonAsync(uri, + new DeprecatedCallable + (GetSharedObserver(), &OrthancSlicesLoader::ParseSliceImageJpeg), + new DeprecatedCallable + (GetSharedObserver(), &OrthancSlicesLoader::OnSliceImageError), + Operation::DownloadSliceImage( + static_cast(index), slice, quality)); } @@ -890,15 +887,15 @@ { std::string uri = ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" + boost::lexical_cast(slice.GetFrame()) + "/raw.gz"); - orthanc_.GetBinaryAsync(uri, IWebService::HttpHeaders(), - new OrthancStone::Callable - (*this, &OrthancSlicesLoader::ParseSliceRawImage), - new OrthancStone::Callable - (*this, &OrthancSlicesLoader::OnSliceImageError), - Operation::DownloadSliceRawImage( - static_cast(index), slice)); + orthanc_->GetBinaryAsync(uri, IWebService::HttpHeaders(), + new DeprecatedCallable + (GetSharedObserver(), &OrthancSlicesLoader::ParseSliceRawImage), + new DeprecatedCallable + (GetSharedObserver(), &OrthancSlicesLoader::OnSliceImageError), + Operation::DownloadSliceRawImage( + static_cast(index), slice)); } } } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Toolbox/OrthancSlicesLoader.h --- a/Framework/Deprecated/Toolbox/OrthancSlicesLoader.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Deprecated/Toolbox/OrthancSlicesLoader.h Tue Feb 04 15:24:03 2020 +0100 @@ -22,6 +22,7 @@ #pragma once #include "../../Messages/IObservable.h" +#include "../../Messages/ObserverBase.h" #include "../../StoneEnumerations.h" #include "../../Toolbox/SlicesSorter.h" #include "IWebService.h" @@ -33,7 +34,9 @@ namespace Deprecated { - class OrthancSlicesLoader : public OrthancStone::IObservable, public OrthancStone::IObserver + class OrthancSlicesLoader : + public OrthancStone::IObservable, + public OrthancStone::ObserverBase { public: ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, SliceGeometryReadyMessage, OrthancSlicesLoader); @@ -143,7 +146,7 @@ class Operation; - OrthancApiClient& orthanc_; + boost::shared_ptr orthanc_; State state_; OrthancStone::SlicesSorter slices_; @@ -183,9 +186,8 @@ void SortAndFinalizeSlices(); public: - OrthancSlicesLoader(OrthancStone::MessageBroker& broker, - //ISliceLoaderObserver& callback, - OrthancApiClient& orthancApi); + OrthancSlicesLoader(//ISliceLoaderObserver& callback, + boost::shared_ptr orthancApi); void ScheduleLoadSeries(const std::string& seriesId); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Viewport/IViewport.h --- a/Framework/Deprecated/Viewport/IViewport.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Deprecated/Viewport/IViewport.h Tue Feb 04 15:24:03 2020 +0100 @@ -37,15 +37,6 @@ public: ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, ViewportChangedMessage, IViewport); - IViewport(OrthancStone::MessageBroker& broker) : - IObservable(broker) - { - } - - virtual ~IViewport() - { - } - virtual void FitContent() = 0; virtual void SetStatusBar(IStatusBar& statusBar) = 0; diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Viewport/WidgetViewport.cpp --- a/Framework/Deprecated/Viewport/WidgetViewport.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Deprecated/Viewport/WidgetViewport.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -26,8 +26,7 @@ namespace Deprecated { - WidgetViewport::WidgetViewport(OrthancStone::MessageBroker& broker) : - IViewport(broker), + WidgetViewport::WidgetViewport() : statusBar_(NULL), isMouseOver_(false), lastMouseX_(0), @@ -57,7 +56,7 @@ } - IWidget& WidgetViewport::SetCentralWidget(IWidget* widget) + void WidgetViewport::SetCentralWidget(boost::shared_ptr widget) { if (widget == NULL) { @@ -66,7 +65,7 @@ mouseTracker_.reset(NULL); - centralWidget_.reset(widget); + centralWidget_ = widget; centralWidget_->SetViewport(*this); if (statusBar_ != NULL) @@ -75,8 +74,6 @@ } NotifyBackgroundChanged(); - - return *widget; } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Viewport/WidgetViewport.h --- a/Framework/Deprecated/Viewport/WidgetViewport.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Deprecated/Viewport/WidgetViewport.h Tue Feb 04 15:24:03 2020 +0100 @@ -31,7 +31,7 @@ class WidgetViewport : public IViewport { private: - std::auto_ptr centralWidget_; + boost::shared_ptr centralWidget_; IStatusBar* statusBar_; std::auto_ptr mouseTracker_; bool isMouseOver_; @@ -41,13 +41,13 @@ bool backgroundChanged_; public: - WidgetViewport(OrthancStone::MessageBroker& broker); + WidgetViewport(); virtual void FitContent(); virtual void SetStatusBar(IStatusBar& statusBar); - IWidget& SetCentralWidget(IWidget* widget); // Takes ownership + void SetCentralWidget(boost::shared_ptr widget); virtual void NotifyBackgroundChanged(); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Volumes/ISlicedVolume.h --- a/Framework/Deprecated/Volumes/ISlicedVolume.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Deprecated/Volumes/ISlicedVolume.h Tue Feb 04 15:24:03 2020 +0100 @@ -65,11 +65,6 @@ }; - ISlicedVolume(OrthancStone::MessageBroker& broker) : - IObservable(broker) - { - } - virtual size_t GetSliceCount() const = 0; virtual const Slice& GetSlice(size_t slice) const = 0; diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Volumes/IVolumeLoader.h --- a/Framework/Deprecated/Volumes/IVolumeLoader.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Deprecated/Volumes/IVolumeLoader.h Tue Feb 04 15:24:03 2020 +0100 @@ -31,10 +31,5 @@ ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, GeometryReadyMessage, IVolumeLoader); ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, GeometryErrorMessage, IVolumeLoader); ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, ContentChangedMessage, IVolumeLoader); - - IVolumeLoader(OrthancStone::MessageBroker& broker) : - IObservable(broker) - { - } }; } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Volumes/StructureSetLoader.cpp --- a/Framework/Deprecated/Volumes/StructureSetLoader.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Deprecated/Volumes/StructureSetLoader.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -27,10 +27,7 @@ namespace Deprecated { - StructureSetLoader::StructureSetLoader(OrthancStone::MessageBroker& broker, - OrthancApiClient& orthanc) : - IVolumeLoader(broker), - IObserver(broker), + StructureSetLoader::StructureSetLoader(OrthancApiClient& orthanc) : orthanc_(orthanc) { } @@ -60,7 +57,7 @@ it != instances.end(); ++it) { orthanc_.PostBinaryAsyncExpectJson("/tools/lookup", *it, - new OrthancStone::Callable(*this, &StructureSetLoader::OnLookupCompleted)); + new DeprecatedCallable(GetSharedObserver(), &StructureSetLoader::OnLookupCompleted)); } BroadcastMessage(GeometryReadyMessage(*this)); @@ -84,7 +81,7 @@ const std::string& instance = lookup[0]["ID"].asString(); orthanc_.GetJsonAsync("/instances/" + instance + "/tags", - new OrthancStone::Callable(*this, &StructureSetLoader::OnReferencedSliceLoaded)); + new DeprecatedCallable(GetSharedObserver(), &StructureSetLoader::OnReferencedSliceLoaded)); } @@ -97,7 +94,7 @@ else { orthanc_.GetJsonAsync("/instances/" + instance + "/tags?ignore-length=3006-0050", - new OrthancStone::Callable(*this, &StructureSetLoader::OnStructureSetLoaded)); + new DeprecatedCallable(GetSharedObserver(), &StructureSetLoader::OnStructureSetLoaded)); } } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Volumes/StructureSetLoader.h --- a/Framework/Deprecated/Volumes/StructureSetLoader.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Deprecated/Volumes/StructureSetLoader.h Tue Feb 04 15:24:03 2020 +0100 @@ -21,6 +21,7 @@ #pragma once +#include "../../Messages/ObserverBase.h" #include "../../Toolbox/DicomStructureSet.h" #include "../Toolbox/OrthancApiClient.h" #include "IVolumeLoader.h" @@ -29,7 +30,7 @@ { class StructureSetLoader : public IVolumeLoader, - public OrthancStone::IObserver + public OrthancStone::ObserverBase { private: OrthancApiClient& orthanc_; @@ -42,8 +43,7 @@ void OnLookupCompleted(const OrthancApiClient::JsonResponseReadyMessage& message); public: - StructureSetLoader(OrthancStone::MessageBroker& broker, - OrthancApiClient& orthanc); + StructureSetLoader(OrthancApiClient& orthanc); void ScheduleLoadInstance(const std::string& instance); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Widgets/LayoutWidget.cpp --- a/Framework/Deprecated/Widgets/LayoutWidget.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Deprecated/Widgets/LayoutWidget.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -85,14 +85,14 @@ class LayoutWidget::ChildWidget : public boost::noncopyable { private: - std::auto_ptr widget_; + boost::shared_ptr widget_; int left_; int top_; unsigned int width_; unsigned int height_; public: - ChildWidget(IWidget* widget) : + ChildWidget(boost::shared_ptr widget) : widget_(widget) { assert(widget != NULL); @@ -354,7 +354,7 @@ } - IWidget& LayoutWidget::AddWidget(IWidget* widget) // Takes ownership + void LayoutWidget::AddWidget(boost::shared_ptr widget) // Takes ownership { if (widget == NULL) { @@ -375,8 +375,6 @@ { hasAnimation_ = true; } - - return *widget; } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Widgets/LayoutWidget.h --- a/Framework/Deprecated/Widgets/LayoutWidget.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Deprecated/Widgets/LayoutWidget.h Tue Feb 04 15:24:03 2020 +0100 @@ -94,7 +94,7 @@ return paddingInternal_; } - IWidget& AddWidget(IWidget* widget); // Takes ownership + void AddWidget(boost::shared_ptr widget); virtual void SetStatusBar(IStatusBar& statusBar); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Widgets/SliceViewerWidget.cpp --- a/Framework/Deprecated/Widgets/SliceViewerWidget.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Deprecated/Widgets/SliceViewerWidget.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -250,7 +250,7 @@ { index = found->second; assert(index < layers_.size() && - layers_[index] == &layer); + layers_[index].get() == &layer); return true; } } @@ -364,42 +364,27 @@ } - SliceViewerWidget::SliceViewerWidget(OrthancStone::MessageBroker& broker, - const std::string& name) : + SliceViewerWidget::SliceViewerWidget(const std::string& name) : WorldSceneWidget(name), - IObserver(broker), - IObservable(broker), started_(false) { SetBackgroundCleared(true); } - SliceViewerWidget::~SliceViewerWidget() - { - for (size_t i = 0; i < layers_.size(); i++) - { - delete layers_[i]; - } - } - void SliceViewerWidget::ObserveLayer(IVolumeSlicer& layer) { - layer.RegisterObserverCallback(new OrthancStone::Callable - (*this, &SliceViewerWidget::OnGeometryReady)); - // currently ignore errors layer->RegisterObserverCallback(new Callable(*this, &SliceViewerWidget::...)); - layer.RegisterObserverCallback(new OrthancStone::Callable - (*this, &SliceViewerWidget::OnSliceChanged)); - layer.RegisterObserverCallback(new OrthancStone::Callable - (*this, &SliceViewerWidget::OnContentChanged)); - layer.RegisterObserverCallback(new OrthancStone::Callable - (*this, &SliceViewerWidget::OnLayerReady)); - layer.RegisterObserverCallback(new OrthancStone::Callable - (*this, &SliceViewerWidget::OnLayerError)); + // currently ignoring errors of type IVolumeSlicer::GeometryErrorMessage + + Register(layer, &SliceViewerWidget::OnGeometryReady); + Register(layer, &SliceViewerWidget::OnSliceChanged); + Register(layer, &SliceViewerWidget::OnContentChanged); + Register(layer, &SliceViewerWidget::OnLayerReady); + Register(layer, &SliceViewerWidget::OnLayerError); } - size_t SliceViewerWidget::AddLayer(IVolumeSlicer* layer) // Takes ownership + size_t SliceViewerWidget::AddLayer(boost::shared_ptr layer) { if (layer == NULL) { @@ -409,7 +394,7 @@ size_t index = layers_.size(); layers_.push_back(layer); styles_.push_back(RenderStyle()); - layersIndex_[layer] = index; + layersIndex_[layer.get()] = index; ResetPendingScene(); @@ -421,7 +406,8 @@ } - void SliceViewerWidget::ReplaceLayer(size_t index, IVolumeSlicer* layer) // Takes ownership + void SliceViewerWidget::ReplaceLayer(size_t index, + boost::shared_ptr layer) { if (layer == NULL) { @@ -433,9 +419,8 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } - delete layers_[index]; layers_[index] = layer; - layersIndex_[layer] = index; + layersIndex_[layer.get()] = index; ResetPendingScene(); @@ -452,13 +437,13 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } - IVolumeSlicer* previousLayer = layers_[index]; + IVolumeSlicer* previousLayer = layers_[index].get(); layersIndex_.erase(layersIndex_.find(previousLayer)); layers_.erase(layers_.begin() + index); changedLayers_.erase(changedLayers_.begin() + index); styles_.erase(styles_.begin() + index); - delete layers_[index]; + layers_[index].reset(); currentScene_->DeleteLayer(index); ResetPendingScene(); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Deprecated/Widgets/SliceViewerWidget.h --- a/Framework/Deprecated/Widgets/SliceViewerWidget.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Deprecated/Widgets/SliceViewerWidget.h Tue Feb 04 15:24:03 2020 +0100 @@ -24,7 +24,7 @@ #include "WorldSceneWidget.h" #include "../Layers/IVolumeSlicer.h" #include "../../Toolbox/Extent2D.h" -#include "../../Messages/IObserver.h" +#include "../../Messages/ObserverBase.h" #include @@ -32,7 +32,7 @@ { class SliceViewerWidget : public WorldSceneWidget, - public OrthancStone::IObserver, + public OrthancStone::ObserverBase, public OrthancStone::IObservable { public: @@ -72,7 +72,7 @@ bool started_; LayersIndex layersIndex_; - std::vector layers_; + std::vector > layers_; std::vector styles_; OrthancStone::CoordinateSystem3D plane_; std::auto_ptr currentScene_; @@ -100,8 +100,7 @@ void ResetChangedLayers(); public: - SliceViewerWidget(OrthancStone::MessageBroker& broker, - const std::string& name); + SliceViewerWidget(const std::string& name); virtual OrthancStone::Extent2D GetSceneExtent(); @@ -120,11 +119,13 @@ void InvalidateLayer(size_t layer); public: - virtual ~SliceViewerWidget(); + virtual ~SliceViewerWidget() + { + } - size_t AddLayer(IVolumeSlicer* layer); // Takes ownership + size_t AddLayer(boost::shared_ptr layer); - void ReplaceLayer(size_t layerIndex, IVolumeSlicer* layer); // Takes ownership + void ReplaceLayer(size_t layerIndex, boost::shared_ptr layer); // Takes ownership void RemoveLayer(size_t layerIndex); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Fonts/GlyphTextureAlphabet.cpp --- a/Framework/Fonts/GlyphTextureAlphabet.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Fonts/GlyphTextureAlphabet.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -28,7 +28,7 @@ #include #include -#ifdef __EMSCRIPTEN__ +#if defined(__EMSCRIPTEN__) /* Avoid this error: .../boost/math/special_functions/round.hpp:86:12: warning: implicit conversion from 'std::__2::numeric_limits::type' (aka 'int') to 'float' changes value from 2147483647 to 2147483648 [-Wimplicit-int-float-conversion] diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/DicomResourcesLoader.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/DicomResourcesLoader.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,905 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "DicomResourcesLoader.h" + +#if !defined(ORTHANC_ENABLE_DCMTK) +# error The macro ORTHANC_ENABLE_DCMTK must be defined +#endif + +#if ORTHANC_ENABLE_DCMTK == 1 +# include "../Oracle/ParseDicomFromFileCommand.h" +# include +#endif + +#include + +namespace OrthancStone +{ + static std::string GetUri(Orthanc::ResourceType level) + { + switch (level) + { + case Orthanc::ResourceType_Patient: + return "patients"; + + case Orthanc::ResourceType_Study: + return "studies"; + + case Orthanc::ResourceType_Series: + return "series"; + + case Orthanc::ResourceType_Instance: + return "instances"; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + + + class DicomResourcesLoader::Handler : public Orthanc::IDynamicObject + { + private: + boost::shared_ptr loader_; + boost::shared_ptr target_; + int priority_; + DicomSource source_; + boost::shared_ptr userPayload_; + + public: + Handler(boost::shared_ptr loader, + boost::shared_ptr target, + int priority, + const DicomSource& source, + boost::shared_ptr userPayload) : + loader_(loader), + target_(target), + priority_(priority), + source_(source), + userPayload_(userPayload) + { + if (!loader || + !target) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + } + + virtual ~Handler() + { + } + + void BroadcastSuccess() + { + SuccessMessage message(*loader_, target_, priority_, source_, userPayload_.get()); + loader_->BroadcastMessage(message); + } + + boost::shared_ptr GetLoader() + { + assert(loader_); + return loader_; + } + + boost::shared_ptr GetTarget() + { + assert(target_); + return target_; + } + + int GetPriority() const + { + return priority_; + } + + const DicomSource& GetSource() const + { + return source_; + } + + const boost::shared_ptr GetUserPayload() const + { + return userPayload_; + } + }; + + + class DicomResourcesLoader::StringHandler : public DicomResourcesLoader::Handler + { + public: + StringHandler(boost::shared_ptr loader, + boost::shared_ptr target, + int priority, + const DicomSource& source, + boost::shared_ptr userPayload) : + Handler(loader, target, priority, source, userPayload) + { + } + + virtual void HandleJson(const Json::Value& body) = 0; + + virtual void HandleString(const std::string& body) + { + Json::Reader reader; + Json::Value value; + if (reader.parse(body, value)) + { + HandleJson(value); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + } + }; + + + class DicomResourcesLoader::DicomWebHandler : public StringHandler + { + public: + DicomWebHandler(boost::shared_ptr loader, + boost::shared_ptr target, + int priority, + const DicomSource& source, + boost::shared_ptr userPayload) : + StringHandler(loader, target, priority, source, userPayload) + { + } + + virtual void HandleJson(const Json::Value& body) + { + GetTarget()->AddFromDicomWeb(body); + BroadcastSuccess(); + } + }; + + + class DicomResourcesLoader::OrthancHandler : public StringHandler + { + private: + boost::shared_ptr remainingCommands_; + + protected: + void CloseCommand() + { + assert(remainingCommands_); + + if (*remainingCommands_ == 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + (*remainingCommands_) --; + + if (*remainingCommands_ == 0) + { + BroadcastSuccess(); + } + } + + public: + OrthancHandler(boost::shared_ptr loader, + boost::shared_ptr target, + int priority, + const DicomSource& source, + boost::shared_ptr remainingCommands, + boost::shared_ptr userPayload) : + StringHandler(loader, target, priority, source, userPayload), + remainingCommands_(remainingCommands) + { + if (!remainingCommands) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + (*remainingCommands) ++; + } + + boost::shared_ptr GetRemainingCommands() + { + assert(remainingCommands_); + return remainingCommands_; + } + }; + + + class DicomResourcesLoader::OrthancInstanceTagsHandler : public OrthancHandler + { + public: + OrthancInstanceTagsHandler(boost::shared_ptr loader, + boost::shared_ptr target, + int priority, + const DicomSource& source, + boost::shared_ptr remainingCommands, + boost::shared_ptr userPayload) : + OrthancHandler(loader, target, priority, source, remainingCommands, userPayload) + { + } + + virtual void HandleJson(const Json::Value& body) + { + GetTarget()->AddFromOrthanc(body); + CloseCommand(); + } + }; + + + class DicomResourcesLoader::OrthancOneChildInstanceHandler : public OrthancHandler + { + public: + OrthancOneChildInstanceHandler(boost::shared_ptr loader, + boost::shared_ptr target, + int priority, + const DicomSource& source, + boost::shared_ptr remainingCommands, + boost::shared_ptr userPayload) : + OrthancHandler(loader, target, priority, source, remainingCommands, userPayload) + { + } + + virtual void HandleJson(const Json::Value& body) + { + static const char* const ID = "ID"; + + if (body.type() == Json::arrayValue) + { + if (body.size() > 0) + { + if (body[0].type() == Json::objectValue && + body[0].isMember(ID) && + body[0][ID].type() == Json::stringValue) + { + GetLoader()->ScheduleLoadOrthancInstanceTags + (GetTarget(), GetPriority(), GetSource(), body[0][ID].asString(), GetRemainingCommands(), GetUserPayload()); + CloseCommand(); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + } + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + } + }; + + + class DicomResourcesLoader::OrthancAllChildrenInstancesHandler : public OrthancHandler + { + private: + Orthanc::ResourceType bottomLevel_; + + public: + OrthancAllChildrenInstancesHandler(boost::shared_ptr loader, + boost::shared_ptr target, + int priority, + const DicomSource& source, + boost::shared_ptr remainingCommands, + Orthanc::ResourceType bottomLevel, + boost::shared_ptr userPayload) : + OrthancHandler(loader, target, priority, source, remainingCommands, userPayload), + bottomLevel_(bottomLevel) + { + } + + virtual void HandleJson(const Json::Value& body) + { + static const char* const ID = "ID"; + static const char* const INSTANCES = "Instances"; + + if (body.type() == Json::arrayValue) + { + for (Json::Value::ArrayIndex i = 0; i < body.size(); i++) + { + switch (bottomLevel_) + { + case Orthanc::ResourceType_Patient: + case Orthanc::ResourceType_Study: + if (body[i].type() == Json::objectValue && + body[i].isMember(ID) && + body[i][ID].type() == Json::stringValue) + { + GetLoader()->ScheduleLoadOrthancOneChildInstance + (GetTarget(), GetPriority(), GetSource(), bottomLevel_, + body[i][ID].asString(), GetRemainingCommands(), GetUserPayload()); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + + break; + + case Orthanc::ResourceType_Series: + // At the series level, avoid a call to + // "/series/.../instances", as we already have this + // information in the JSON + if (body[i].type() == Json::objectValue && + body[i].isMember(INSTANCES) && + body[i][INSTANCES].type() == Json::arrayValue) + { + if (body[i][INSTANCES].size() > 0) + { + if (body[i][INSTANCES][0].type() == Json::stringValue) + { + GetLoader()->ScheduleLoadOrthancInstanceTags + (GetTarget(), GetPriority(), GetSource(), + body[i][INSTANCES][0].asString(), GetRemainingCommands(), GetUserPayload()); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + } + } + + break; + + case Orthanc::ResourceType_Instance: + if (body[i].type() == Json::objectValue && + body[i].isMember(ID) && + body[i][ID].type() == Json::stringValue) + { + GetLoader()->ScheduleLoadOrthancInstanceTags + (GetTarget(), GetPriority(), GetSource(), + body[i][ID].asString(), GetRemainingCommands(), GetUserPayload()); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + } + + CloseCommand(); + } + }; + + +#if ORTHANC_ENABLE_DCMTK == 1 + static void ExploreDicomDir(OrthancStone::LoadedDicomResources& instances, + const Orthanc::ParsedDicomDir& dicomDir, + Orthanc::ResourceType level, + size_t index, + const Orthanc::DicomMap& parent) + { + std::string expectedType; + + switch (level) + { + case Orthanc::ResourceType_Patient: + expectedType = "PATIENT"; + break; + + case Orthanc::ResourceType_Study: + expectedType = "STUDY"; + break; + + case Orthanc::ResourceType_Series: + expectedType = "SERIES"; + break; + + case Orthanc::ResourceType_Instance: + expectedType = "IMAGE"; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + for (;;) + { + std::auto_ptr current(dicomDir.GetItem(index).Clone()); + current->RemoveBinaryTags(); + current->Merge(parent); + + std::string type; + if (!current->LookupStringValue(type, Orthanc::DICOM_TAG_DIRECTORY_RECORD_TYPE, false)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + if (type == expectedType) + { + if (level == Orthanc::ResourceType_Instance) + { + instances.AddResource(*current); + } + else + { + size_t lower; + if (dicomDir.LookupLower(lower, index)) + { + ExploreDicomDir(instances, dicomDir, Orthanc::GetChildResourceType(level), lower, *current); + } + } + } + + size_t next; + if (dicomDir.LookupNext(next, index)) + { + index = next; + } + else + { + return; + } + } + } +#endif + + +#if ORTHANC_ENABLE_DCMTK == 1 + void DicomResourcesLoader::GetDicomDirInstances(LoadedDicomResources& target, + const Orthanc::ParsedDicomDir& dicomDir) + { + Orthanc::DicomMap parent; + ExploreDicomDir(target, dicomDir, Orthanc::ResourceType_Patient, 0, parent); + } +#endif + + +#if ORTHANC_ENABLE_DCMTK == 1 + class DicomResourcesLoader::DicomDirHandler : public StringHandler + { + public: + DicomDirHandler(boost::shared_ptr loader, + boost::shared_ptr target, + int priority, + const DicomSource& source, + boost::shared_ptr userPayload) : + StringHandler(loader, target, priority, source, userPayload) + { + } + + virtual void HandleJson(const Json::Value& body) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + virtual void HandleString(const std::string& body) + { + Orthanc::ParsedDicomDir dicomDir(body); + GetDicomDirInstances(*GetTarget(), dicomDir); + BroadcastSuccess(); + } + }; +#endif + + + void DicomResourcesLoader::Handle(const HttpCommand::SuccessMessage& message) + { + if (message.GetOrigin().HasPayload()) + { + dynamic_cast(message.GetOrigin().GetPayload()).HandleString(message.GetAnswer()); + } + } + + + void DicomResourcesLoader::Handle(const OrthancRestApiCommand::SuccessMessage& message) + { + if (message.GetOrigin().HasPayload()) + { + dynamic_cast(message.GetOrigin().GetPayload()).HandleString(message.GetAnswer()); + } + } + + + void DicomResourcesLoader::Handle(const ReadFileCommand::SuccessMessage& message) + { + if (message.GetOrigin().HasPayload()) + { + dynamic_cast(message.GetOrigin().GetPayload()).HandleString(message.GetContent()); + } + } + + +#if ORTHANC_ENABLE_DCMTK == 1 + void DicomResourcesLoader::Handle(const ParseDicomSuccessMessage& message) + { + if (message.GetOrigin().HasPayload()) + { + Handler& handler = dynamic_cast(message.GetOrigin().GetPayload()); + + std::set ignoreTagLength; + ignoreTagLength.insert(Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR); // Needed for RT-DOSE + + Orthanc::DicomMap summary; + message.GetDicom().ExtractDicomSummary(summary, ignoreTagLength); + handler.GetTarget()->AddResource(summary); + + handler.BroadcastSuccess(); + } + } +#endif + + + void DicomResourcesLoader::Handle(const OracleCommandExceptionMessage& message) + { + // TODO + LOG(ERROR) << "Exception: " << message.GetException().What(); + } + + + void DicomResourcesLoader::ScheduleLoadOrthancInstanceTags(boost::shared_ptr target, + int priority, + const DicomSource& source, + const std::string& instanceId, + boost::shared_ptr remainingCommands, + boost::shared_ptr userPayload) + { + std::auto_ptr command(new OrthancRestApiCommand); + command->SetUri("/instances/" + instanceId + "/tags"); + command->AcquirePayload(new OrthancInstanceTagsHandler(shared_from_this(), target, priority, + source, remainingCommands, userPayload)); + + { + std::auto_ptr lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } + } + + + void DicomResourcesLoader::ScheduleLoadOrthancOneChildInstance(boost::shared_ptr target, + int priority, + const DicomSource& source, + Orthanc::ResourceType level, + const std::string& id, + boost::shared_ptr remainingCommands, + boost::shared_ptr userPayload) + { + std::auto_ptr command(new OrthancRestApiCommand); + command->SetUri("/" + GetUri(level) + "/" + id + "/instances"); + command->AcquirePayload(new OrthancOneChildInstanceHandler(shared_from_this(), target, priority, + source, remainingCommands, userPayload)); + + { + std::auto_ptr lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } + } + + + + const Orthanc::IDynamicObject& DicomResourcesLoader::SuccessMessage::GetUserPayload() const + { + if (userPayload_ == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + return *userPayload_; + } + } + + + boost::shared_ptr DicomResourcesLoader::Factory::Create(ILoadersContext::ILock& stone) + { + boost::shared_ptr result(new DicomResourcesLoader(stone.GetContext())); + result->Register(stone.GetOracleObservable(), &DicomResourcesLoader::Handle); + result->Register(stone.GetOracleObservable(), &DicomResourcesLoader::Handle); + result->Register(stone.GetOracleObservable(), &DicomResourcesLoader::Handle); + result->Register(stone.GetOracleObservable(), &DicomResourcesLoader::Handle); + +#if ORTHANC_ENABLE_DCMTK == 1 + result->Register(stone.GetOracleObservable(), &DicomResourcesLoader::Handle); +#endif + + return boost::shared_ptr(result); + } + + + static void SetIncludeTags(std::map& arguments, + const std::set& includeTags) + { + if (!includeTags.empty()) + { + std::string s; + bool first = true; + + for (std::set::const_iterator + it = includeTags.begin(); it != includeTags.end(); ++it) + { + if (first) + { + first = false; + } + else + { + s += ","; + } + + char buf[16]; + sprintf(buf, "%04X%04X", it->GetGroup(), it->GetElement()); + s += std::string(buf); + } + + arguments["includefield"] = s; + } + } + + + void DicomResourcesLoader::ScheduleGetDicomWeb(boost::shared_ptr target, + int priority, + const DicomSource& source, + const std::string& uri, + const std::set& includeTags, + Orthanc::IDynamicObject* userPayload) + { + boost::shared_ptr protection(userPayload); + + if (!source.IsDicomWeb()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, "Not a DICOMweb source"); + } + + std::map arguments, headers; + SetIncludeTags(arguments, includeTags); + + std::auto_ptr command( + source.CreateDicomWebCommand(uri, arguments, headers, + new DicomWebHandler(shared_from_this(), target, priority, source, protection))); + + { + std::auto_ptr lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } + } + + + void DicomResourcesLoader::ScheduleQido(boost::shared_ptr target, + int priority, + const DicomSource& source, + Orthanc::ResourceType level, + const Orthanc::DicomMap& filter, + const std::set& includeTags, + Orthanc::IDynamicObject* userPayload) + { + boost::shared_ptr protection(userPayload); + + if (!source.IsDicomWeb()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, "Not a DICOMweb source"); + } + + std::string uri; + switch (level) + { + case Orthanc::ResourceType_Study: + uri = "/studies"; + break; + + case Orthanc::ResourceType_Series: + uri = "/series"; + break; + + case Orthanc::ResourceType_Instance: + uri = "/instances"; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + std::set tags; + filter.GetTags(tags); + + std::map arguments, headers; + + for (std::set::const_iterator it = tags.begin(); it != tags.end(); ++it) + { + std::string s; + if (filter.LookupStringValue(s, *it, false /* no binary */)) + { + char buf[16]; + sprintf(buf, "%04X%04X", it->GetGroup(), it->GetElement()); + arguments[buf] = s; + } + } + + SetIncludeTags(arguments, includeTags); + + std::auto_ptr command( + source.CreateDicomWebCommand(uri, arguments, headers, + new DicomWebHandler(shared_from_this(), target, priority, source, protection))); + + + { + std::auto_ptr lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } + } + + + void DicomResourcesLoader::ScheduleLoadOrthancResources(boost::shared_ptr target, + int priority, + const DicomSource& source, + Orthanc::ResourceType topLevel, + const std::string& topId, + Orthanc::ResourceType bottomLevel, + Orthanc::IDynamicObject* userPayload) + { + boost::shared_ptr protection(userPayload); + + if (!source.IsOrthanc()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, "Not an Orthanc source"); + } + + bool ok = false; + + switch (topLevel) + { + case Orthanc::ResourceType_Patient: + ok = (bottomLevel == Orthanc::ResourceType_Patient || + bottomLevel == Orthanc::ResourceType_Study || + bottomLevel == Orthanc::ResourceType_Series || + bottomLevel == Orthanc::ResourceType_Instance); + break; + + case Orthanc::ResourceType_Study: + ok = (bottomLevel == Orthanc::ResourceType_Study || + bottomLevel == Orthanc::ResourceType_Series || + bottomLevel == Orthanc::ResourceType_Instance); + break; + + case Orthanc::ResourceType_Series: + ok = (bottomLevel == Orthanc::ResourceType_Series || + bottomLevel == Orthanc::ResourceType_Instance); + break; + + case Orthanc::ResourceType_Instance: + ok = (bottomLevel == Orthanc::ResourceType_Instance); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + if (!ok) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + boost::shared_ptr remainingCommands(new unsigned int(0)); + + if (topLevel == Orthanc::ResourceType_Instance) + { + ScheduleLoadOrthancInstanceTags(target, priority, source, topId, remainingCommands, protection); + } + else if (topLevel == bottomLevel) + { + ScheduleLoadOrthancOneChildInstance(target, priority, source, topLevel, topId, remainingCommands, protection); + } + else + { + std::auto_ptr command(new OrthancRestApiCommand); + command->SetUri("/" + GetUri(topLevel) + "/" + topId + "/" + GetUri(bottomLevel)); + command->AcquirePayload(new OrthancAllChildrenInstancesHandler + (shared_from_this(), target, priority, source, + remainingCommands, bottomLevel, protection)); + + { + std::auto_ptr lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } + } + } + + + void DicomResourcesLoader::ScheduleLoadDicomDir(boost::shared_ptr target, + int priority, + const DicomSource& source, + const std::string& path, + Orthanc::IDynamicObject* userPayload) + { + boost::shared_ptr protection(userPayload); + + if (!source.IsDicomDir()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, "Not a DICOMDIR source"); + } + + if (target->GetIndexedTag() == Orthanc::DICOM_TAG_SOP_INSTANCE_UID) + { + LOG(WARNING) << "If loading DICOMDIR, it is advised to index tag " + << "ReferencedSopInstanceUidInFile (0004,1511)"; + } + +#if ORTHANC_ENABLE_DCMTK == 1 + std::auto_ptr command(new ReadFileCommand(path)); + command->AcquirePayload(new DicomDirHandler(shared_from_this(), target, priority, source, protection)); + + { + std::auto_ptr lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } +#else + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, + "DCMTK is disabled, cannot load DICOMDIR"); +#endif + } + + + void DicomResourcesLoader::ScheduleLoadDicomFile(boost::shared_ptr target, + int priority, + const DicomSource& source, + const std::string& path, + bool includePixelData, + Orthanc::IDynamicObject* userPayload) + { + boost::shared_ptr protection(userPayload); + +#if ORTHANC_ENABLE_DCMTK == 1 + std::auto_ptr command(new ParseDicomFromFileCommand(path)); + command->SetPixelDataIncluded(includePixelData); + command->AcquirePayload(new Handler(shared_from_this(), target, priority, source, protection)); + + { + std::auto_ptr lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } +#else + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, + "DCMTK is disabled, cannot load DICOM files"); +#endif + } + + + bool DicomResourcesLoader::ScheduleLoadDicomFile(boost::shared_ptr target, + int priority, + const DicomSource& source, + const std::string& dicomDirPath, + const Orthanc::DicomMap& dicomDirEntry, + bool includePixelData, + Orthanc::IDynamicObject* userPayload) + { + std::auto_ptr protection(userPayload); + +#if ORTHANC_ENABLE_DCMTK == 1 + std::string file; + if (dicomDirEntry.LookupStringValue(file, Orthanc::DICOM_TAG_REFERENCED_FILE_ID, false)) + { + ScheduleLoadDicomFile(target, priority, source, ParseDicomFromFileCommand::GetDicomDirPath(dicomDirPath, file), + includePixelData, protection.release()); + return true; + } + else + { + return false; + } +#else + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, + "DCMTK is disabled, cannot load DICOM files"); +#endif + } +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/DicomResourcesLoader.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/DicomResourcesLoader.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,220 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#if !defined(ORTHANC_ENABLE_DCMTK) +# error The macro ORTHANC_ENABLE_DCMTK must be defined +#endif + +#if ORTHANC_ENABLE_DCMTK == 1 +# include "../Oracle/ParseDicomFromFileCommand.h" +# include +#endif + +#include "../Oracle/HttpCommand.h" +#include "../Oracle/OracleCommandExceptionMessage.h" +#include "../Oracle/OrthancRestApiCommand.h" +#include "../Oracle/ReadFileCommand.h" +#include "DicomSource.h" +#include "ILoaderFactory.h" +#include "LoadedDicomResources.h" +#include "OracleScheduler.h" + +namespace OrthancStone +{ + class DicomResourcesLoader : + public ObserverBase, + public IObservable + { + private: + class Handler; + class StringHandler; + class DicomWebHandler; + class OrthancHandler; + class OrthancInstanceTagsHandler; + class OrthancOneChildInstanceHandler; + class OrthancAllChildrenInstancesHandler; + +#if ORTHANC_ENABLE_DCMTK == 1 + class DicomDirHandler; +#endif + + void Handle(const HttpCommand::SuccessMessage& message); + + void Handle(const OrthancRestApiCommand::SuccessMessage& message); + + void Handle(const ReadFileCommand::SuccessMessage& message); + + void Handle(const OracleCommandExceptionMessage& message); + +#if ORTHANC_ENABLE_DCMTK == 1 + void Handle(const ParseDicomSuccessMessage& message); +#endif + + void ScheduleLoadOrthancInstanceTags(boost::shared_ptr target, + int priority, + const DicomSource& source, + const std::string& instanceId, + boost::shared_ptr remainingCommands, + boost::shared_ptr userPayload); + + void ScheduleLoadOrthancOneChildInstance(boost::shared_ptr target, + int priority, + const DicomSource& source, + Orthanc::ResourceType level, + const std::string& id, + boost::shared_ptr remainingCommands, + boost::shared_ptr userPayload); + + DicomResourcesLoader(ILoadersContext& context) : + context_(context) + { + } + + ILoadersContext& context_; + + + public: + class SuccessMessage : public OrthancStone::OriginMessage + { + ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); + + private: + boost::shared_ptr resources_; + int priority_; + const DicomSource& source_; + const Orthanc::IDynamicObject* userPayload_; + + public: + SuccessMessage(const DicomResourcesLoader& origin, + boost::shared_ptr resources, + int priority, + const DicomSource& source, + const Orthanc::IDynamicObject* userPayload) : + OriginMessage(origin), + resources_(resources), + priority_(priority), + source_(source), + userPayload_(userPayload) + { + } + + int GetPriority() const + { + return priority_; + } + + const boost::shared_ptr GetResources() const + { + return resources_; + } + + const DicomSource& GetDicomSource() const + { + return source_; + } + + bool HasUserPayload() const + { + return userPayload_ != NULL; + } + + const Orthanc::IDynamicObject& GetUserPayload() const; + }; + + + class Factory : public ILoaderFactory + { + public: + virtual boost::shared_ptr Create(ILoadersContext::ILock& stone); + }; + + void ScheduleGetDicomWeb(boost::shared_ptr target, + int priority, + const DicomSource& source, + const std::string& uri, + const std::set& includeTags, + Orthanc::IDynamicObject* userPayload); + + void ScheduleGetDicomWeb(boost::shared_ptr target, + int priority, + const DicomSource& source, + const std::string& uri, + Orthanc::IDynamicObject* userPayload) + { + std::set includeTags; + ScheduleGetDicomWeb(target, priority, source, uri, includeTags, userPayload); + } + + void ScheduleQido(boost::shared_ptr target, + int priority, + const DicomSource& source, + Orthanc::ResourceType level, + const Orthanc::DicomMap& filter, + const std::set& includeTags, + Orthanc::IDynamicObject* userPayload); + + void ScheduleLoadOrthancResources(boost::shared_ptr target, + int priority, + const DicomSource& source, + Orthanc::ResourceType topLevel, + const std::string& topId, + Orthanc::ResourceType bottomLevel, + Orthanc::IDynamicObject* userPayload); + + void ScheduleLoadOrthancResource(boost::shared_ptr target, + int priority, + const DicomSource& source, + Orthanc::ResourceType level, + const std::string& id, + Orthanc::IDynamicObject* userPayload) + { + ScheduleLoadOrthancResources(target, priority, source, level, id, level, userPayload); + } + +#if ORTHANC_ENABLE_DCMTK == 1 + static void GetDicomDirInstances(LoadedDicomResources& target, + const Orthanc::ParsedDicomDir& dicomDir); +#endif + + void ScheduleLoadDicomDir(boost::shared_ptr target, + int priority, + const DicomSource& source, + const std::string& path, + Orthanc::IDynamicObject* userPayload); + + void ScheduleLoadDicomFile(boost::shared_ptr target, + int priority, + const DicomSource& source, + const std::string& path, + bool includePixelData, + Orthanc::IDynamicObject* userPayload); + + bool ScheduleLoadDicomFile(boost::shared_ptr target, + int priority, + const DicomSource& source, + const std::string& dicomDirPath, + const Orthanc::DicomMap& dicomDirEntry, + bool includePixelData, + Orthanc::IDynamicObject* userPayload); + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/DicomSource.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/DicomSource.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,356 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "DicomSource.h" + +#include "../Oracle/HttpCommand.h" +#include "../Oracle/OrthancRestApiCommand.h" + +#include + +#include + +namespace OrthancStone +{ + static std::string EncodeGetArguments(const std::string& uri, + const std::map& arguments) + { + std::string s = uri; + bool first = true; + + for (std::map::const_iterator + it = arguments.begin(); it != arguments.end(); ++it) + { + if (first) + { + s += "?"; + first = false; + } + else + { + s += "&"; + } + + s += it->first + "=" + it->second; + } + + // TODO: Call Orthanc::Toolbox::UriEncode() ? + + return s; + } + + + void DicomSource::SetOrthancSource(const Orthanc::WebServiceParameters& parameters) + { + type_ = DicomSourceType_Orthanc; + webService_ = parameters; + hasOrthancWebViewer1_ = false; + hasOrthancAdvancedPreview_ = false; + } + + + void DicomSource::SetOrthancSource() + { + Orthanc::WebServiceParameters parameters; + parameters.SetUrl("http://localhost:8042/"); + SetOrthancSource(parameters); + } + + + const Orthanc::WebServiceParameters& DicomSource::GetOrthancParameters() const + { + if (type_ == DicomSourceType_Orthanc || + type_ == DicomSourceType_DicomWebThroughOrthanc) + { + return webService_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + void DicomSource::SetDicomDirSource() + { + type_ = DicomSourceType_DicomDir; + } + + + void DicomSource::SetDicomWebSource(const std::string& baseUrl) + { + type_ = DicomSourceType_DicomWeb; + webService_.SetUrl(baseUrl); + webService_.ClearCredentials(); + } + + + void DicomSource::SetDicomWebSource(const std::string& baseUrl, + const std::string& username, + const std::string& password) + { + type_ = DicomSourceType_DicomWeb; + webService_.SetUrl(baseUrl); + webService_.SetCredentials(username, password); + } + + + void DicomSource::SetDicomWebThroughOrthancSource(const Orthanc::WebServiceParameters& orthancParameters, + const std::string& dicomWebRoot, + const std::string& serverName) + { + type_ = DicomSourceType_DicomWebThroughOrthanc; + webService_ = orthancParameters; + orthancDicomWebRoot_ = dicomWebRoot; + serverName_ = serverName; + } + + + void DicomSource::SetDicomWebThroughOrthancSource(const std::string& serverName) + { + Orthanc::WebServiceParameters orthanc; + orthanc.SetUrl("http://localhost:8042/"); + SetDicomWebThroughOrthancSource(orthanc, "/dicom-web/", serverName); + } + + + bool DicomSource::IsDicomWeb() const + { + return (type_ == DicomSourceType_DicomWeb || + type_ == DicomSourceType_DicomWebThroughOrthanc); + } + + + IOracleCommand* DicomSource::CreateDicomWebCommand(const std::string& uri, + const std::map& arguments, + const std::map& headers, + Orthanc::IDynamicObject* payload) const + { + std::auto_ptr protection(payload); + + switch (type_) + { + case DicomSourceType_DicomWeb: + { + std::auto_ptr command(new HttpCommand); + + command->SetMethod(Orthanc::HttpMethod_Get); + command->SetUrl(webService_.GetUrl() + "/" + EncodeGetArguments(uri, arguments)); + command->SetHttpHeaders(webService_.GetHttpHeaders()); + + for (std::map::const_iterator + it = headers.begin(); it != headers.end(); ++it) + { + command->SetHttpHeader(it->first, it->second); + } + + if (!webService_.GetUsername().empty()) + { + command->SetCredentials(webService_.GetUsername(), webService_.GetPassword()); + } + + if (protection.get()) + { + command->AcquirePayload(protection.release()); + } + + return command.release(); + } + + case DicomSourceType_DicomWebThroughOrthanc: + { + Json::Value args = Json::objectValue; + for (std::map::const_iterator + it = arguments.begin(); it != arguments.end(); ++it) + { + args[it->first] = it->second; + } + + Json::Value h = Json::objectValue; + for (std::map::const_iterator + it = headers.begin(); it != headers.end(); ++it) + { + h[it->first] = it->second; + } + + Json::Value body = Json::objectValue; + body["Uri"] = uri; + body["Arguments"] = args; + body["Headers"] = h; + + std::auto_ptr command(new OrthancRestApiCommand); + command->SetMethod(Orthanc::HttpMethod_Post); + command->SetUri(orthancDicomWebRoot_ + "/servers/" + serverName_ + "/get"); + command->SetBody(body); + + if (protection.get()) + { + command->AcquirePayload(protection.release()); + } + + return command.release(); + } + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + void DicomSource::AutodetectOrthancFeatures(const std::string& system, + const std::string& plugins) + { + static const char* const REST_API_VERSION = "ApiVersion"; + + if (IsDicomWeb()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + Json::Value a, b; + Json::Reader reader; + if (reader.parse(system, a) && + reader.parse(plugins, b) && + a.type() == Json::objectValue && + b.type() == Json::arrayValue && + a.isMember(REST_API_VERSION) && + a[REST_API_VERSION].type() == Json::intValue) + { + SetOrthancAdvancedPreview(a[REST_API_VERSION].asInt() >= 5); + + hasOrthancWebViewer1_ = false; + + for (Json::Value::ArrayIndex i = 0; i < b.size(); i++) + { + if (b[i].type() != Json::stringValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + if (boost::iequals(b[i].asString(), "web-viewer")) + { + hasOrthancWebViewer1_ = true; + } + } + } + else + { + printf("[%s] [%s]\n", system.c_str(), plugins.c_str()); + + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + } + + + void DicomSource::SetOrthancWebViewer1(bool hasPlugin) + { + if (IsOrthanc()) + { + hasOrthancWebViewer1_ = hasPlugin; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + bool DicomSource::HasOrthancWebViewer1() const + { + if (IsOrthanc()) + { + return hasOrthancWebViewer1_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + void DicomSource::SetOrthancAdvancedPreview(bool hasFeature) + { + if (IsOrthanc()) + { + hasOrthancAdvancedPreview_ = hasFeature; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + bool DicomSource::HasOrthancAdvancedPreview() const + { + if (IsOrthanc()) + { + return hasOrthancAdvancedPreview_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + void DicomSource::SetDicomWebRendered(bool hasFeature) + { + if (IsDicomWeb()) + { + hasDicomWebRendered_ = hasFeature; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + bool DicomSource::HasDicomWebRendered() const + { + if (IsDicomWeb()) + { + return hasDicomWebRendered_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + unsigned int DicomSource::GetQualityCount() const + { + if (IsDicomWeb()) + { + return (HasDicomWebRendered() ? 2 : 1); + } + else if (IsOrthanc()) + { + return (HasOrthancWebViewer1() || + HasOrthancAdvancedPreview() ? 2 : 1); + } + else if (IsDicomDir()) + { + return 1; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/DicomSource.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/DicomSource.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,118 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../Oracle/IOracleCommand.h" + +#include + +namespace OrthancStone +{ + enum DicomSourceType + { + DicomSourceType_Orthanc, + DicomSourceType_DicomWeb, + DicomSourceType_DicomWebThroughOrthanc, + DicomSourceType_DicomDir + }; + + + class DicomSource + { + private: + DicomSourceType type_; + Orthanc::WebServiceParameters webService_; + std::string orthancDicomWebRoot_; + std::string serverName_; + bool hasOrthancWebViewer1_; + bool hasOrthancAdvancedPreview_; + bool hasDicomWebRendered_; + + public: + DicomSource() : + hasOrthancWebViewer1_(false), + hasOrthancAdvancedPreview_(false), + hasDicomWebRendered_(false) + { + SetOrthancSource(); + } + + DicomSourceType GetType() const + { + return type_; + } + + void SetOrthancSource(); + + void SetOrthancSource(const Orthanc::WebServiceParameters& parameters); + + const Orthanc::WebServiceParameters& GetOrthancParameters() const; + + void SetDicomDirSource(); + + void SetDicomWebSource(const std::string& baseUrl); + + void SetDicomWebSource(const std::string& baseUrl, + const std::string& username, + const std::string& password); + + void SetDicomWebThroughOrthancSource(const Orthanc::WebServiceParameters& orthancParameters, + const std::string& dicomWebRoot, + const std::string& serverName); + + void SetDicomWebThroughOrthancSource(const std::string& serverName); + + bool IsDicomWeb() const; + + bool IsOrthanc() const + { + return type_ == DicomSourceType_Orthanc; + } + + bool IsDicomDir() const + { + return type_ == DicomSourceType_DicomDir; + } + + IOracleCommand* CreateDicomWebCommand(const std::string& uri, + const std::map& arguments, + const std::map& headers, + Orthanc::IDynamicObject* payload /* takes ownership */) const; + + void AutodetectOrthancFeatures(const std::string& system, + const std::string& plugins); + + void SetOrthancWebViewer1(bool hasPlugin); + + bool HasOrthancWebViewer1() const; + + void SetOrthancAdvancedPreview(bool hasFeature); + + bool HasOrthancAdvancedPreview() const; + + void SetDicomWebRendered(bool hasFeature); + + bool HasDicomWebRendered() const; + + unsigned int GetQualityCount() const; + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/DicomStructureSetLoader.cpp --- a/Framework/Loaders/DicomStructureSetLoader.cpp Tue Feb 04 15:23:33 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,417 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - - -#include "DicomStructureSetLoader.h" - -#include "../Scene2D/PolylineSceneLayer.h" -#include "../StoneException.h" -#include "../Toolbox/GeometryToolbox.h" - -#include - -#include - -#if 0 -bool logbgo233 = false; -bool logbgo115 = false; -#endif - -namespace OrthancStone -{ - -#if 0 - void DumpDicomMap(std::ostream& o, const Orthanc::DicomMap& dicomMap) - { - using namespace std; - //ios_base::fmtflags state = o.flags(); - //o.flags(ios::right | ios::hex); - //o << "(" << setfill('0') << setw(4) << tag.GetGroup() - // << "," << setw(4) << tag.GetElement() << ")"; - //o.flags(state); - Json::Value val; - dicomMap.Serialize(val); - o << val; - //return o; - } -#endif - - - class DicomStructureSetLoader::AddReferencedInstance : public LoaderStateMachine::State - { - private: - std::string instanceId_; - - public: - AddReferencedInstance(DicomStructureSetLoader& that, - const std::string& instanceId) : - State(that), - instanceId_(instanceId) - { - } - - virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) - { - Json::Value tags; - message.ParseJsonBody(tags); - - Orthanc::DicomMap dicom; - dicom.FromDicomAsJson(tags); - - DicomStructureSetLoader& loader = GetLoader(); - - loader.content_->AddReferencedSlice(dicom); - - loader.countProcessedInstances_ ++; - assert(loader.countProcessedInstances_ <= loader.countReferencedInstances_); - - if (loader.countProcessedInstances_ == loader.countReferencedInstances_) - { - // All the referenced instances have been loaded, finalize the RT-STRUCT - loader.content_->CheckReferencedSlices(); - loader.revision_++; - loader.SetStructuresReady(); - } - } - }; - - - // State that converts a "SOP Instance UID" to an Orthanc identifier - class DicomStructureSetLoader::LookupInstance : public LoaderStateMachine::State - { - private: - std::string sopInstanceUid_; - - public: - LookupInstance(DicomStructureSetLoader& that, - const std::string& sopInstanceUid) : - State(that), - sopInstanceUid_(sopInstanceUid) - { - } - - virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) - { -#if 0 - LOG(TRACE) << "DicomStructureSetLoader::LookupInstance::Handle() (SUCCESS)"; -#endif - DicomStructureSetLoader& loader = GetLoader(); - - Json::Value lookup; - message.ParseJsonBody(lookup); - - if (lookup.type() != Json::arrayValue || - lookup.size() != 1 || - !lookup[0].isMember("Type") || - !lookup[0].isMember("Path") || - lookup[0]["Type"].type() != Json::stringValue || - lookup[0]["ID"].type() != Json::stringValue || - lookup[0]["Type"].asString() != "Instance") - { - std::stringstream msg; - msg << "Unknown resource! message.GetAnswer() = " << message.GetAnswer() << " message.GetAnswerHeaders() = "; - for (OrthancRestApiCommand::HttpHeaders::const_iterator it = message.GetAnswerHeaders().begin(); - it != message.GetAnswerHeaders().end(); ++it) - { - msg << "\nkey: \"" << it->first << "\" value: \"" << it->second << "\"\n"; - } - const std::string msgStr = msg.str(); - LOG(ERROR) << msgStr; - throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource); - } - - const std::string instanceId = lookup[0]["ID"].asString(); - - { - std::auto_ptr command(new OrthancRestApiCommand); - command->SetHttpHeader("Accept-Encoding", "gzip"); - std::string uri = "/instances/" + instanceId + "/tags"; - command->SetUri(uri); - command->SetPayload(new AddReferencedInstance(loader, instanceId)); - Schedule(command.release()); - } - } - }; - - - class DicomStructureSetLoader::LoadStructure : public LoaderStateMachine::State - { - public: - LoadStructure(DicomStructureSetLoader& that) : - State(that) - { - } - - virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) - { -#if 0 - if (logbgo115) - LOG(TRACE) << "DicomStructureSetLoader::LoadStructure::Handle() (SUCCESS)"; -#endif - DicomStructureSetLoader& loader = GetLoader(); - - { - OrthancPlugins::FullOrthancDataset dicom(message.GetAnswer()); - loader.content_.reset(new DicomStructureSet(dicom)); - size_t structureCount = loader.content_->GetStructuresCount(); - loader.structureVisibility_.resize(structureCount); - bool everythingVisible = false; - if ((loader.initiallyVisibleStructures_.size() == 1) - && (loader.initiallyVisibleStructures_[0].size() == 1) - && (loader.initiallyVisibleStructures_[0][0] == '*')) - { - everythingVisible = true; - } - - for (size_t i = 0; i < structureCount; ++i) - { - // if a single "*" string is supplied, this means we want everything - // to be visible... - if(everythingVisible) - { - loader.structureVisibility_.at(i) = true; - } - else - { - // otherwise, we only enable visibility for those structures whose - // names are mentioned in the initiallyVisibleStructures_ array - const std::string& structureName = loader.content_->GetStructureName(i); - - std::vector::iterator foundIt = - std::find( - loader.initiallyVisibleStructures_.begin(), - loader.initiallyVisibleStructures_.end(), - structureName); - std::vector::iterator endIt = loader.initiallyVisibleStructures_.end(); - if (foundIt != endIt) - loader.structureVisibility_.at(i) = true; - else - loader.structureVisibility_.at(i) = false; - } - } - } - - // Some (admittedly invalid) Dicom files have empty values in the - // 0008,1155 tag. We try our best to cope with this. - std::set instances; - std::set nonEmptyInstances; - loader.content_->GetReferencedInstances(instances); - for (std::set::const_iterator - it = instances.begin(); it != instances.end(); ++it) - { - std::string instance = Orthanc::Toolbox::StripSpaces(*it); - if(instance != "") - nonEmptyInstances.insert(instance); - } - - loader.countReferencedInstances_ = - static_cast(nonEmptyInstances.size()); - - for (std::set::const_iterator - it = nonEmptyInstances.begin(); it != nonEmptyInstances.end(); ++it) - { - std::auto_ptr command(new OrthancRestApiCommand); - command->SetUri("/tools/lookup"); - command->SetMethod(Orthanc::HttpMethod_Post); - command->SetBody(*it); - command->SetPayload(new LookupInstance(loader, *it)); - Schedule(command.release()); - } - } - }; - - - class DicomStructureSetLoader::Slice : public IExtractedSlice - { - private: - const DicomStructureSet& content_; - uint64_t revision_; - bool isValid_; - std::vector visibility_; - - public: - /** - The visibility vector must either: - - be empty - or - - contain the same number of items as the number of structures in the - structure set. - In the first case (empty vector), all the structures are displayed. - In the second case, the visibility of each structure is defined by the - content of the vector at the corresponding index. - */ - Slice(const DicomStructureSet& content, - uint64_t revision, - const CoordinateSystem3D& cuttingPlane, - std::vector visibility = std::vector()) - : content_(content) - , revision_(revision) - , visibility_(visibility) - { - ORTHANC_ASSERT((visibility_.size() == content_.GetStructuresCount()) - || (visibility_.size() == 0u)); - - bool opposite; - - const Vector normal = content.GetNormal(); - isValid_ = ( - GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetNormal()) || - GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisX()) || - GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisY())); - } - - virtual bool IsValid() - { - return isValid_; - } - - virtual uint64_t GetRevision() - { - return revision_; - } - - virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator, - const CoordinateSystem3D& cuttingPlane) - { - assert(isValid_); - - std::auto_ptr layer(new PolylineSceneLayer); - layer->SetThickness(2); - - for (size_t i = 0; i < content_.GetStructuresCount(); i++) - { - if ((visibility_.size() == 0) || visibility_.at(i)) - { - const Color& color = content_.GetStructureColor(i); - -#ifdef USE_BOOST_UNION_FOR_POLYGONS - std::vector< std::vector > polygons; - - if (content_.ProjectStructure(polygons, i, cuttingPlane)) - { - for (size_t j = 0; j < polygons.size(); j++) - { - PolylineSceneLayer::Chain chain; - chain.resize(polygons[j].size()); - - for (size_t k = 0; k < polygons[j].size(); k++) - { - chain[k] = ScenePoint2D(polygons[j][k].x, polygons[j][k].y); - } - - layer->AddChain(chain, true /* closed */, color); - } - } -#else - std::vector< std::pair > segments; - - if (content_.ProjectStructure(segments, i, cuttingPlane)) - { - for (size_t j = 0; j < segments.size(); j++) - { - PolylineSceneLayer::Chain chain; - chain.resize(2); - - chain[0] = ScenePoint2D(segments[j].first.x, segments[j].first.y); - chain[1] = ScenePoint2D(segments[j].second.x, segments[j].second.y); - - layer->AddChain(chain, false /* NOT closed */, color); - } - } -#endif - } - } - - return layer.release(); - } - }; - - - DicomStructureSetLoader::DicomStructureSetLoader(IOracle& oracle, - IObservable& oracleObservable) : - LoaderStateMachine(oracle, oracleObservable), - IObservable(oracleObservable.GetBroker()), - revision_(0), - countProcessedInstances_(0), - countReferencedInstances_(0), - structuresReady_(false) - { - } - - - void DicomStructureSetLoader::SetStructureDisplayState(size_t structureIndex, bool display) - { - structureVisibility_.at(structureIndex) = display; - revision_++; - } - - DicomStructureSetLoader::~DicomStructureSetLoader() - { - LOG(TRACE) << "DicomStructureSetLoader::~DicomStructureSetLoader()"; - } - - void DicomStructureSetLoader::LoadInstance( - const std::string& instanceId, - const std::vector& initiallyVisibleStructures) - { - Start(); - - instanceId_ = instanceId; - initiallyVisibleStructures_ = initiallyVisibleStructures; - - { - std::auto_ptr command(new OrthancRestApiCommand); - command->SetHttpHeader("Accept-Encoding", "gzip"); - - std::string uri = "/instances/" + instanceId + "/tags?ignore-length=3006-0050"; - - command->SetUri(uri); - command->SetPayload(new LoadStructure(*this)); - Schedule(command.release()); - } - } - - - IVolumeSlicer::IExtractedSlice* DicomStructureSetLoader::ExtractSlice(const CoordinateSystem3D& cuttingPlane) - { - if (content_.get() == NULL) - { - // Geometry is not available yet - return new IVolumeSlicer::InvalidSlice; - } - else - { - return new Slice(*content_, revision_, cuttingPlane, structureVisibility_); - } - } - - void DicomStructureSetLoader::SetStructuresReady() - { - ORTHANC_ASSERT(!structuresReady_); - structuresReady_ = true; - BroadcastMessage(DicomStructureSetLoader::StructuresReady(*this)); - } - - bool DicomStructureSetLoader::AreStructuresReady() const - { - return structuresReady_; - } - -} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/DicomStructureSetLoader.h --- a/Framework/Loaders/DicomStructureSetLoader.h Tue Feb 04 15:23:33 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,100 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "../Toolbox/DicomStructureSet.h" -#include "../Volumes/IVolumeSlicer.h" -#include "LoaderStateMachine.h" - -#include - -namespace OrthancStone -{ - class DicomStructureSetLoader : - public LoaderStateMachine, - public IVolumeSlicer, - public IObservable - { - private: - class Slice; - - // States of LoaderStateMachine - class AddReferencedInstance; // 3rd state - class LookupInstance; // 2nd state - class LoadStructure; // 1st state - - std::auto_ptr content_; - uint64_t revision_; - std::string instanceId_; - unsigned int countProcessedInstances_; - unsigned int countReferencedInstances_; - - // will be set to true once the loading is finished - bool structuresReady_; - - /** - At load time, these strings are used to initialize the structureVisibility_ - vector. - - As a special case, if initiallyVisibleStructures_ contains a single string - that is '*', ALL structures will be made visible. - */ - std::vector initiallyVisibleStructures_; - - /** - Contains the "Should this structure be displayed?" flag for all structures. - Only filled when structures are loaded. - - Changing this value directly affects the rendering - */ - std::vector structureVisibility_; - - public: - ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, StructuresReady, DicomStructureSetLoader); - - DicomStructureSetLoader(IOracle& oracle, - IObservable& oracleObservable); - - DicomStructureSet* GetContent() - { - return content_.get(); - } - - void SetStructureDisplayState(size_t structureIndex, bool display); - - bool GetStructureDisplayState(size_t structureIndex) const - { - return structureVisibility_.at(structureIndex); - } - - ~DicomStructureSetLoader(); - - void LoadInstance(const std::string& instanceId, - const std::vector& initiallyVisibleStructures = std::vector()); - - virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE; - - void SetStructuresReady(); - - bool AreStructuresReady() const; - }; -} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/DicomStructureSetLoader2.cpp --- a/Framework/Loaders/DicomStructureSetLoader2.cpp Tue Feb 04 15:23:33 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,125 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - -#include "DicomStructureSetLoader2.h" - -#include "../Messages/IObservable.h" -#include "../Oracle/IOracle.h" -#include "../Oracle/OracleCommandExceptionMessage.h" - -namespace OrthancStone -{ - - DicomStructureSetLoader2::DicomStructureSetLoader2( - DicomStructureSet2& structureSet - , IOracle& oracle - , IObservable& oracleObservable) - : IObserver(oracleObservable.GetBroker()) - , IObservable(oracleObservable.GetBroker()) - , structureSet_(structureSet) - , oracle_(oracle) - , oracleObservable_(oracleObservable) - , structuresReady_(false) - { - LOG(TRACE) << "DicomStructureSetLoader2(" << std::hex << this << std::dec << ")::DicomStructureSetLoader2()"; - - oracleObservable.RegisterObserverCallback( - new Callable - (*this, &DicomStructureSetLoader2::HandleSuccessMessage)); - - oracleObservable.RegisterObserverCallback( - new Callable - (*this, &DicomStructureSetLoader2::HandleExceptionMessage)); - } - - DicomStructureSetLoader2::~DicomStructureSetLoader2() - { - LOG(TRACE) << "DicomStructureSetLoader2(" << std::hex << this << std::dec << ")::~DicomStructureSetLoader2()"; - oracleObservable_.Unregister(this); - } - - void DicomStructureSetLoader2::LoadInstanceFromString(const std::string& body) - { - OrthancPlugins::FullOrthancDataset dicom(body); - //loader.content_.reset(new DicomStructureSet(dicom)); - structureSet_.Clear(); - structureSet_.SetContents(dicom); - SetStructuresReady(); - } - - void DicomStructureSetLoader2::HandleSuccessMessage(const OrthancRestApiCommand::SuccessMessage& message) - { - const std::string& body = message.GetAnswer(); - LoadInstanceFromString(body); - } - - void DicomStructureSetLoader2::HandleExceptionMessage(const OracleCommandExceptionMessage& message) - { - LOG(ERROR) << "DicomStructureSetLoader2::HandleExceptionMessage: error when trying to load data. " - << "Error: " << message.GetException().What() << " Details: " - << message.GetException().GetDetails(); - } - - void DicomStructureSetLoader2::LoadInstance(const std::string& instanceId) - { - std::auto_ptr command(new OrthancRestApiCommand); - command->SetHttpHeader("Accept-Encoding", "gzip"); - - std::string uri = "/instances/" + instanceId + "/tags?ignore-length=3006-0050"; - - command->SetUri(uri); - oracle_.Schedule(*this, command.release()); - } - - void DicomStructureSetLoader2::SetStructuresReady() - { - structuresReady_ = true; - } - - bool DicomStructureSetLoader2::AreStructuresReady() const - { - return structuresReady_; - } - - /* - - void LoaderStateMachine::HandleExceptionMessage(const OracleCommandExceptionMessage& message) - { - LOG(ERROR) << "LoaderStateMachine::HandleExceptionMessage: error in the state machine, stopping all processing"; - LOG(ERROR) << "Error: " << message.GetException().What() << " Details: " << - message.GetException().GetDetails(); - Clear(); - } - - LoaderStateMachine::~LoaderStateMachine() - { - Clear(); - } - - - */ - -} - -#endif -// BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/DicomStructureSetLoader2.h --- a/Framework/Loaders/DicomStructureSetLoader2.h Tue Feb 04 15:23:33 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,87 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - -#pragma once - -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - -#include "../Toolbox/DicomStructureSet2.h" -#include "../Messages/IMessage.h" -#include "../Messages/IObserver.h" -#include "../Messages/IObservable.h" -#include "../Oracle/OrthancRestApiCommand.h" - -#include - -namespace OrthancStone -{ - class IOracle; - class IObservable; - class OrthancRestApiCommand; - class OracleCommandExceptionMessage; - - class DicomStructureSetLoader2 : public IObserver, public IObservable - { - public: - ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, StructuresReady, DicomStructureSetLoader2); - - /** - Warning: the structureSet, oracle and oracleObservable objects must live - at least as long as this object (TODO: shared_ptr?) - */ - DicomStructureSetLoader2(DicomStructureSet2& structureSet, IOracle& oracle, IObservable& oracleObservable); - - ~DicomStructureSetLoader2(); - - void LoadInstance(const std::string& instanceId); - - /** Internal use */ - void LoadInstanceFromString(const std::string& body); - - void SetStructuresReady(); - bool AreStructuresReady() const; - - private: - /** - Called back by the oracle when data is ready! - */ - void HandleSuccessMessage(const OrthancRestApiCommand::SuccessMessage& message); - - /** - Called back by the oracle when shit hits the fan - */ - void HandleExceptionMessage(const OracleCommandExceptionMessage& message); - - /** - The structure set that will be (cleared and) filled with data from the - loader - */ - DicomStructureSet2& structureSet_; - - IOracle& oracle_; - IObservable& oracleObservable_; - bool structuresReady_; - }; -} - -#endif -// BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - - diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/DicomVolumeLoader.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/DicomVolumeLoader.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,182 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "DicomVolumeLoader.h" + +#include + +namespace OrthancStone +{ + DicomVolumeLoader::DicomVolumeLoader(boost::shared_ptr& framesLoader, + bool computeRange) : + framesLoader_(framesLoader), + isValid_(false), + started_(false), + remaining_(0) + { + volume_.reset(new OrthancStone::DicomVolumeImage); + + const SeriesOrderedFrames& frames = framesLoader_->GetOrderedFrames(); + + if (frames.IsRegular3DVolume() && + frames.GetFramesCount() > 0) + { + // TODO - Is "0" the good choice for the reference frame? + // Shouldn't we use "count - 1" depending on the direction + // of the normal? + const OrthancStone::DicomInstanceParameters& parameters = frames.GetInstanceParameters(0); + + OrthancStone::CoordinateSystem3D plane(frames.GetInstance(0)); + + OrthancStone::VolumeImageGeometry geometry; + geometry.SetSizeInVoxels(parameters.GetImageInformation().GetWidth(), + parameters.GetImageInformation().GetHeight(), + static_cast(frames.GetFramesCount())); + geometry.SetAxialGeometry(plane); + + double spacing; + if (parameters.GetSopClassUid() == SopClassUid_RTDose) + { + if (!parameters.ComputeRegularSpacing(spacing)) + { + LOG(WARNING) << "Unable to compute the spacing in a RT-DOSE instance"; + spacing = frames.GetSpacingBetweenSlices(); + } + } + else + { + spacing = frames.GetSpacingBetweenSlices(); + } + + geometry.SetVoxelDimensions(parameters.GetPixelSpacingX(), + parameters.GetPixelSpacingY(), spacing); + volume_->Initialize(geometry, parameters.GetExpectedPixelFormat(), computeRange); + volume_->GetPixelData().Clear(); + volume_->SetDicomParameters(parameters); + + remaining_ = frames.GetFramesCount(); + isValid_ = true; + } + else + { + LOG(WARNING) << "Not a regular 3D volume"; + } + } + + + void DicomVolumeLoader::Handle(const OrthancStone::SeriesFramesLoader::FrameLoadedMessage& message) + { + if (remaining_ == 0 || + !message.HasUserPayload()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + if (message.GetImage().GetWidth() != volume_->GetPixelData().GetWidth() || + message.GetImage().GetHeight() != volume_->GetPixelData().GetHeight()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize); + } + + if (message.GetImage().GetFormat() != volume_->GetPixelData().GetFormat()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat); + } + + if (message.GetFrameIndex() >= volume_->GetPixelData().GetDepth()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + size_t frameIndex = dynamic_cast&>(message.GetUserPayload()).GetValue(); + + { + ImageBuffer3D::SliceWriter writer(volume_->GetPixelData(), VolumeProjection_Axial, frameIndex); + Orthanc::ImageProcessing::Copy(writer.GetAccessor(), message.GetImage()); + } + + volume_->IncrementRevision(); + + { + VolumeUpdatedMessage updated(*this, frameIndex); + BroadcastMessage(updated); + } + + remaining_--; + + if (remaining_ == 0) + { + VolumeReadyMessage ready(*this); + BroadcastMessage(ready); + } + } + + + DicomVolumeLoader::Factory::Factory(LoadedDicomResources& instances) : + framesFactory_(instances), + computeRange_(false) + { + } + + DicomVolumeLoader::Factory::Factory(const SeriesMetadataLoader::SeriesLoadedMessage& metadata) : + framesFactory_(metadata.GetInstances()), + computeRange_(false) + { + SetDicomDir(metadata.GetDicomDirPath(), metadata.GetDicomDir()); // Only useful for DICOMDIR sources + } + + + boost::shared_ptr DicomVolumeLoader::Factory::Create(ILoadersContext::ILock& context) + { + boost::shared_ptr frames = + boost::dynamic_pointer_cast(framesFactory_.Create(context)); + + boost::shared_ptr volume(new DicomVolumeLoader(frames, computeRange_)); + volume->Register(*frames, &DicomVolumeLoader::Handle); + + return volume; + } + + void DicomVolumeLoader::Start(int priority, + const DicomSource& source) + { + if (started_) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + started_ = true; + + if (IsValid()) + { + for (size_t i = 0; i < GetOrderedFrames().GetFramesCount(); i++) + { + framesLoader_->ScheduleLoadFrame(priority, source, i, source.GetQualityCount() - 1, + new Orthanc::SingleValueObject(i)); + } + } + else + { + VolumeReadyMessage ready(*this); + BroadcastMessage(ready); + } + } +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/DicomVolumeLoader.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/DicomVolumeLoader.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,141 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../Volumes/DicomVolumeImage.h" +#include "SeriesFramesLoader.h" +#include "SeriesMetadataLoader.h" + +namespace OrthancStone +{ + class DicomVolumeLoader : + public ObserverBase, + public IObservable + { + private: + boost::shared_ptr framesLoader_; + boost::shared_ptr volume_; + bool isValid_; + bool started_; + size_t remaining_; + + DicomVolumeLoader(boost::shared_ptr& framesLoader, + bool computeRange); + + void Handle(const OrthancStone::SeriesFramesLoader::FrameLoadedMessage& message); + + public: + class VolumeReadyMessage : public OriginMessage + { + ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); + + public: + VolumeReadyMessage(const DicomVolumeLoader& loader) : + OriginMessage(loader) + { + } + + const DicomVolumeImage& GetVolume() const + { + assert(GetOrigin().GetVolume()); + return *GetOrigin().GetVolume(); + } + }; + + + class VolumeUpdatedMessage : public OriginMessage + { + ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); + + private: + unsigned int axial_; + + public: + VolumeUpdatedMessage(const DicomVolumeLoader& loader, + unsigned int axial) : + OriginMessage(loader), + axial_(axial) + { + } + + unsigned int GetAxialIndex() const + { + return axial_; + } + + const DicomVolumeImage& GetVolume() const + { + assert(GetOrigin().GetVolume()); + return *GetOrigin().GetVolume(); + } + }; + + + class Factory : public ILoaderFactory + { + private: + SeriesFramesLoader::Factory framesFactory_; + bool computeRange_; + + public: + Factory(LoadedDicomResources& instances); + + Factory(const SeriesMetadataLoader::SeriesLoadedMessage& metadata); + + void SetComputeRange(bool computeRange) + { + computeRange_ = computeRange; + } + + void SetDicomDir(const std::string& dicomDirPath, + boost::shared_ptr dicomDir) + { + framesFactory_.SetDicomDir(dicomDirPath, dicomDir); + } + + virtual boost::shared_ptr Create(ILoadersContext::ILock& context) ORTHANC_OVERRIDE; + }; + + bool IsValid() const + { + return isValid_; + } + + bool IsFullyLoaded() const + { + return remaining_ == 0; + } + + boost::shared_ptr GetVolume() const + { + return volume_; + } + + const SeriesOrderedFrames& GetOrderedFrames() const + { + return framesLoader_->GetOrderedFrames(); + } + + void Start(int priority, + const DicomSource& source); + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/GenericLoadersContext.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/GenericLoadersContext.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,183 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "GenericLoadersContext.h" + +namespace OrthancStone +{ + class GenericLoadersContext::Locker : public ILoadersContext::ILock + { + private: + GenericLoadersContext& that_; + boost::recursive_mutex::scoped_lock lock_; + + public: + Locker(GenericLoadersContext& that) : + that_(that), + lock_(that.mutex_) + { + if (!that_.scheduler_) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + + virtual ILoadersContext& GetContext() const ORTHANC_OVERRIDE + { + return that_; + }; + + virtual void AddLoader(boost::shared_ptr loader) ORTHANC_OVERRIDE + { + that_.loaders_.push_back(loader); + } + + virtual IObservable& GetOracleObservable() const ORTHANC_OVERRIDE + { + return that_.oracleObservable_; + } + + virtual void Schedule(boost::shared_ptr receiver, + int priority, + IOracleCommand* command /* Takes ownership */) ORTHANC_OVERRIDE + { + that_.scheduler_->Schedule(receiver, priority, command); + }; + + virtual void CancelRequests(boost::shared_ptr receiver) ORTHANC_OVERRIDE + { + that_.scheduler_->CancelRequests(receiver); + } + + virtual void CancelAllRequests() ORTHANC_OVERRIDE + { + that_.scheduler_->CancelAllRequests(); + } + + virtual void GetStatistics(uint64_t& scheduledCommands, + uint64_t& processedCommands) + { + scheduledCommands = that_.scheduler_->GetTotalScheduled(); + processedCommands = that_.scheduler_->GetTotalProcessed(); + } + }; + + + void GenericLoadersContext::EmitMessage(boost::weak_ptr observer, + const IMessage& message) + { + boost::recursive_mutex::scoped_lock lock(mutex_); + //LOG(INFO) << " inside emit lock: " << message.GetIdentifier().AsString(); + oracleObservable_.EmitMessage(observer, message); + //LOG(INFO) << " outside emit lock"; + } + + + GenericLoadersContext::GenericLoadersContext(unsigned int maxHighPriority, + unsigned int maxStandardPriority, + unsigned int maxLowPriority) + { + oracle_.reset(new ThreadedOracle(*this)); + scheduler_ = OracleScheduler::Create(*oracle_, oracleObservable_, *this, + maxHighPriority, maxStandardPriority, maxLowPriority); + + if (!scheduler_) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + + + GenericLoadersContext::~GenericLoadersContext() + { + LOG(WARNING) << "scheduled commands: " << scheduler_->GetTotalScheduled() + << ", processed commands: " << scheduler_->GetTotalProcessed(); + scheduler_.reset(); + //LOG(INFO) << "counter: " << scheduler_.use_count(); + } + + + void GenericLoadersContext::SetOrthancParameters(const Orthanc::WebServiceParameters& parameters) + { + boost::recursive_mutex::scoped_lock lock(mutex_); + oracle_->SetOrthancParameters(parameters); + } + + + void GenericLoadersContext::SetRootDirectory(const std::string& root) + { + boost::recursive_mutex::scoped_lock lock(mutex_); + oracle_->SetRootDirectory(root); + } + + + void GenericLoadersContext::SetDicomCacheSize(size_t size) + { + boost::recursive_mutex::scoped_lock lock(mutex_); + oracle_->SetDicomCacheSize(size); + } + + + void GenericLoadersContext::StartOracle() + { + boost::recursive_mutex::scoped_lock lock(mutex_); + oracle_->Start(); + //LOG(INFO) << "STARTED ORACLE"; + } + + + void GenericLoadersContext::StopOracle() + { + /** + * DON'T lock "mutex_" here, otherwise Stone won't be able to + * stop if one command being executed by the oracle has to emit + * a message (method "EmitMessage()" would have to lock the + * mutex too). + **/ + + //LOG(INFO) << "STOPPING ORACLE"; + oracle_->Stop(); + //LOG(INFO) << "STOPPED ORACLE"; + } + + + void GenericLoadersContext::WaitUntilComplete() + { + for (;;) + { + { + boost::recursive_mutex::scoped_lock lock(mutex_); + if (scheduler_ && + scheduler_->GetTotalScheduled() == scheduler_->GetTotalProcessed()) + { + return; + } + } + + boost::this_thread::sleep(boost::posix_time::milliseconds(100)); + } + } + + ILoadersContext::ILock* GenericLoadersContext::Lock() + { + return new Locker(*this); + } +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/GenericLoadersContext.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/GenericLoadersContext.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,79 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../Messages/IMessageEmitter.h" +#include "../Oracle/ThreadedOracle.h" +#include "ILoadersContext.h" +#include "DicomSource.h" +#include "OracleScheduler.h" + +#include + +namespace OrthancStone +{ + class GenericLoadersContext : + public ILoadersContext, + private IMessageEmitter + { + private: + class Locker; + + // "Recursive mutex" is necessary, to be able to run + // "ILoaderFactory" from a message handler triggered by + // "EmitMessage()" + boost::recursive_mutex mutex_; + + IObservable oracleObservable_; + std::auto_ptr oracle_; + boost::shared_ptr scheduler_; + + // Necessary to keep the loaders persistent (including global + // function promises), after the function that created them is + // left. This avoids creating one global variable for each loader. + std::list< boost::shared_ptr > loaders_; + + virtual void EmitMessage(boost::weak_ptr observer, + const IMessage& message); + + public: + GenericLoadersContext(unsigned int maxHighPriority, + unsigned int maxStandardPriority, + unsigned int maxLowPriority); + + virtual ~GenericLoadersContext(); + + virtual ILock* Lock() ORTHANC_OVERRIDE; + + void SetOrthancParameters(const Orthanc::WebServiceParameters& parameters); + + void SetRootDirectory(const std::string& root); + + void SetDicomCacheSize(size_t size); + + void StartOracle(); + + void StopOracle(); + + void WaitUntilComplete(); + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/ILoaderFactory.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/ILoaderFactory.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,41 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "ILoadersContext.h" + +namespace OrthancStone +{ + class ILoaderFactory : public boost::noncopyable + { + public: + virtual ~ILoaderFactory() + { + } + + /** + * Factory function that creates a new loader, to be used by the + * Stone loaders context. + **/ + virtual boost::shared_ptr Create(ILoadersContext::ILock& context) = 0; + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/ILoadersContext.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/ILoadersContext.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,126 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../Messages/IObserver.h" +#include "../Messages/IObservable.h" +#include "../Oracle/IOracleCommand.h" + +#include + +namespace OrthancStone +{ + class ILoadersContext : public boost::noncopyable + { + public: + class ILock : public boost::noncopyable + { + public: + virtual ~ILock() + { + } + + /** + * This method is useful for loaders that must be able to + * re-lock the Stone loaders context in the future (for instance + * to schedule new commands once some command is processed). + **/ + virtual ILoadersContext& GetContext() const = 0; + + /** + * Get a reference to the observable against which a loader must + * listen to be informed of messages issued by the oracle once + * some command is processed. + **/ + virtual IObservable& GetOracleObservable() const = 0; + + /** + * Schedule a new command for further processing by the + * oracle. The "receiver" argument indicates to which object the + * notification messages are sent by the oracle upon completion + * of the command. The command is possibly not directly sent to + * the oracle: Instead, an internal "OracleScheduler" object is + * often used as a priority queue to rule the order in which + * commands are actually sent to the oracle. Hence the + * "priority" argument (commands with lower value are executed + * first). + **/ + virtual void Schedule(boost::shared_ptr receiver, + int priority, + IOracleCommand* command /* Takes ownership */) = 0; + + /** + * Cancel all the commands that are waiting in the + * "OracleScheduler" queue and that are linked to the given + * receiver (i.e. the observer that was specified at the time + * method "Schedule()" was called). This is useful for real-time + * processing, as it allows to replace commands that were + * scheduled in the past by more urgent commands. + * + * Note that this call does not affect commands that would have + * already be sent to the oracle. As a consequence, the receiver + * might still receive messages that were sent to the oracle + * before the cancellation (be prepared to handle such + * messages). + **/ + virtual void CancelRequests(boost::shared_ptr receiver) = 0; + + /** + * Same as "CancelRequests()", but targets all the receivers. + **/ + virtual void CancelAllRequests() = 0; + + /** + * Add a reference to the given observer in the Stone loaders + * context. This can be used to match the lifetime of a loader + * with the lifetime of the Stone context: This is useful if + * your Stone application does not keep a reference to the + * loader by itself (typically in global promises), which would + * make the loader disappear as soon as the scope of the + * variable is left. + **/ + virtual void AddLoader(boost::shared_ptr loader) = 0; + + /** + * Returns the number of commands that were scheduled and + * processed using the "Schedule()" method. By "processed" + * commands, we refer to the number of commands that were either + * executed by the oracle, or canceled by the user. So the + * counting sequences are monotonically increasing over time. + **/ + virtual void GetStatistics(uint64_t& scheduledCommands, + uint64_t& processedCommands) = 0; + }; + + virtual ~ILoadersContext() + { + } + + /** + * Locks the Stone loaders context, to give access to its + * underlying features. This is important for Stone applications + * running in a multi-threaded environment, for which a global + * mutex is locked. + **/ + virtual ILock* Lock() = 0; + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/LoadedDicomResources.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/LoadedDicomResources.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,233 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "LoadedDicomResources.h" + +#include + +#include + + +namespace OrthancStone +{ + void LoadedDicomResources::Flatten() + { + // Lazy generation of a "std::vector" from the "std::map" + if (flattened_.empty()) + { + flattened_.resize(resources_.size()); + + size_t pos = 0; + for (Resources::const_iterator it = resources_.begin(); it != resources_.end(); ++it) + { + assert(it->second != NULL); + flattened_[pos++] = it->second; + } + } + else + { + // No need to flatten + assert(flattened_.size() == resources_.size()); + } + } + + + void LoadedDicomResources::AddFromDicomWebInternal(const Json::Value& dicomweb) + { + assert(dicomweb.type() == Json::objectValue); + Orthanc::DicomMap dicom; + dicom.FromDicomWeb(dicomweb); + AddResource(dicom); + } + + + LoadedDicomResources::LoadedDicomResources(const LoadedDicomResources& other, + const Orthanc::DicomTag& indexedTag) : + indexedTag_(indexedTag) + { + for (Resources::const_iterator it = other.resources_.begin(); + it != other.resources_.end(); ++it) + { + assert(it->second != NULL); + AddResource(*it->second); + } + } + + void LoadedDicomResources::Clear() + { + for (Resources::iterator it = resources_.begin(); it != resources_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + + resources_.clear(); + flattened_.clear(); + } + + + Orthanc::DicomMap& LoadedDicomResources::GetResource(size_t index) + { + Flatten(); + + if (index >= flattened_.size()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + assert(flattened_[index] != NULL); + return *flattened_[index]; + } + } + + + void LoadedDicomResources::MergeResource(Orthanc::DicomMap& target, + const std::string& id) const + { + Resources::const_iterator it = resources_.find(id); + + if (it == resources_.end()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem); + } + else + { + assert(it->second != NULL); + target.Merge(*it->second); + } + } + + + bool LoadedDicomResources::LookupStringValue(std::string& target, + const std::string& id, + const Orthanc::DicomTag& tag) const + { + Resources::const_iterator found = resources_.find(id); + + if (found == resources_.end()) + { + return false; + } + else + { + assert(found->second != NULL); + return found->second->LookupStringValue(target, tag, false); + } + } + + + void LoadedDicomResources::AddResource(const Orthanc::DicomMap& dicom) + { + std::string id; + + if (dicom.LookupStringValue(id, indexedTag_, false /* no binary value */) && + resources_.find(id) == resources_.end() /* Don't index twice the same resource */) + { + resources_[id] = dicom.Clone(); + flattened_.clear(); // Invalidate the flattened version + } + } + + + void LoadedDicomResources::AddFromOrthanc(const Json::Value& tags) + { + Orthanc::DicomMap dicom; + dicom.FromDicomAsJson(tags); + AddResource(dicom); + } + + + void LoadedDicomResources::AddFromDicomWeb(const Json::Value& dicomweb) + { + if (dicomweb.type() == Json::objectValue) + { + AddFromDicomWebInternal(dicomweb); + } + else if (dicomweb.type() == Json::arrayValue) + { + for (Json::Value::ArrayIndex i = 0; i < dicomweb.size(); i++) + { + if (dicomweb[i].type() == Json::objectValue) + { + AddFromDicomWebInternal(dicomweb[i]); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + } + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + } + + + bool LoadedDicomResources::LookupTagValueConsensus(std::string& target, + const Orthanc::DicomTag& tag) const + { + typedef std::map Counter; + + Counter counter; + + for (Resources::const_iterator it = resources_.begin(); it != resources_.end(); ++it) + { + assert(it->second != NULL); + + std::string value; + if (it->second->LookupStringValue(value, tag, false)) + { + Counter::iterator found = counter.find(value); + if (found == counter.end()) + { + counter[value] = 1; + } + else + { + found->second ++; + } + } + } + + Counter::const_iterator best = counter.end(); + + for (Counter::const_iterator it = counter.begin(); it != counter.end(); ++it) + { + if (best == counter.end() || + best->second < it->second) + { + best = it; + } + } + + if (best == counter.end()) + { + return false; + } + else + { + target = best->first; + return true; + } + } +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/LoadedDicomResources.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/LoadedDicomResources.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,92 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include + + +namespace OrthancStone +{ + class LoadedDicomResources : public boost::noncopyable + { + private: + typedef std::map Resources; + + Orthanc::DicomTag indexedTag_; + Resources resources_; + std::vector flattened_; + + void Flatten(); + + void AddFromDicomWebInternal(const Json::Value& dicomweb); + + public: + LoadedDicomResources(const Orthanc::DicomTag& indexedTag) : + indexedTag_(indexedTag) + { + } + + // Re-index another set of resources using another tag + LoadedDicomResources(const LoadedDicomResources& other, + const Orthanc::DicomTag& indexedTag); + + ~LoadedDicomResources() + { + Clear(); + } + + const Orthanc::DicomTag& GetIndexedTag() const + { + return indexedTag_; + } + + void Clear(); + + size_t GetSize() const + { + return resources_.size(); + } + + Orthanc::DicomMap& GetResource(size_t index); + + bool HasResource(const std::string& id) const + { + return resources_.find(id) != resources_.end(); + } + + void MergeResource(Orthanc::DicomMap& target, + const std::string& id) const; + + bool LookupStringValue(std::string& target, + const std::string& id, + const Orthanc::DicomTag& tag) const; + + void AddResource(const Orthanc::DicomMap& dicom); + + void AddFromOrthanc(const Json::Value& tags); + + void AddFromDicomWeb(const Json::Value& dicomweb); + + bool LookupTagValueConsensus(std::string& target, + const Orthanc::DicomTag& tag) const; + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/LoaderCache.cpp --- a/Framework/Loaders/LoaderCache.cpp Tue Feb 04 15:23:33 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,415 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - -#include "LoaderCache.h" - -#include "../StoneException.h" -#include "OrthancSeriesVolumeProgressiveLoader.h" -#include "OrthancMultiframeVolumeLoader.h" -#include "DicomStructureSetLoader.h" - -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 -#include "DicomStructureSetLoader2.h" -#endif - //BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - - -#if ORTHANC_ENABLE_WASM == 1 -# include -# include "../Oracle/WebAssemblyOracle.h" -#else -# include "../Oracle/ThreadedOracle.h" -#endif - -#include "../Messages/LockingEmitter.h" - -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 -#include "../Toolbox/DicomStructureSet2.h" -#endif -//BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - -#include "../Volumes/DicomVolumeImage.h" -#include "../Volumes/DicomVolumeImageMPRSlicer.h" - -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 -#include "../Volumes/DicomStructureSetSlicer2.h" -#endif -//BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - -#include -#include - -namespace OrthancStone -{ -#if ORTHANC_ENABLE_WASM == 1 - LoaderCache::LoaderCache(WebAssemblyOracle& oracle) - : oracle_(oracle) - { - - } -#else - LoaderCache::LoaderCache(ThreadedOracle& oracle, LockingEmitter& lockingEmitter) - : oracle_(oracle) - , lockingEmitter_(lockingEmitter) - { - } -#endif - - boost::shared_ptr - LoaderCache::GetSeriesVolumeProgressiveLoader(std::string seriesUuid) - { - try - { - - // normalize keys a little - seriesUuid = Orthanc::Toolbox::StripSpaces(seriesUuid); - Orthanc::Toolbox::ToLowerCase(seriesUuid); - - // find in cache - if (seriesVolumeProgressiveLoaders_.find(seriesUuid) == seriesVolumeProgressiveLoaders_.end()) - { -// LOG(TRACE) << "LoaderCache::GetSeriesVolumeProgressiveLoader : CACHEMISS --> need to load seriesUUid = " << seriesUuid; -#if ORTHANC_ENABLE_WASM == 1 -// LOG(TRACE) << "Performing request for series " << seriesUuid << " sbrk(0) = " << sbrk(0); -#else -// LOG(TRACE) << "Performing request for series " << seriesUuid; -#endif - boost::shared_ptr volumeImage(new DicomVolumeImage); - boost::shared_ptr loader; -// LOG(TRACE) << "volumeImage = " << volumeImage.get(); - { -#if ORTHANC_ENABLE_WASM == 1 - loader.reset(new OrthancSeriesVolumeProgressiveLoader(volumeImage, oracle_, oracle_)); -#else - LockingEmitter::WriterLock lock(lockingEmitter_); - loader.reset(new OrthancSeriesVolumeProgressiveLoader(volumeImage, oracle_, lock.GetOracleObservable())); -#endif -// LOG(TRACE) << "LoaderCache::GetSeriesVolumeProgressiveLoader : loader = " << loader.get(); - loader->LoadSeries(seriesUuid); -// LOG(TRACE) << "LoaderCache::GetSeriesVolumeProgressiveLoader : loader->LoadSeries successful"; - } - seriesVolumeProgressiveLoaders_[seriesUuid] = loader; - } - else - { -// LOG(TRACE) << "LoaderCache::GetSeriesVolumeProgressiveLoader : returning cached loader for seriesUUid = " << seriesUuid; - } - return seriesVolumeProgressiveLoaders_[seriesUuid]; - } - catch (const Orthanc::OrthancException& e) - { - if (e.HasDetails()) - { - LOG(ERROR) << "OrthancException in LoaderCache: " << e.What() << " Details: " << e.GetDetails(); - } - else - { - LOG(ERROR) << "OrthancException in LoaderCache: " << e.What(); - } - throw; - } - catch (const std::exception& e) - { - LOG(ERROR) << "std::exception in LoaderCache: " << e.what(); - throw; - } - catch (...) - { - LOG(ERROR) << "Unknown exception in LoaderCache"; - throw; - } - } - - boost::shared_ptr LoaderCache::GetMultiframeVolumeLoader(std::string instanceUuid) - { - // if the loader is not available, let's trigger its creation - if(multiframeVolumeLoaders_.find(instanceUuid) == multiframeVolumeLoaders_.end()) - { - GetMultiframeDicomVolumeImageMPRSlicer(instanceUuid); - } - ORTHANC_ASSERT(multiframeVolumeLoaders_.find(instanceUuid) != multiframeVolumeLoaders_.end()); - - return multiframeVolumeLoaders_[instanceUuid]; - } - - boost::shared_ptr LoaderCache::GetMultiframeDicomVolumeImageMPRSlicer(std::string instanceUuid) - { - try - { - // normalize keys a little - instanceUuid = Orthanc::Toolbox::StripSpaces(instanceUuid); - Orthanc::Toolbox::ToLowerCase(instanceUuid); - - // find in cache - if (dicomVolumeImageMPRSlicers_.find(instanceUuid) == dicomVolumeImageMPRSlicers_.end()) - { - boost::shared_ptr volumeImage(new DicomVolumeImage); - boost::shared_ptr loader; - - { -#if ORTHANC_ENABLE_WASM == 1 - loader.reset(new OrthancMultiframeVolumeLoader(volumeImage, oracle_, oracle_)); -#else - LockingEmitter::WriterLock lock(lockingEmitter_); - loader.reset(new OrthancMultiframeVolumeLoader(volumeImage, - oracle_, - lock.GetOracleObservable())); -#endif - loader->LoadInstance(instanceUuid); - } - multiframeVolumeLoaders_[instanceUuid] = loader; - boost::shared_ptr mprSlicer(new DicomVolumeImageMPRSlicer(volumeImage)); - dicomVolumeImageMPRSlicers_[instanceUuid] = mprSlicer; - } - return dicomVolumeImageMPRSlicers_[instanceUuid]; - } - catch (const Orthanc::OrthancException& e) - { - if (e.HasDetails()) - { - LOG(ERROR) << "OrthancException in LoaderCache: " << e.What() << " Details: " << e.GetDetails(); - } - else - { - LOG(ERROR) << "OrthancException in LoaderCache: " << e.What(); - } - throw; - } - catch (const std::exception& e) - { - LOG(ERROR) << "std::exception in LoaderCache: " << e.what(); - throw; - } - catch (...) - { - LOG(ERROR) << "Unknown exception in LoaderCache"; - throw; - } - } - -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - - boost::shared_ptr LoaderCache::GetDicomStructureSetSlicer2(std::string instanceUuid) - { - // if the loader is not available, let's trigger its creation - if (dicomStructureSetSlicers2_.find(instanceUuid) == dicomStructureSetSlicers2_.end()) - { - GetDicomStructureSetLoader2(instanceUuid); - } - ORTHANC_ASSERT(dicomStructureSetSlicers2_.find(instanceUuid) != dicomStructureSetSlicers2_.end()); - - return dicomStructureSetSlicers2_[instanceUuid]; - } -#endif -//BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - - - /** - This method allows to convert a list of string into a string by - sorting the strings then joining them - */ - static std::string SortAndJoin(const std::vector& stringList) - { - if (stringList.size() == 0) - { - return ""; - } - else - { - std::vector sortedStringList = stringList; - std::sort(sortedStringList.begin(), sortedStringList.end()); - std::stringstream s; - s << sortedStringList[0]; - for (size_t i = 1; i < sortedStringList.size(); ++i) - { - s << "-" << sortedStringList[i]; - } - return s.str(); - } - } - - boost::shared_ptr - LoaderCache::GetDicomStructureSetLoader( - std::string inInstanceUuid, - const std::vector& initiallyVisibleStructures) - { - try - { - // normalize keys a little - inInstanceUuid = Orthanc::Toolbox::StripSpaces(inInstanceUuid); - Orthanc::Toolbox::ToLowerCase(inInstanceUuid); - - std::string initiallyVisibleStructuresKey = - SortAndJoin(initiallyVisibleStructures); - - std::string entryKey = inInstanceUuid + "_" + initiallyVisibleStructuresKey; - - // find in cache - if (dicomStructureSetLoaders_.find(entryKey) == dicomStructureSetLoaders_.end()) - { - boost::shared_ptr loader; - - { -#if ORTHANC_ENABLE_WASM == 1 - loader.reset(new DicomStructureSetLoader(oracle_, oracle_)); -#else - LockingEmitter::WriterLock lock(lockingEmitter_); - loader.reset(new DicomStructureSetLoader(oracle_, lock.GetOracleObservable())); -#endif - loader->LoadInstance(inInstanceUuid, initiallyVisibleStructures); - } - dicomStructureSetLoaders_[entryKey] = loader; - } - return dicomStructureSetLoaders_[entryKey]; - } - catch (const Orthanc::OrthancException& e) - { - if (e.HasDetails()) - { - LOG(ERROR) << "OrthancException in LoaderCache: " << e.What() << " Details: " << e.GetDetails(); - } - else - { - LOG(ERROR) << "OrthancException in LoaderCache: " << e.What(); - } - throw; - } - catch (const std::exception& e) - { - LOG(ERROR) << "std::exception in LoaderCache: " << e.what(); - throw; - } - catch (...) - { - LOG(ERROR) << "Unknown exception in LoaderCache"; - throw; - } - } - -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - - boost::shared_ptr LoaderCache::GetDicomStructureSetLoader2(std::string instanceUuid) - { - try - { - // normalize keys a little - instanceUuid = Orthanc::Toolbox::StripSpaces(instanceUuid); - Orthanc::Toolbox::ToLowerCase(instanceUuid); - - // find in cache - if (dicomStructureSetLoaders2_.find(instanceUuid) == dicomStructureSetLoaders2_.end()) - { - boost::shared_ptr loader; - boost::shared_ptr structureSet(new DicomStructureSet2()); - boost::shared_ptr rtSlicer(new DicomStructureSetSlicer2(structureSet)); - dicomStructureSetSlicers2_[instanceUuid] = rtSlicer; - dicomStructureSets2_[instanceUuid] = structureSet; // to prevent it from being deleted - { -#if ORTHANC_ENABLE_WASM == 1 - loader.reset(new DicomStructureSetLoader2(*(structureSet.get()), oracle_, oracle_)); -#else - LockingEmitter::WriterLock lock(lockingEmitter_); - // TODO: clarify lifetimes... this is DANGEROUS! - loader.reset(new DicomStructureSetLoader2(*(structureSet.get()), oracle_, lock.GetOracleObservable())); -#endif - loader->LoadInstance(instanceUuid); - } - dicomStructureSetLoaders2_[instanceUuid] = loader; - } - return dicomStructureSetLoaders2_[instanceUuid]; - } - catch (const Orthanc::OrthancException& e) - { - if (e.HasDetails()) - { - LOG(ERROR) << "OrthancException in GetDicomStructureSetLoader2: " << e.What() << " Details: " << e.GetDetails(); - } - else - { - LOG(ERROR) << "OrthancException in GetDicomStructureSetLoader2: " << e.What(); - } - throw; - } - catch (const std::exception& e) - { - LOG(ERROR) << "std::exception in GetDicomStructureSetLoader2: " << e.what(); - throw; - } - catch (...) - { - LOG(ERROR) << "Unknown exception in GetDicomStructureSetLoader2"; - throw; - } - } - -#endif -// BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - - - void LoaderCache::ClearCache() - { -#if ORTHANC_ENABLE_WASM != 1 - LockingEmitter::WriterLock lock(lockingEmitter_); -#endif - -//#ifndef NDEBUG - // ISO way of checking for debug builds - DebugDisplayObjRefCounts(); -//#endif - seriesVolumeProgressiveLoaders_.clear(); - multiframeVolumeLoaders_.clear(); - dicomVolumeImageMPRSlicers_.clear(); - dicomStructureSetLoaders_.clear(); - -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - // order is important! - dicomStructureSetLoaders2_.clear(); - dicomStructureSetSlicers2_.clear(); - dicomStructureSets2_.clear(); -#endif -// BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - } - - template void DebugDisplayObjRefCountsInMap( - const std::string& name, const std::map >& myMap) - { - LOG(TRACE) << "Map \"" << name << "\" ref counts:"; - size_t i = 0; - for (typename std::map >::const_iterator - it = myMap.begin(); it != myMap.end(); ++it) - { - LOG(TRACE) << " element #" << i << ": ref count = " << it->second.use_count(); - i++; - } - } - - void LoaderCache::DebugDisplayObjRefCounts() - { - DebugDisplayObjRefCountsInMap("seriesVolumeProgressiveLoaders_", seriesVolumeProgressiveLoaders_); - DebugDisplayObjRefCountsInMap("multiframeVolumeLoaders_", multiframeVolumeLoaders_); - DebugDisplayObjRefCountsInMap("dicomVolumeImageMPRSlicers_", dicomVolumeImageMPRSlicers_); - DebugDisplayObjRefCountsInMap("dicomStructureSetLoaders_", dicomStructureSetLoaders_); -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - DebugDisplayObjRefCountsInMap("dicomStructureSetLoaders2_", dicomStructureSetLoaders2_); - DebugDisplayObjRefCountsInMap("dicomStructureSetSlicers2_", dicomStructureSetSlicers2_); -#endif -//BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - } -} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/LoaderCache.h --- a/Framework/Loaders/LoaderCache.h Tue Feb 04 15:23:33 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,112 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - -#pragma once - -#include - -#include -#include -#include - -namespace OrthancStone -{ - class OrthancSeriesVolumeProgressiveLoader; - class DicomVolumeImageMPRSlicer; - class DicomStructureSetLoader; -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - class DicomStructureSetLoader2; - class DicomStructureSetSlicer2; - class DicomStructureSet2; -#endif - //BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - class OrthancMultiframeVolumeLoader; - -#if ORTHANC_ENABLE_WASM == 1 - class WebAssemblyOracle; -#else - class ThreadedOracle; - class LockingEmitter; -#endif - - class LoaderCache - { - public: -#if ORTHANC_ENABLE_WASM == 1 - LoaderCache(WebAssemblyOracle& oracle); -#else - LoaderCache(ThreadedOracle& oracle, LockingEmitter& lockingEmitter); -#endif - - boost::shared_ptr - GetSeriesVolumeProgressiveLoader (std::string seriesUuid); - - boost::shared_ptr - GetMultiframeDicomVolumeImageMPRSlicer(std::string instanceUuid); - - boost::shared_ptr - GetMultiframeVolumeLoader(std::string instanceUuid); - - boost::shared_ptr - GetDicomStructureSetLoader( - std::string instanceUuid, - const std::vector& initiallyVisibleStructures); - -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - boost::shared_ptr - GetDicomStructureSetLoader2(std::string instanceUuid); - - boost::shared_ptr - GetDicomStructureSetSlicer2(std::string instanceUuid); -#endif - //BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - - void ClearCache(); - - private: - - void DebugDisplayObjRefCounts(); -#if ORTHANC_ENABLE_WASM == 1 - WebAssemblyOracle& oracle_; -#else - ThreadedOracle& oracle_; - LockingEmitter& lockingEmitter_; -#endif - - std::map > - seriesVolumeProgressiveLoaders_; - std::map > - multiframeVolumeLoaders_; - std::map > - dicomVolumeImageMPRSlicers_; - std::map > - dicomStructureSetLoaders_; -#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - std::map > - dicomStructureSetLoaders2_; - std::map > - dicomStructureSets2_; - std::map > - dicomStructureSetSlicers2_; -#endif - //BGO_ENABLE_DICOMSTRUCTURESETLOADER2 - }; -} - diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/LoaderStateMachine.cpp --- a/Framework/Loaders/LoaderStateMachine.cpp Tue Feb 04 15:23:33 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,211 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - - -#include "LoaderStateMachine.h" - -#include - -namespace OrthancStone -{ - void LoaderStateMachine::State::Handle(const OrthancRestApiCommand::SuccessMessage& message) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - - - void LoaderStateMachine::State::Handle(const GetOrthancImageCommand::SuccessMessage& message) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - - - void LoaderStateMachine::State::Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - - - void LoaderStateMachine::Schedule(OracleCommandWithPayload* command) - { - LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::Schedule()"; - - std::auto_ptr protection(command); - - if (command == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - - if (!command->HasPayload()) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, - "The payload must contain the next state"); - } - pendingCommands_.push_back(protection.release()); - - Step(); - } - - - void LoaderStateMachine::Start() - { - LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::Start()"; - - if (active_) - { - LOG(TRACE) << "LoaderStateMachine::Start() called while active_ is true"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - - active_ = true; - - for (size_t i = 0; i < simultaneousDownloads_; i++) - { - Step(); - } - } - - - void LoaderStateMachine::Step() - { - if (!pendingCommands_.empty() && - activeCommands_ < simultaneousDownloads_) - { - - IOracleCommand* nextCommand = pendingCommands_.front(); - - LOG(TRACE) << " LoaderStateMachine(" << std::hex << this << std::dec << - ")::Step(): activeCommands_ (" << activeCommands_ << - ") < simultaneousDownloads_ (" << simultaneousDownloads_ << - ") --> will Schedule command addr " << std::hex << nextCommand << std::dec; - - oracle_.Schedule(*this, nextCommand); - pendingCommands_.pop_front(); - - activeCommands_++; - } - else - { - LOG(TRACE) << " LoaderStateMachine(" << std::hex << this << std::dec << - ")::Step(): activeCommands_ (" << activeCommands_ << - ") >= simultaneousDownloads_ (" << simultaneousDownloads_ << - ") --> will NOT Schedule command"; - } - } - - - void LoaderStateMachine::Clear() - { - LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::Clear()"; - for (PendingCommands::iterator it = pendingCommands_.begin(); - it != pendingCommands_.end(); ++it) - { - delete *it; - } - - pendingCommands_.clear(); - } - - - void LoaderStateMachine::HandleExceptionMessage(const OracleCommandExceptionMessage& message) - { - LOG(ERROR) << "LoaderStateMachine::HandleExceptionMessage: error in the state machine, stopping all processing"; - LOG(ERROR) << "Error: " << message.GetException().What() << " Details: " << - message.GetException().GetDetails(); - Clear(); - } - - template - void LoaderStateMachine::HandleSuccessMessage(const T& message) - { - LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::HandleSuccessMessage(). Receiver fingerprint = " << GetFingerprint(); - if (activeCommands_ <= 0) { - LOG(ERROR) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::HandleSuccessMessage : activeCommands_ should be > 0 but is: " << activeCommands_; - } - else { - activeCommands_--; - try - { - dynamic_cast(message.GetOrigin().GetPayload()).Handle(message); - Step(); - } - catch (Orthanc::OrthancException& e) - { - LOG(ERROR) << "Error in the state machine, stopping all processing: " << - e.What() << " Details: " << e.GetDetails(); - Clear(); - } - } - } - - - LoaderStateMachine::LoaderStateMachine(IOracle& oracle, - IObservable& oracleObservable) : - IObserver(oracleObservable.GetBroker()), - oracle_(oracle), - oracleObservable_(oracleObservable), - active_(false), - simultaneousDownloads_(4), - activeCommands_(0) - { - LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::LoaderStateMachine()"; - - oracleObservable.RegisterObserverCallback( - new Callable - (*this, &LoaderStateMachine::HandleSuccessMessage)); - - oracleObservable.RegisterObserverCallback( - new Callable - (*this, &LoaderStateMachine::HandleSuccessMessage)); - - oracleObservable.RegisterObserverCallback( - new Callable - (*this, &LoaderStateMachine::HandleSuccessMessage)); - - oracleObservable.RegisterObserverCallback( - new Callable - (*this, &LoaderStateMachine::HandleExceptionMessage)); - } - - LoaderStateMachine::~LoaderStateMachine() - { - oracleObservable_.Unregister(this); - LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::~LoaderStateMachine()"; - Clear(); - } - - void LoaderStateMachine::SetSimultaneousDownloads(unsigned int count) - { - if (active_) - { - LOG(ERROR) << "LoaderStateMachine::SetSimultaneousDownloads called while active_ is true"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - else if (count == 0) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); - } - else - { - simultaneousDownloads_ = count; - } - } -} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/LoaderStateMachine.h --- a/Framework/Loaders/LoaderStateMachine.h Tue Feb 04 15:23:33 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,117 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "../Messages/IObservable.h" -#include "../Messages/IObserver.h" -#include "../Oracle/GetOrthancImageCommand.h" -#include "../Oracle/GetOrthancWebViewerJpegCommand.h" -#include "../Oracle/IOracle.h" -#include "../Oracle/OracleCommandExceptionMessage.h" -#include "../Oracle/OrthancRestApiCommand.h" - -#include - -#include - -namespace OrthancStone -{ - /** - This class is supplied with Oracle commands and will schedule up to - simultaneousDownloads_ of them at the same time, then will schedule the - rest once slots become available. It is used, a.o., by the - OrtancMultiframeVolumeLoader class. - */ - class LoaderStateMachine : public IObserver - { - protected: - class State : public Orthanc::IDynamicObject - { - private: - LoaderStateMachine& that_; - - public: - State(LoaderStateMachine& that) : - that_(that) - { - } - - State(const State& currentState) : - that_(currentState.that_) - { - } - - void Schedule(OracleCommandWithPayload* command) const - { - that_.Schedule(command); - } - - template - T& GetLoader() const - { - return dynamic_cast(that_); - } - - virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message); - - virtual void Handle(const GetOrthancImageCommand::SuccessMessage& message); - - virtual void Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message); - }; - - void Schedule(OracleCommandWithPayload* command); - - void Start(); - - private: - void Step(); - - void Clear(); - - void HandleExceptionMessage(const OracleCommandExceptionMessage& message); - - template - void HandleSuccessMessage(const T& message); - - typedef std::list PendingCommands; - - IOracle& oracle_; - IObservable& oracleObservable_; - bool active_; - unsigned int simultaneousDownloads_; - PendingCommands pendingCommands_; - unsigned int activeCommands_; - - public: - LoaderStateMachine(IOracle& oracle, - IObservable& oracleObservable); - - virtual ~LoaderStateMachine(); - - bool IsActive() const - { - return active_; - } - - void SetSimultaneousDownloads(unsigned int count); - }; -} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/OracleScheduler.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/OracleScheduler.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,557 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "OracleScheduler.h" + +#include "../Oracle/ParseDicomFromFileCommand.h" + +namespace OrthancStone +{ + class OracleScheduler::ReceiverPayload : public Orthanc::IDynamicObject + { + private: + Priority priority_; + boost::weak_ptr receiver_; + std::auto_ptr command_; + + public: + ReceiverPayload(Priority priority, + boost::weak_ptr receiver, + IOracleCommand* command) : + priority_(priority), + receiver_(receiver), + command_(command) + { + if (command == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + } + + Priority GetActivePriority() const + { + return priority_; + } + + boost::weak_ptr GetOriginalReceiver() const + { + return receiver_; + } + + const IOracleCommand& GetOriginalCommand() const + { + assert(command_.get() != NULL); + return *command_; + } + }; + + + class OracleScheduler::ScheduledCommand : public boost::noncopyable + { + private: + boost::weak_ptr receiver_; + std::auto_ptr command_; + + public: + ScheduledCommand(boost::shared_ptr receiver, + IOracleCommand* command) : + receiver_(receiver), + command_(command) + { + if (command == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + } + + boost::weak_ptr GetReceiver() + { + return receiver_; + } + + bool IsSameReceiver(boost::shared_ptr receiver) const + { + boost::shared_ptr lock(receiver_.lock()); + + return (lock && + lock.get() == receiver.get()); + } + + IOracleCommand* WrapCommand(Priority priority) + { + if (command_.get() == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + std::auto_ptr wrapped(command_->Clone()); + dynamic_cast(*wrapped).AcquirePayload(new ReceiverPayload(priority, receiver_, command_.release())); + return wrapped.release(); + } + } + }; + + + + void OracleScheduler::ClearQueue(Queue& queue) + { + for (Queue::iterator it = queue.begin(); it != queue.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + + totalProcessed_ ++; + } + + queue.clear(); + } + + + void OracleScheduler::RemoveReceiverFromQueue(Queue& queue, + boost::shared_ptr receiver) + { + if (!receiver) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + Queue tmp; + + for (Queue::iterator it = queue.begin(); it != queue.end(); ++it) + { + assert(it->second != NULL); + + if (!(it->second->IsSameReceiver(receiver))) + { + // This promise is still active + tmp.insert(std::make_pair(it->first, it->second)); + } + else + { + delete it->second; + + totalProcessed_ ++; + } + } + + queue = tmp; + } + + + void OracleScheduler::CheckInvariants() const + { +#ifndef NDEBUG + /*char buf[1024]; + sprintf(buf, "active: %d %d %d ; pending: %lu %lu %lu", + activeHighPriorityCommands_, activeStandardPriorityCommands_, activeLowPriorityCommands_, + highPriorityQueue_.size(), standardPriorityQueue_.size(), lowPriorityQueue_.size()); + LOG(INFO) << buf;*/ + + assert(activeHighPriorityCommands_ <= maxHighPriorityCommands_); + assert(activeStandardPriorityCommands_ <= maxStandardPriorityCommands_); + assert(activeLowPriorityCommands_ <= maxLowPriorityCommands_); + assert(totalProcessed_ <= totalScheduled_); + + for (Queue::const_iterator it = standardPriorityQueue_.begin(); it != standardPriorityQueue_.end(); ++it) + { + assert(it->first > PRIORITY_HIGH && + it->first < PRIORITY_LOW); + } + + for (Queue::const_iterator it = highPriorityQueue_.begin(); it != highPriorityQueue_.end(); ++it) + { + assert(it->first <= PRIORITY_HIGH); + } + + for (Queue::const_iterator it = lowPriorityQueue_.begin(); it != lowPriorityQueue_.end(); ++it) + { + assert(it->first >= PRIORITY_LOW); + } +#endif + } + + + void OracleScheduler::SpawnFromQueue(Queue& queue, + Priority priority) + { + CheckInvariants(); + + Queue::iterator item = queue.begin(); + assert(item != queue.end()); + + std::auto_ptr command(dynamic_cast(item->second)); + queue.erase(item); + + if (command.get() != NULL) + { + /** + * Only schedule the command for execution in the oracle, if its + * receiver has not been destroyed yet. + **/ + boost::shared_ptr observer(command->GetReceiver().lock()); + if (observer) + { + if (oracle_.Schedule(GetSharedObserver(), command->WrapCommand(priority))) + { + /** + * Executing this code if "Schedule()" returned "false" + * above, will result in a memory leak within + * "OracleScheduler", as the scheduler believes that some + * command is still active (i.e. pending to be executed by + * the oracle), hereby stalling the scheduler during its + * destruction, and not freeing the + * "shared_ptr" of the Stone context (check + * out "sjo-playground/WebViewer/Backend/Leak") + **/ + + switch (priority) + { + case Priority_High: + activeHighPriorityCommands_ ++; + break; + + case Priority_Standard: + activeStandardPriorityCommands_ ++; + break; + + case Priority_Low: + activeLowPriorityCommands_ ++; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + else + { + totalProcessed_ ++; + } + } + } + else + { + LOG(ERROR) << "NULL command, should never happen"; + } + + CheckInvariants(); + } + + + void OracleScheduler::SpawnCommands() + { + // Send as many commands as possible to the oracle + while (!highPriorityQueue_.empty()) + { + if (activeHighPriorityCommands_ < maxHighPriorityCommands_) + { + // First fill the high-priority lane + SpawnFromQueue(highPriorityQueue_, Priority_High); + } + else if (activeStandardPriorityCommands_ < maxStandardPriorityCommands_) + { + // There remain too many high-priority commands for the + // high-priority lane, schedule them to the standard-priority lanes + SpawnFromQueue(highPriorityQueue_, Priority_Standard); + } + else if (activeLowPriorityCommands_ < maxLowPriorityCommands_) + { + SpawnFromQueue(highPriorityQueue_, Priority_Low); + } + else + { + return; // No slot available + } + } + + while (!standardPriorityQueue_.empty()) + { + if (activeStandardPriorityCommands_ < maxStandardPriorityCommands_) + { + SpawnFromQueue(standardPriorityQueue_, Priority_Standard); + } + else if (activeLowPriorityCommands_ < maxLowPriorityCommands_) + { + SpawnFromQueue(standardPriorityQueue_, Priority_Low); + } + else + { + return; + } + } + + while (!lowPriorityQueue_.empty()) + { + if (activeLowPriorityCommands_ < maxLowPriorityCommands_) + { + SpawnFromQueue(lowPriorityQueue_, Priority_Low); + } + else + { + return; + } + } + } + + + void OracleScheduler::RemoveActiveCommand(const ReceiverPayload& payload) + { + CheckInvariants(); + + totalProcessed_ ++; + + switch (payload.GetActivePriority()) + { + case Priority_High: + assert(activeHighPriorityCommands_ > 0); + activeHighPriorityCommands_ --; + break; + + case Priority_Standard: + assert(activeStandardPriorityCommands_ > 0); + activeStandardPriorityCommands_ --; + break; + + case Priority_Low: + assert(activeLowPriorityCommands_ > 0); + activeLowPriorityCommands_ --; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + SpawnCommands(); + + CheckInvariants(); + } + + + void OracleScheduler::Handle(const GetOrthancImageCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + const ReceiverPayload& payload = dynamic_cast(message.GetOrigin().GetPayload()); + + RemoveActiveCommand(payload); + + GetOrthancImageCommand::SuccessMessage bis( + dynamic_cast(payload.GetOriginalCommand()), + message.GetImage(), message.GetMimeType()); + emitter_.EmitMessage(payload.GetOriginalReceiver(), bis); + } + + + void OracleScheduler::Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + const ReceiverPayload& payload = dynamic_cast(message.GetOrigin().GetPayload()); + + RemoveActiveCommand(payload); + + GetOrthancWebViewerJpegCommand::SuccessMessage bis( + dynamic_cast(payload.GetOriginalCommand()), + message.GetImage()); + emitter_.EmitMessage(payload.GetOriginalReceiver(), bis); + } + + + void OracleScheduler::Handle(const HttpCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + const ReceiverPayload& payload = dynamic_cast(message.GetOrigin().GetPayload()); + + RemoveActiveCommand(payload); + + HttpCommand::SuccessMessage bis( + dynamic_cast(payload.GetOriginalCommand()), + message.GetAnswerHeaders(), message.GetAnswer()); + emitter_.EmitMessage(payload.GetOriginalReceiver(), bis); + } + + + void OracleScheduler::Handle(const OrthancRestApiCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + const ReceiverPayload& payload = dynamic_cast(message.GetOrigin().GetPayload()); + + RemoveActiveCommand(payload); + + OrthancRestApiCommand::SuccessMessage bis( + dynamic_cast(payload.GetOriginalCommand()), + message.GetAnswerHeaders(), message.GetAnswer()); + emitter_.EmitMessage(payload.GetOriginalReceiver(), bis); + } + + +#if ORTHANC_ENABLE_DCMTK == 1 + void OracleScheduler::Handle(const ParseDicomSuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + const ReceiverPayload& payload = dynamic_cast(message.GetOrigin().GetPayload()); + + RemoveActiveCommand(payload); + + ParseDicomSuccessMessage bis( + dynamic_cast(payload.GetOriginalCommand()), + message.GetDicom(), message.GetFileSize(), message.HasPixelData()); + emitter_.EmitMessage(payload.GetOriginalReceiver(), bis); + } +#endif + + + void OracleScheduler::Handle(const ReadFileCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + const ReceiverPayload& payload = dynamic_cast(message.GetOrigin().GetPayload()); + + RemoveActiveCommand(payload); + + ReadFileCommand::SuccessMessage bis( + dynamic_cast(payload.GetOriginalCommand()), + message.GetContent()); + emitter_.EmitMessage(payload.GetOriginalReceiver(), bis); + } + + + void OracleScheduler::Handle(const OracleCommandExceptionMessage& message) + { + const OracleCommandBase& command = dynamic_cast(message.GetOrigin()); + + assert(command.HasPayload()); + const ReceiverPayload& payload = dynamic_cast(command.GetPayload()); + + RemoveActiveCommand(payload); + + OracleCommandExceptionMessage bis(payload.GetOriginalCommand(), message.GetException()); + emitter_.EmitMessage(payload.GetOriginalReceiver(), bis); + } + + + OracleScheduler::OracleScheduler(IOracle& oracle, + IMessageEmitter& emitter, + unsigned int maxHighPriority, + unsigned int maxStandardPriority, + unsigned int maxLowPriority) : + oracle_(oracle), + emitter_(emitter), + maxHighPriorityCommands_(maxHighPriority), + maxStandardPriorityCommands_(maxStandardPriority), + maxLowPriorityCommands_(maxLowPriority), + activeHighPriorityCommands_(0), + activeStandardPriorityCommands_(0), + activeLowPriorityCommands_(0), + totalScheduled_(0), + totalProcessed_(0) + { + assert(PRIORITY_HIGH < 0 && + PRIORITY_LOW > 0); + + if (maxLowPriority <= 0) + { + // There must be at least 1 lane available to deal with low-priority commands + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + + + boost::shared_ptr OracleScheduler::Create(IOracle& oracle, + IObservable& oracleObservable, + IMessageEmitter& emitter, + unsigned int maxHighPriority, + unsigned int maxStandardPriority, + unsigned int maxLowPriority) + { + boost::shared_ptr scheduler + (new OracleScheduler(oracle, emitter, maxHighPriority, maxStandardPriority, maxLowPriority)); + scheduler->Register(oracleObservable, &OracleScheduler::Handle); + scheduler->Register(oracleObservable, &OracleScheduler::Handle); + scheduler->Register(oracleObservable, &OracleScheduler::Handle); + scheduler->Register(oracleObservable, &OracleScheduler::Handle); + scheduler->Register(oracleObservable, &OracleScheduler::Handle); + scheduler->Register(oracleObservable, &OracleScheduler::Handle); + +#if ORTHANC_ENABLE_DCMTK == 1 + scheduler->Register(oracleObservable, &OracleScheduler::Handle); +#endif + + return scheduler; + } + + + OracleScheduler::~OracleScheduler() + { + CancelAllRequests(); + } + + + void OracleScheduler::CancelRequests(boost::shared_ptr receiver) + { + RemoveReceiverFromQueue(standardPriorityQueue_, receiver); + RemoveReceiverFromQueue(highPriorityQueue_, receiver); + RemoveReceiverFromQueue(lowPriorityQueue_, receiver); + } + + + void OracleScheduler::CancelAllRequests() + { + ClearQueue(standardPriorityQueue_); + ClearQueue(highPriorityQueue_); + ClearQueue(lowPriorityQueue_); + } + + + void OracleScheduler::Schedule(boost::shared_ptr receiver, + int priority, + IOracleCommand* command /* Takes ownership */) + { + std::auto_ptr pending(new ScheduledCommand(receiver, dynamic_cast(command))); + + /** + * Safeguard to remember that a new "Handle()" method and a call + * to "scheduler->Register()" must be implemented for each + * possible oracle command. + **/ + assert(command->GetType() == IOracleCommand::Type_GetOrthancImage || + command->GetType() == IOracleCommand::Type_GetOrthancWebViewerJpeg || + command->GetType() == IOracleCommand::Type_Http || + command->GetType() == IOracleCommand::Type_OrthancRestApi || + command->GetType() == IOracleCommand::Type_ParseDicomFromFile || + command->GetType() == IOracleCommand::Type_ParseDicomFromWado || + command->GetType() == IOracleCommand::Type_ReadFile); + + if (priority <= PRIORITY_HIGH) + { + highPriorityQueue_.insert(std::make_pair(priority, pending.release())); + } + else if (priority >= PRIORITY_LOW) + { + lowPriorityQueue_.insert(std::make_pair(priority, pending.release())); + } + else + { + standardPriorityQueue_.insert(std::make_pair(priority, pending.release())); + } + + totalScheduled_ ++; + + SpawnCommands(); + } +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/OracleScheduler.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/OracleScheduler.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,167 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#if !defined(ORTHANC_ENABLE_DCMTK) +# error The macro ORTHANC_ENABLE_DCMTK must be defined +#endif + +#include "../Messages/IMessageEmitter.h" +#include "../Messages/ObserverBase.h" +#include "../Oracle/GetOrthancImageCommand.h" +#include "../Oracle/GetOrthancWebViewerJpegCommand.h" +#include "../Oracle/HttpCommand.h" +#include "../Oracle/IOracle.h" +#include "../Oracle/OracleCommandExceptionMessage.h" +#include "../Oracle/OrthancRestApiCommand.h" +#include "../Oracle/ReadFileCommand.h" + +#if ORTHANC_ENABLE_DCMTK == 1 +# include "../Oracle/ParseDicomSuccessMessage.h" +#endif + +namespace OrthancStone +{ + class OracleScheduler : public ObserverBase + { + public: + static const int PRIORITY_HIGH = -1; + static const int PRIORITY_LOW = 100; + + private: + enum Priority + { + Priority_Low, + Priority_Standard, + Priority_High + }; + + class ReceiverPayload; + class ScheduledCommand; + + typedef std::multimap Queue; + + IOracle& oracle_; + IMessageEmitter& emitter_; + Queue standardPriorityQueue_; + Queue highPriorityQueue_; + Queue lowPriorityQueue_; + unsigned int maxHighPriorityCommands_; // Used if priority <= PRIORITY_HIGH + unsigned int maxStandardPriorityCommands_; + unsigned int maxLowPriorityCommands_; // Used if priority >= PRIORITY_LOW + unsigned int activeHighPriorityCommands_; + unsigned int activeStandardPriorityCommands_; + unsigned int activeLowPriorityCommands_; + uint64_t totalScheduled_; + uint64_t totalProcessed_; + + void ClearQueue(Queue& queue); + + void RemoveReceiverFromQueue(Queue& queue, + boost::shared_ptr receiver); + + void CheckInvariants() const; + + void SpawnFromQueue(Queue& queue, + Priority priority); + + void SpawnCommands(); + + void RemoveActiveCommand(const ReceiverPayload& payload); + + void Handle(const GetOrthancImageCommand::SuccessMessage& message); + + void Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message); + + void Handle(const HttpCommand::SuccessMessage& message); + + void Handle(const OrthancRestApiCommand::SuccessMessage& message); + +#if ORTHANC_ENABLE_DCMTK == 1 + void Handle(const ParseDicomSuccessMessage& message); +#endif + + void Handle(const ReadFileCommand::SuccessMessage& message); + + void Handle(const OracleCommandExceptionMessage& message); + + OracleScheduler(IOracle& oracle, + IMessageEmitter& emitter, + unsigned int maxHighPriority, + unsigned int maxStandardPriority, + unsigned int maxLowPriority); + + public: + static boost::shared_ptr Create(IOracle& oracle, + IObservable& oracleObservable, + IMessageEmitter& emitter) + { + return Create(oracle, oracleObservable, emitter, 1, 4, 1); + } + + static boost::shared_ptr Create(IOracle& oracle, + IObservable& oracleObservable, + IMessageEmitter& emitter, + unsigned int maxHighPriority, + unsigned int maxStandardPriority, + unsigned int maxLowPriority); + + ~OracleScheduler(); + + unsigned int GetMaxHighPriorityCommands() const + { + return maxHighPriorityCommands_; + } + + unsigned int GetMaxStandardPriorityCommands() const + { + return maxStandardPriorityCommands_; + } + + unsigned int GetMaxLowPriorityCommands() const + { + return maxLowPriorityCommands_; + } + + uint64_t GetTotalScheduled() const + { + return totalScheduled_; + } + + uint64_t GetTotalProcessed() const + { + return totalProcessed_; + } + + // Cancel the HTTP requests that are still pending in the queues, + // and that are associated with the given receiver. Note that the + // receiver might still receive answers to HTTP requests that were + // already submitted to the oracle. + void CancelRequests(boost::shared_ptr receiver); + + void CancelAllRequests(); + + void Schedule(boost::shared_ptr receiver, + int priority, + IOracleCommand* command /* Takes ownership */); + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/OrthancMultiframeVolumeLoader.cpp --- a/Framework/Loaders/OrthancMultiframeVolumeLoader.cpp Tue Feb 04 15:23:33 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,581 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - - -#include "OrthancMultiframeVolumeLoader.h" - -#include -#include - -namespace OrthancStone -{ - class OrthancMultiframeVolumeLoader::LoadRTDoseGeometry : public LoaderStateMachine::State - { - private: - std::auto_ptr dicom_; - - public: - LoadRTDoseGeometry(OrthancMultiframeVolumeLoader& that, - Orthanc::DicomMap* dicom) : - State(that), - dicom_(dicom) - { - if (dicom == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - - } - - virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) - { - // Complete the DICOM tags with just-received "Grid Frame Offset Vector" - std::string s = Orthanc::Toolbox::StripSpaces(message.GetAnswer()); - dicom_->SetValue(Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR, s, false); - - GetLoader().SetGeometry(*dicom_); - } - }; - - - static std::string GetSopClassUid(const Orthanc::DicomMap& dicom) - { - std::string s; - if (!dicom.LookupStringValue(s, Orthanc::DICOM_TAG_SOP_CLASS_UID, false)) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, - "DICOM file without SOP class UID"); - } - else - { - return s; - } - } - - - class OrthancMultiframeVolumeLoader::LoadGeometry : public State - { - public: - LoadGeometry(OrthancMultiframeVolumeLoader& that) : - State(that) - { - } - - virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) - { - OrthancMultiframeVolumeLoader& loader = GetLoader(); - - Json::Value body; - message.ParseJsonBody(body); - - if (body.type() != Json::objectValue) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); - } - - std::auto_ptr dicom(new Orthanc::DicomMap); - dicom->FromDicomAsJson(body); - - if (StringToSopClassUid(GetSopClassUid(*dicom)) == SopClassUid_RTDose) - { - // Download the "Grid Frame Offset Vector" DICOM tag, that is - // mandatory for RT-DOSE, but is too long to be returned by default - - std::auto_ptr command(new OrthancRestApiCommand); - command->SetUri("/instances/" + loader.GetInstanceId() + "/content/" + - Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR.Format()); - command->SetPayload(new LoadRTDoseGeometry(loader, dicom.release())); - - Schedule(command.release()); - } - else - { - loader.SetGeometry(*dicom); - } - } - }; - - class OrthancMultiframeVolumeLoader::LoadTransferSyntax : public State - { - public: - LoadTransferSyntax(OrthancMultiframeVolumeLoader& that) : - State(that) - { - } - - virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) - { - GetLoader().SetTransferSyntax(message.GetAnswer()); - } - }; - - class OrthancMultiframeVolumeLoader::LoadUncompressedPixelData : public State - { - public: - LoadUncompressedPixelData(OrthancMultiframeVolumeLoader& that) : - State(that) - { - } - - virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message) - { - GetLoader().SetUncompressedPixelData(message.GetAnswer()); - } - }; - - const std::string& OrthancMultiframeVolumeLoader::GetInstanceId() const - { - if (IsActive()) - { - return instanceId_; - } - else - { - LOG(ERROR) << "OrthancMultiframeVolumeLoader::GetInstanceId(): (!IsActive())"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - } - - void OrthancMultiframeVolumeLoader::ScheduleFrameDownloads() - { - if (transferSyntaxUid_.empty() || - !volume_->HasGeometry()) - { - return; - } - /* - 1.2.840.10008.1.2 Implicit VR Endian: Default Transfer Syntax for DICOM - 1.2.840.10008.1.2.1 Explicit VR Little Endian - 1.2.840.10008.1.2.2 Explicit VR Big Endian - - See https://www.dicomlibrary.com/dicom/transfer-syntax/ - */ - if (transferSyntaxUid_ == "1.2.840.10008.1.2" || - transferSyntaxUid_ == "1.2.840.10008.1.2.1" || - transferSyntaxUid_ == "1.2.840.10008.1.2.2") - { - std::auto_ptr command(new OrthancRestApiCommand); - command->SetHttpHeader("Accept-Encoding", "gzip"); - command->SetUri("/instances/" + instanceId_ + "/content/" + - Orthanc::DICOM_TAG_PIXEL_DATA.Format() + "/0"); - command->SetPayload(new LoadUncompressedPixelData(*this)); - Schedule(command.release()); - } - else - { - throw Orthanc::OrthancException( - Orthanc::ErrorCode_NotImplemented, - "No support for multiframe instances with transfer syntax: " + transferSyntaxUid_); - } - } - - void OrthancMultiframeVolumeLoader::SetTransferSyntax(const std::string& transferSyntax) - { - transferSyntaxUid_ = Orthanc::Toolbox::StripSpaces(transferSyntax); - ScheduleFrameDownloads(); - } - - void OrthancMultiframeVolumeLoader::SetGeometry(const Orthanc::DicomMap& dicom) - { - DicomInstanceParameters parameters(dicom); - volume_->SetDicomParameters(parameters); - - Orthanc::PixelFormat format; - if (!parameters.GetImageInformation().ExtractPixelFormat(format, true)) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - - double spacingZ; - switch (parameters.GetSopClassUid()) - { - case SopClassUid_RTDose: - spacingZ = parameters.GetThickness(); - break; - - default: - throw Orthanc::OrthancException( - Orthanc::ErrorCode_NotImplemented, - "No support for multiframe instances with SOP class UID: " + GetSopClassUid(dicom)); - } - - const unsigned int width = parameters.GetImageInformation().GetWidth(); - const unsigned int height = parameters.GetImageInformation().GetHeight(); - const unsigned int depth = parameters.GetImageInformation().GetNumberOfFrames(); - - { - VolumeImageGeometry geometry; - geometry.SetSizeInVoxels(width, height, depth); - geometry.SetAxialGeometry(parameters.GetGeometry()); - geometry.SetVoxelDimensions(parameters.GetPixelSpacingX(), - parameters.GetPixelSpacingY(), spacingZ); - volume_->Initialize(geometry, format, true /* Do compute range */); - } - - volume_->GetPixelData().Clear(); - - ScheduleFrameDownloads(); - - - - BroadcastMessage(DicomVolumeImage::GeometryReadyMessage(*volume_)); - } - - - ORTHANC_FORCE_INLINE - static void CopyPixel(uint32_t& target, const void* source) - { - // TODO - check alignement? - target = le32toh(*reinterpret_cast(source)); - } - - ORTHANC_FORCE_INLINE - static void CopyPixel(uint16_t& target, const void* source) - { - // TODO - check alignement? - target = le16toh(*reinterpret_cast(source)); - } - - ORTHANC_FORCE_INLINE - static void CopyPixel(int16_t& target, const void* source) - { - // byte swapping is the same for unsigned and signed integers - // (the sign bit is always stored with the MSByte) - uint16_t* targetUp = reinterpret_cast(&target); - CopyPixel(*targetUp, source); - } - - template - void OrthancMultiframeVolumeLoader::CopyPixelDataAndComputeDistribution( - const std::string& pixelData, std::map& distribution) - { - ImageBuffer3D& target = volume_->GetPixelData(); - - const unsigned int bpp = target.GetBytesPerPixel(); - const unsigned int width = target.GetWidth(); - const unsigned int height = target.GetHeight(); - const unsigned int depth = target.GetDepth(); - - if (pixelData.size() != bpp * width * height * depth) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, - "The pixel data has not the proper size"); - } - - if (pixelData.empty()) - { - return; - } - - // first pass to initialize map - { - const uint8_t* source = reinterpret_cast(pixelData.c_str()); - - for (unsigned int z = 0; z < depth; z++) - { - for (unsigned int y = 0; y < height; y++) - { - for (unsigned int x = 0; x < width; x++) - { - T value; - CopyPixel(value, source); - distribution[value] = 0; - source += bpp; - } - } - } - } - - { - const uint8_t* source = reinterpret_cast(pixelData.c_str()); - - for (unsigned int z = 0; z < depth; z++) - { - ImageBuffer3D::SliceWriter writer(target, VolumeProjection_Axial, z); - - assert(writer.GetAccessor().GetWidth() == width && - writer.GetAccessor().GetHeight() == height); - - for (unsigned int y = 0; y < height; y++) - { - assert(sizeof(T) == Orthanc::GetBytesPerPixel(target.GetFormat())); - - T* target = reinterpret_cast(writer.GetAccessor().GetRow(y)); - - for (unsigned int x = 0; x < width; x++) - { - CopyPixel(*target, source); - - distribution[*target] += 1; - - target++; - source += bpp; - } - } - } - } - } - - template - void OrthancMultiframeVolumeLoader::ComputeMinMaxWithOutlierRejection( - const std::map& distribution) - { - if (distribution.size() == 0) - { - LOG(ERROR) << "ComputeMinMaxWithOutlierRejection -- Volume image empty."; - } - else - { - ImageBuffer3D& target = volume_->GetPixelData(); - - const uint64_t bpp = target.GetBytesPerPixel(); - const uint64_t width = target.GetWidth(); - const uint64_t height = target.GetHeight(); - const uint64_t depth = target.GetDepth(); - const uint64_t voxelCount = width * height * depth; - - // now that we have distribution[pixelValue] == numberOfPixelsWithValue - // compute number of values and check (assertion) that it is equal to - // width * height * depth - { - typename std::map::const_iterator it = distribution.begin(); - uint64_t totalCount = 0; - distributionRawMin_ = static_cast(it->first); - - while (it != distribution.end()) - { - T pixelValue = it->first; - uint64_t count = it->second; - totalCount += count; - it++; - if (it == distribution.end()) - distributionRawMax_ = static_cast(pixelValue); - } - LOG(INFO) << "Volume image. First distribution value = " - << static_cast(distributionRawMin_) - << " | Last distribution value = " - << static_cast(distributionRawMax_); - - if (totalCount != voxelCount) - { - LOG(ERROR) << "Internal error in dose distribution computation. TC (" - << totalCount << ") != VoxC (" << voxelCount; - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - } - - // compute the number of voxels to reject at each end of the distribution - uint64_t endRejectionCount = static_cast( - outliersHalfRejectionRate_ * voxelCount); - - if (endRejectionCount > voxelCount) - { - LOG(ERROR) << "Internal error in dose distribution computation." - << " endRejectionCount = " << endRejectionCount - << " | voxelCount = " << voxelCount; - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - - // this will contain the actual distribution minimum after outlier - // rejection - T resultMin = 0; - - // then start from start and remove pixel values up to - // endRejectionCount voxels rejected - { - typename std::map::const_iterator it = distribution.begin(); - - uint64_t currentCount = 0; - - while (it != distribution.end()) - { - T pixelValue = it->first; - uint64_t count = it->second; - - // if this pixelValue crosses the rejection threshold, let's set it - // and exit the loop - if ((currentCount <= endRejectionCount) && - (currentCount + count > endRejectionCount)) - { - resultMin = pixelValue; - break; - } - else - { - currentCount += count; - } - // and continue walking along the distribution - it++; - } - } - - // this will contain the actual distribution maximum after outlier - // rejection - T resultMax = 0; - // now start from END and remove pixel values up to - // endRejectionCount voxels rejected - { - typename std::map::const_reverse_iterator it = distribution.rbegin(); - - uint64_t currentCount = 0; - - while (it != distribution.rend()) - { - T pixelValue = it->first; - uint64_t count = it->second; - - if ((currentCount <= endRejectionCount) && - (currentCount + count > endRejectionCount)) - { - resultMax = pixelValue; - break; - } - else - { - currentCount += count; - } - // and continue walking along the distribution - it++; - } - } - if (resultMin > resultMax) - { - LOG(ERROR) << "Internal error in dose distribution computation! " << - "resultMin (" << resultMin << ") > resultMax (" << resultMax << ")"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - computedDistributionMin_ = static_cast(resultMin); - computedDistributionMax_ = static_cast(resultMax); - } - } - - template - void OrthancMultiframeVolumeLoader::CopyPixelDataAndComputeMinMax( - const std::string& pixelData) - { - std::map distribution; - CopyPixelDataAndComputeDistribution(pixelData, distribution); - ComputeMinMaxWithOutlierRejection(distribution); - } - - void OrthancMultiframeVolumeLoader::SetUncompressedPixelData(const std::string& pixelData) - { - switch (volume_->GetPixelData().GetFormat()) - { - case Orthanc::PixelFormat_Grayscale32: - CopyPixelDataAndComputeMinMax(pixelData); - break; - case Orthanc::PixelFormat_Grayscale16: - CopyPixelDataAndComputeMinMax(pixelData); - break; - case Orthanc::PixelFormat_SignedGrayscale16: - CopyPixelDataAndComputeMinMax(pixelData); - break; - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - - volume_->IncrementRevision(); - - pixelDataLoaded_ = true; - BroadcastMessage(DicomVolumeImage::ContentUpdatedMessage(*volume_)); - } - - bool OrthancMultiframeVolumeLoader::HasGeometry() const - { - return volume_->HasGeometry(); - } - - const OrthancStone::VolumeImageGeometry& OrthancMultiframeVolumeLoader::GetImageGeometry() const - { - return volume_->GetGeometry(); - } - - OrthancMultiframeVolumeLoader::OrthancMultiframeVolumeLoader( - boost::shared_ptr volume, - IOracle& oracle, - IObservable& oracleObservable, - float outliersHalfRejectionRate) : - LoaderStateMachine(oracle, oracleObservable), - IObservable(oracleObservable.GetBroker()), - volume_(volume), - pixelDataLoaded_(false), - outliersHalfRejectionRate_(outliersHalfRejectionRate), - distributionRawMin_(0), - distributionRawMax_(0), - computedDistributionMin_(0), - computedDistributionMax_(0) - { - if (volume.get() == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - } - - OrthancMultiframeVolumeLoader::~OrthancMultiframeVolumeLoader() - { - LOG(TRACE) << "OrthancMultiframeVolumeLoader::~OrthancMultiframeVolumeLoader()"; - } - - - void OrthancMultiframeVolumeLoader::GetDistributionMinMax - (float& minValue, float& maxValue) const - { - if (distributionRawMin_ == 0 && distributionRawMax_ == 0) - { - LOG(WARNING) << "GetDistributionMinMaxWithOutliersRejection called before computation!"; - } - minValue = distributionRawMin_; - maxValue = distributionRawMax_; - } - - void OrthancMultiframeVolumeLoader::GetDistributionMinMaxWithOutliersRejection - (float& minValue, float& maxValue) const - { - if (computedDistributionMin_ == 0 && computedDistributionMax_ == 0) - { - LOG(WARNING) << "GetDistributionMinMaxWithOutliersRejection called before computation!"; - } - minValue = computedDistributionMin_; - maxValue = computedDistributionMax_; - } - - void OrthancMultiframeVolumeLoader::LoadInstance(const std::string& instanceId) - { - Start(); - - instanceId_ = instanceId; - - { - std::auto_ptr command(new OrthancRestApiCommand); - command->SetHttpHeader("Accept-Encoding", "gzip"); - command->SetUri("/instances/" + instanceId + "/tags"); - command->SetPayload(new LoadGeometry(*this)); - Schedule(command.release()); - } - - { - std::auto_ptr command(new OrthancRestApiCommand); - command->SetUri("/instances/" + instanceId + "/metadata/TransferSyntax"); - command->SetPayload(new LoadTransferSyntax(*this)); - Schedule(command.release()); - } - } -} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/OrthancMultiframeVolumeLoader.h --- a/Framework/Loaders/OrthancMultiframeVolumeLoader.h Tue Feb 04 15:23:33 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,116 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "LoaderStateMachine.h" -#include "../Volumes/DicomVolumeImage.h" - -#include - -namespace OrthancStone -{ - class OrthancMultiframeVolumeLoader : - public LoaderStateMachine, - public IObservable, - public IGeometryProvider - { - private: - class LoadRTDoseGeometry; - class LoadGeometry; - class LoadTransferSyntax; - class LoadUncompressedPixelData; - - boost::shared_ptr volume_; - std::string instanceId_; - std::string transferSyntaxUid_; - bool pixelDataLoaded_; - float outliersHalfRejectionRate_; - float distributionRawMin_; - float distributionRawMax_; - float computedDistributionMin_; - float computedDistributionMax_; - - const std::string& GetInstanceId() const; - - void ScheduleFrameDownloads(); - - void SetTransferSyntax(const std::string& transferSyntax); - - void SetGeometry(const Orthanc::DicomMap& dicom); - - - /** - This method will : - - - copy the pixel values from the response to the volume image - - compute the maximum and minimum value while discarding the - outliersHalfRejectionRate_ fraction of the outliers from both the start - and the end of the distribution. - - In English, this means that, if the volume dataset contains a few extreme - values very different from the rest (outliers) that we want to get rid of, - this method allows to do so. - - If you supply 0.005, for instance, it means 1% of the extreme values will - be rejected (0.5% on each side of the distribution) - */ - template - void CopyPixelDataAndComputeMinMax(const std::string& pixelData); - - /** Service method for CopyPixelDataAndComputeMinMax*/ - template - void CopyPixelDataAndComputeDistribution( - const std::string& pixelData, - std::map& distribution); - - /** Service method for CopyPixelDataAndComputeMinMax*/ - template - void ComputeMinMaxWithOutlierRejection( - const std::map& distribution); - - void SetUncompressedPixelData(const std::string& pixelData); - - virtual bool HasGeometry() const ORTHANC_OVERRIDE; - virtual const VolumeImageGeometry& GetImageGeometry() const ORTHANC_OVERRIDE; - - public: - OrthancMultiframeVolumeLoader(boost::shared_ptr volume, - IOracle& oracle, - IObservable& oracleObservable, - float outliersHalfRejectionRate = 0.0005); - - virtual ~OrthancMultiframeVolumeLoader(); - - bool IsPixelDataLoaded() const - { - return pixelDataLoaded_; - } - - void GetDistributionMinMax - (float& minValue, float& maxValue) const; - - void GetDistributionMinMaxWithOutliersRejection - (float& minValue, float& maxValue) const; - - void LoadInstance(const std::string& instanceId); - }; -} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp --- a/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp Tue Feb 04 15:23:33 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,506 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - - -#include "OrthancSeriesVolumeProgressiveLoader.h" - -#include "../Toolbox/GeometryToolbox.h" -#include "../Volumes/DicomVolumeImageMPRSlicer.h" -#include "BasicFetchingItemsSorter.h" -#include "BasicFetchingStrategy.h" - -#include -#include - -namespace OrthancStone -{ - class OrthancSeriesVolumeProgressiveLoader::ExtractedSlice : public DicomVolumeImageMPRSlicer::Slice - { - private: - const OrthancSeriesVolumeProgressiveLoader& that_; - - public: - ExtractedSlice(const OrthancSeriesVolumeProgressiveLoader& that, - const CoordinateSystem3D& plane) : - DicomVolumeImageMPRSlicer::Slice(*that.volume_, plane), - that_(that) - { - if (IsValid()) - { - if (GetProjection() == VolumeProjection_Axial) - { - // For coronal and sagittal projections, we take the global - // revision of the volume because even if a single slice changes, - // this means the projection will yield a different result --> - // we must increase the revision as soon as any slice changes - SetRevision(that_.seriesGeometry_.GetSliceRevision(GetSliceIndex())); - } - - if (that_.strategy_.get() != NULL && - GetProjection() == VolumeProjection_Axial) - { - that_.strategy_->SetCurrent(GetSliceIndex()); - } - } - } - }; - - - - void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSlice(size_t index, - const DicomInstanceParameters& reference) const - { - const DicomInstanceParameters& slice = *slices_[index]; - - if (!GeometryToolbox::IsParallel( - reference.GetGeometry().GetNormal(), - slice.GetGeometry().GetNormal())) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, - "A slice in the volume image is not parallel to the others"); - } - - if (reference.GetExpectedPixelFormat() != slice.GetExpectedPixelFormat()) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat, - "The pixel format changes across the slices of the volume image"); - } - - if (reference.GetImageInformation().GetWidth() != slice.GetImageInformation().GetWidth() || - reference.GetImageInformation().GetHeight() != slice.GetImageInformation().GetHeight()) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize, - "The width/height of slices are not constant in the volume image"); - } - - if (!LinearAlgebra::IsNear(reference.GetPixelSpacingX(), slice.GetPixelSpacingX()) || - !LinearAlgebra::IsNear(reference.GetPixelSpacingY(), slice.GetPixelSpacingY())) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, - "The pixel spacing of the slices change across the volume image"); - } - } - - - void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckVolume() const - { - for (size_t i = 0; i < slices_.size(); i++) - { - assert(slices_[i] != NULL); - if (slices_[i]->GetImageInformation().GetNumberOfFrames() != 1) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, - "This class does not support multi-frame images"); - } - } - - if (slices_.size() != 0) - { - const DicomInstanceParameters& reference = *slices_[0]; - - for (size_t i = 1; i < slices_.size(); i++) - { - CheckSlice(i, reference); - } - } - } - - - void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::Clear() - { - for (size_t i = 0; i < slices_.size(); i++) - { - assert(slices_[i] != NULL); - delete slices_[i]; - } - - slices_.clear(); - slicesRevision_.clear(); - } - - - void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSliceIndex(size_t index) const - { - if (!HasGeometry()) - { - LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSliceIndex(size_t index): (!HasGeometry())"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - else if (index >= slices_.size()) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); - } - else - { - assert(slices_.size() == GetImageGeometry().GetDepth() && - slices_.size() == slicesRevision_.size()); - } - } - - - // WARNING: The payload of "slices" must be of class "DicomInstanceParameters" - // (called with the slices created in LoadGeometry) - void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::ComputeGeometry(SlicesSorter& slices) - { - Clear(); - - if (!slices.Sort()) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, - "Cannot sort the 3D slices of a DICOM series"); - } - - if (slices.GetSlicesCount() == 0) - { - geometry_.reset(new VolumeImageGeometry); - } - else - { - slices_.reserve(slices.GetSlicesCount()); - slicesRevision_.resize(slices.GetSlicesCount(), 0); - - for (size_t i = 0; i < slices.GetSlicesCount(); i++) - { - const DicomInstanceParameters& slice = - dynamic_cast(slices.GetSlicePayload(i)); - slices_.push_back(new DicomInstanceParameters(slice)); - } - - CheckVolume(); - - const double spacingZ = slices.ComputeSpacingBetweenSlices(); - LOG(INFO) << "Computed spacing between slices: " << spacingZ << "mm"; - - const DicomInstanceParameters& parameters = *slices_[0]; - - geometry_.reset(new VolumeImageGeometry); - geometry_->SetSizeInVoxels(parameters.GetImageInformation().GetWidth(), - parameters.GetImageInformation().GetHeight(), - static_cast(slices.GetSlicesCount())); - geometry_->SetAxialGeometry(slices.GetSliceGeometry(0)); - geometry_->SetVoxelDimensions(parameters.GetPixelSpacingX(), - parameters.GetPixelSpacingY(), spacingZ); - } - } - - - const VolumeImageGeometry& OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetImageGeometry() const - { - if (!HasGeometry()) - { - LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetImageGeometry(): (!HasGeometry())"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - else - { - assert(slices_.size() == geometry_->GetDepth()); - return *geometry_; - } - } - - - const DicomInstanceParameters& OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetSliceParameters(size_t index) const - { - CheckSliceIndex(index); - return *slices_[index]; - } - - - uint64_t OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetSliceRevision(size_t index) const - { - CheckSliceIndex(index); - return slicesRevision_[index]; - } - - - void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::IncrementSliceRevision(size_t index) - { - CheckSliceIndex(index); - slicesRevision_[index] ++; - } - - - static unsigned int GetSliceIndexPayload(const OracleCommandWithPayload& command) - { - return dynamic_cast< const Orthanc::SingleValueObject& >(command.GetPayload()).GetValue(); - } - - - void OrthancSeriesVolumeProgressiveLoader::ScheduleNextSliceDownload() - { - assert(strategy_.get() != NULL); - - unsigned int sliceIndex, quality; - - if (strategy_->GetNext(sliceIndex, quality)) - { - assert(quality <= BEST_QUALITY); - - const DicomInstanceParameters& slice = seriesGeometry_.GetSliceParameters(sliceIndex); - - const std::string& instance = slice.GetOrthancInstanceIdentifier(); - if (instance.empty()) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - - std::auto_ptr command; - - if (quality == BEST_QUALITY) - { - std::auto_ptr tmp(new GetOrthancImageCommand); - // TODO: review the following comment. - // - Commented out by bgo on 2019-07-19 | reason: Alain has seen cases - // where gzipping the uint16 image took 11 sec to produce 5mb. - // The unzipped request was much much faster. - // - Re-enabled on 2019-07-30. Reason: in Web Assembly, the browser - // does not use the Accept-Encoding header and always requests - // compression. Furthermore, NOT - tmp->SetHttpHeader("Accept-Encoding", "gzip"); - tmp->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam))); - tmp->SetInstanceUri(instance, slice.GetExpectedPixelFormat()); - tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat()); - command.reset(tmp.release()); - } - else - { - std::auto_ptr tmp(new GetOrthancWebViewerJpegCommand); - // TODO: review the following comment. Commented out by bgo on 2019-07-19 - // (gzip for jpeg seems overkill) - //tmp->SetHttpHeader("Accept-Encoding", "gzip"); - tmp->SetInstance(instance); - tmp->SetQuality((quality == 0 ? 50 : 90)); - tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat()); - command.reset(tmp.release()); - } - - command->SetPayload(new Orthanc::SingleValueObject(sliceIndex)); - oracle_.Schedule(*this, command.release()); - } - else - { - // loading is finished! - volumeImageReadyInHighQuality_ = true; - BroadcastMessage(OrthancSeriesVolumeProgressiveLoader::VolumeImageReadyInHighQuality(*this)); - } - } - -/** - This is called in response to GET "/series/XXXXXXXXXXXXX/instances-tags" -*/ - void OrthancSeriesVolumeProgressiveLoader::LoadGeometry(const OrthancRestApiCommand::SuccessMessage& message) - { - Json::Value body; - message.ParseJsonBody(body); - - if (body.type() != Json::objectValue) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); - } - - { - Json::Value::Members instances = body.getMemberNames(); - - SlicesSorter slices; - - for (size_t i = 0; i < instances.size(); i++) - { - Orthanc::DicomMap dicom; - dicom.FromDicomAsJson(body[instances[i]]); - - std::auto_ptr instance(new DicomInstanceParameters(dicom)); - instance->SetOrthancInstanceIdentifier(instances[i]); - - // the 3D plane corresponding to the slice - CoordinateSystem3D geometry = instance->GetGeometry(); - slices.AddSlice(geometry, instance.release()); - } - - seriesGeometry_.ComputeGeometry(slices); - } - - size_t slicesCount = seriesGeometry_.GetImageGeometry().GetDepth(); - - if (slicesCount == 0) - { - volume_->Initialize(seriesGeometry_.GetImageGeometry(), Orthanc::PixelFormat_Grayscale8); - } - else - { - const DicomInstanceParameters& parameters = seriesGeometry_.GetSliceParameters(0); - - volume_->Initialize(seriesGeometry_.GetImageGeometry(), parameters.GetExpectedPixelFormat()); - volume_->SetDicomParameters(parameters); - volume_->GetPixelData().Clear(); - - strategy_.reset(new BasicFetchingStrategy(sorter_->CreateSorter(static_cast(slicesCount)), BEST_QUALITY)); - - assert(simultaneousDownloads_ != 0); - for (unsigned int i = 0; i < simultaneousDownloads_; i++) - { - ScheduleNextSliceDownload(); - } - } - - slicesQuality_.resize(slicesCount, 0); - - BroadcastMessage(DicomVolumeImage::GeometryReadyMessage(*volume_)); - } - - - void OrthancSeriesVolumeProgressiveLoader::SetSliceContent(unsigned int sliceIndex, - const Orthanc::ImageAccessor& image, - unsigned int quality) - { - assert(sliceIndex < slicesQuality_.size() && - slicesQuality_.size() == volume_->GetPixelData().GetDepth()); - - if (quality >= slicesQuality_[sliceIndex]) - { - { - ImageBuffer3D::SliceWriter writer(volume_->GetPixelData(), VolumeProjection_Axial, sliceIndex); - Orthanc::ImageProcessing::Copy(writer.GetAccessor(), image); - } - - volume_->IncrementRevision(); - seriesGeometry_.IncrementSliceRevision(sliceIndex); - slicesQuality_[sliceIndex] = quality; - - BroadcastMessage(DicomVolumeImage::ContentUpdatedMessage(*volume_)); - } - - ScheduleNextSliceDownload(); - } - - - void OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent(const GetOrthancImageCommand::SuccessMessage& message) - { - SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), BEST_QUALITY); - } - - - void OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent(const GetOrthancWebViewerJpegCommand::SuccessMessage& message) - { - unsigned int quality; - - switch (message.GetOrigin().GetQuality()) - { - case 50: - quality = LOW_QUALITY; - break; - - case 90: - quality = MIDDLE_QUALITY; - break; - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - - SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), quality); - } - - - OrthancSeriesVolumeProgressiveLoader::OrthancSeriesVolumeProgressiveLoader(const boost::shared_ptr& volume, - IOracle& oracle, - IObservable& oracleObservable) : - IObserver(oracleObservable.GetBroker()), - IObservable(oracleObservable.GetBroker()), - oracle_(oracle), - oracleObservable_(oracleObservable), - active_(false), - simultaneousDownloads_(4), - volume_(volume), - sorter_(new BasicFetchingItemsSorter::Factory), - volumeImageReadyInHighQuality_(false) - { - oracleObservable.RegisterObserverCallback( - new Callable - (*this, &OrthancSeriesVolumeProgressiveLoader::LoadGeometry)); - - oracleObservable.RegisterObserverCallback( - new Callable - (*this, &OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent)); - - oracleObservable.RegisterObserverCallback( - new Callable - (*this, &OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent)); - } - - OrthancSeriesVolumeProgressiveLoader::~OrthancSeriesVolumeProgressiveLoader() - { - oracleObservable_.Unregister(this); - LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::~OrthancSeriesVolumeProgressiveLoader()"; - } - - void OrthancSeriesVolumeProgressiveLoader::SetSimultaneousDownloads(unsigned int count) - { - if (active_) - { - LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::SetSimultaneousDownloads(): (active_)"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - else if (count == 0) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); - } - else - { - simultaneousDownloads_ = count; - } - } - - - void OrthancSeriesVolumeProgressiveLoader::LoadSeries(const std::string& seriesId) - { -// LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries seriesId=" << seriesId; - if (active_) - { -// LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries NOT ACTIVE! --> ERROR"; - LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries(const std::string& seriesId): (active_)"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - else - { - active_ = true; - - std::auto_ptr command(new OrthancRestApiCommand); - command->SetUri("/series/" + seriesId + "/instances-tags"); - -// LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries about to call oracle_.Schedule"; - oracle_.Schedule(*this, command.release()); -// LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries called oracle_.Schedule"; - } - } - - - IVolumeSlicer::IExtractedSlice* - OrthancSeriesVolumeProgressiveLoader::ExtractSlice(const CoordinateSystem3D& cuttingPlane) - { - if (volume_->HasGeometry()) - { - return new ExtractedSlice(*this, cuttingPlane); - } - else - { - return new IVolumeSlicer::InvalidSlice; - } - } -} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h --- a/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h Tue Feb 04 15:23:33 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,165 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "../Messages/IObservable.h" -#include "../Messages/IObserver.h" -#include "../Oracle/GetOrthancImageCommand.h" -#include "../Oracle/GetOrthancWebViewerJpegCommand.h" -#include "../Oracle/IOracle.h" -#include "../Oracle/OrthancRestApiCommand.h" -#include "../Toolbox/SlicesSorter.h" -#include "../Volumes/DicomVolumeImage.h" -#include "../Volumes/IVolumeSlicer.h" -#include "IFetchingItemsSorter.h" -#include "IFetchingStrategy.h" - -#include - -namespace OrthancStone -{ - /** - This class is used to manage the progressive loading of a volume that - is stored in a Dicom series. - */ - class OrthancSeriesVolumeProgressiveLoader : - public IObserver, - public IObservable, - public IVolumeSlicer, - public IGeometryProvider - { - private: - static const unsigned int LOW_QUALITY = 0; - static const unsigned int MIDDLE_QUALITY = 1; - static const unsigned int BEST_QUALITY = 2; - - class ExtractedSlice; - - /** Helper class internal to OrthancSeriesVolumeProgressiveLoader */ - class SeriesGeometry : public boost::noncopyable - { - private: - void CheckSlice(size_t index, - const DicomInstanceParameters& reference) const; - - void CheckVolume() const; - - void Clear(); - - void CheckSliceIndex(size_t index) const; - - std::auto_ptr geometry_; - std::vector slices_; - std::vector slicesRevision_; - - public: - ~SeriesGeometry() - { - Clear(); - } - - void ComputeGeometry(SlicesSorter& slices); - - virtual bool HasGeometry() const - { - return geometry_.get() != NULL; - } - - virtual const VolumeImageGeometry& GetImageGeometry() const; - - const DicomInstanceParameters& GetSliceParameters(size_t index) const; - - uint64_t GetSliceRevision(size_t index) const; - - void IncrementSliceRevision(size_t index); - }; - - void ScheduleNextSliceDownload(); - - void LoadGeometry(const OrthancRestApiCommand::SuccessMessage& message); - - void SetSliceContent(unsigned int sliceIndex, - const Orthanc::ImageAccessor& image, - unsigned int quality); - - void LoadBestQualitySliceContent(const GetOrthancImageCommand::SuccessMessage& message); - - void LoadJpegSliceContent(const GetOrthancWebViewerJpegCommand::SuccessMessage& message); - - IOracle& oracle_; - IObservable& oracleObservable_; - bool active_; - unsigned int simultaneousDownloads_; - SeriesGeometry seriesGeometry_; - boost::shared_ptr volume_; - std::auto_ptr sorter_; - std::auto_ptr strategy_; - std::vector slicesQuality_; - bool volumeImageReadyInHighQuality_; - - - public: - ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, VolumeImageReadyInHighQuality, OrthancSeriesVolumeProgressiveLoader); - - - OrthancSeriesVolumeProgressiveLoader(const boost::shared_ptr& volume, - IOracle& oracle, - IObservable& oracleObservable); - - virtual ~OrthancSeriesVolumeProgressiveLoader(); - - void SetSimultaneousDownloads(unsigned int count); - - bool IsVolumeImageReadyInHighQuality() const - { - return volumeImageReadyInHighQuality_; - } - - void LoadSeries(const std::string& seriesId); - - /** - This getter is used by clients that do not receive the geometry through - subscribing, for instance if they are created or listening only AFTER the - "geometry loaded" message is broadcast - */ - bool HasGeometry() const ORTHANC_OVERRIDE - { - return seriesGeometry_.HasGeometry(); - } - - /** - Same remark as HasGeometry - */ - const VolumeImageGeometry& GetImageGeometry() const ORTHANC_OVERRIDE - { - return seriesGeometry_.GetImageGeometry(); - } - - /** - When a slice is requested, the strategy algorithm (that defines the - sequence of resources to be loaded from the server) is modified to - take into account this request (this is done in the ExtractedSlice ctor) - */ - virtual IExtractedSlice* - ExtractSlice(const CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE; - }; -} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/SeriesFramesLoader.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/SeriesFramesLoader.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,548 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "SeriesFramesLoader.h" + +#include "../Oracle/ParseDicomFromFileCommand.h" +#include "../Oracle/ParseDicomFromWadoCommand.h" + +#if ORTHANC_ENABLE_DCMTK == 1 +# include +#endif + +#include +#include +#include +#include + +#include + +namespace OrthancStone +{ + class SeriesFramesLoader::Payload : public Orthanc::IDynamicObject + { + private: + DicomSource source_; + size_t seriesIndex_; + std::string sopInstanceUid_; // Only used for debug purpose + unsigned int quality_; + bool hasWindowing_; + float windowingCenter_; + float windowingWidth_; + std::auto_ptr userPayload_; + + public: + Payload(const DicomSource& source, + size_t seriesIndex, + const std::string& sopInstanceUid, + unsigned int quality, + Orthanc::IDynamicObject* userPayload) : + source_(source), + seriesIndex_(seriesIndex), + sopInstanceUid_(sopInstanceUid), + quality_(quality), + hasWindowing_(false), + userPayload_(userPayload) + { + } + + size_t GetSeriesIndex() const + { + return seriesIndex_; + } + + const std::string& GetSopInstanceUid() const + { + return sopInstanceUid_; + } + + unsigned int GetQuality() const + { + return quality_; + } + + void SetWindowing(float center, + float width) + { + hasWindowing_ = true; + windowingCenter_ = center; + windowingWidth_ = width; + } + + bool HasWindowing() const + { + return hasWindowing_; + } + + float GetWindowingCenter() const + { + if (hasWindowing_) + { + return windowingCenter_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + float GetWindowingWidth() const + { + if (hasWindowing_) + { + return windowingWidth_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + const DicomSource& GetSource() const + { + return source_; + } + + Orthanc::IDynamicObject* GetUserPayload() const + { + return userPayload_.get(); + } + }; + + + SeriesFramesLoader::SeriesFramesLoader(ILoadersContext& context, + LoadedDicomResources& instances, + const std::string& dicomDirPath, + boost::shared_ptr dicomDir) : + context_(context), + frames_(instances), + dicomDirPath_(dicomDirPath), + dicomDir_(dicomDir) + { + } + + + void SeriesFramesLoader::EmitMessage(const Payload& payload, + const Orthanc::ImageAccessor& image) + { + const DicomInstanceParameters& parameters = frames_.GetInstanceParameters(payload.GetSeriesIndex()); + const Orthanc::DicomMap& instance = frames_.GetInstance(payload.GetSeriesIndex()); + size_t frameIndex = frames_.GetFrameIndex(payload.GetSeriesIndex()); + + if (frameIndex >= parameters.GetImageInformation().GetNumberOfFrames() || + payload.GetSopInstanceUid() != parameters.GetSopInstanceUid()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + LOG(TRACE) << "Decoded instance " << payload.GetSopInstanceUid() << ", frame " + << frameIndex << ": " << image.GetWidth() << "x" + << image.GetHeight() << ", " << Orthanc::EnumerationToString(image.GetFormat()) + << ", quality " << payload.GetQuality(); + + FrameLoadedMessage message(*this, frameIndex, payload.GetQuality(), image, instance, parameters, payload.GetUserPayload()); + BroadcastMessage(message); + } + + +#if ORTHANC_ENABLE_DCMTK == 1 + void SeriesFramesLoader::HandleDicom(const Payload& payload, + Orthanc::ParsedDicomFile& dicom) + { + size_t frameIndex = frames_.GetFrameIndex(payload.GetSeriesIndex()); + + std::auto_ptr decoded; + decoded.reset(Orthanc::DicomImageDecoder::Decode(dicom, frameIndex)); + + if (decoded.get() == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + EmitMessage(payload, *decoded); + } +#endif + + + void SeriesFramesLoader::HandleDicomWebRendered(const Payload& payload, + const std::string& body, + const std::map& headers) + { + assert(payload.GetSource().IsDicomWeb() && + payload.HasWindowing()); + + bool ok = false; + for (std::map::const_iterator it = headers.begin(); + it != headers.end(); ++it) + { + if (boost::iequals("content-type", it->first) && + boost::iequals(Orthanc::MIME_JPEG, it->second)) + { + ok = true; + break; + } + } + + if (!ok) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol, + "The WADO-RS server has not generated a JPEG image on /rendered"); + } + + Orthanc::JpegReader reader; + reader.ReadFromMemory(body); + + switch (reader.GetFormat()) + { + case Orthanc::PixelFormat_RGB24: + EmitMessage(payload, reader); + break; + + case Orthanc::PixelFormat_Grayscale8: + { + const DicomInstanceParameters& parameters = frames_.GetInstanceParameters(payload.GetSeriesIndex()); + + Orthanc::Image scaled(parameters.GetExpectedPixelFormat(), reader.GetWidth(), reader.GetHeight(), false); + Orthanc::ImageProcessing::Convert(scaled, reader); + + float w = payload.GetWindowingWidth(); + if (w <= 0.01f) + { + w = 0.01; // Prevent division by zero + } + + const float c = payload.GetWindowingCenter(); + const float scaling = w / 255.0f; + const float offset = (c - w / 2.0f) / scaling; + + Orthanc::ImageProcessing::ShiftScale(scaled, offset, scaling, false /* truncation to speed up */); + EmitMessage(payload, scaled); + break; + } + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } + + +#if ORTHANC_ENABLE_DCMTK == 1 + void SeriesFramesLoader::Handle(const ParseDicomSuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + + const Payload& payload = dynamic_cast(message.GetOrigin().GetPayload()); + if ((payload.GetSource().IsDicomDir() || + payload.GetSource().IsDicomWeb()) && + message.HasPixelData()) + { + HandleDicom(dynamic_cast(message.GetOrigin().GetPayload()), message.GetDicom()); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } +#endif + + + void SeriesFramesLoader::Handle(const GetOrthancImageCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + + const Payload& payload = dynamic_cast(message.GetOrigin().GetPayload()); + assert(payload.GetSource().IsOrthanc()); + + EmitMessage(payload, message.GetImage()); + } + + + void SeriesFramesLoader::Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + + const Payload& payload = dynamic_cast(message.GetOrigin().GetPayload()); + assert(payload.GetSource().IsOrthanc()); + + EmitMessage(payload, message.GetImage()); + } + + + void SeriesFramesLoader::Handle(const OrthancRestApiCommand::SuccessMessage& message) + { + // This is to handle "/rendered" in DICOMweb + assert(message.GetOrigin().HasPayload()); + HandleDicomWebRendered(dynamic_cast(message.GetOrigin().GetPayload()), + message.GetAnswer(), message.GetAnswerHeaders()); + } + + + void SeriesFramesLoader::Handle(const HttpCommand::SuccessMessage& message) + { + // This is to handle "/rendered" in DICOMweb + assert(message.GetOrigin().HasPayload()); + HandleDicomWebRendered(dynamic_cast(message.GetOrigin().GetPayload()), + message.GetAnswer(), message.GetAnswerHeaders()); + } + + + void SeriesFramesLoader::GetPreviewWindowing(float& center, + float& width, + size_t index) const + { + const Orthanc::DicomMap& instance = frames_.GetInstance(index); + const DicomInstanceParameters& parameters = frames_.GetInstanceParameters(index); + + if (parameters.HasDefaultWindowing()) + { + // TODO - Handle multiple presets (take the largest width) + center = parameters.GetDefaultWindowingCenter(); + width = parameters.GetDefaultWindowingWidth(); + } + else + { + float a, b; + if (instance.ParseFloat(a, Orthanc::DICOM_TAG_SMALLEST_IMAGE_PIXEL_VALUE) && + instance.ParseFloat(b, Orthanc::DICOM_TAG_LARGEST_IMAGE_PIXEL_VALUE) && + a < b) + { + center = (a + b) / 2.0f; + width = (b - a); + } + else + { + // Cannot infer a suitable windowing from the available tags + center = 128.0f; + width = 256.0f; + } + } + } + + + Orthanc::IDynamicObject& SeriesFramesLoader::FrameLoadedMessage::GetUserPayload() const + { + if (userPayload_) + { + return *userPayload_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + void SeriesFramesLoader::Factory::SetDicomDir(const std::string& dicomDirPath, + boost::shared_ptr dicomDir) + { + dicomDirPath_ = dicomDirPath; + dicomDir_ = dicomDir; + } + + + boost::shared_ptr SeriesFramesLoader::Factory::Create(ILoadersContext::ILock& stone) + { + boost::shared_ptr loader( + new SeriesFramesLoader(stone.GetContext(), instances_, dicomDirPath_, dicomDir_)); + loader->Register(stone.GetOracleObservable(), &SeriesFramesLoader::Handle); + loader->Register(stone.GetOracleObservable(), &SeriesFramesLoader::Handle); + loader->Register(stone.GetOracleObservable(), &SeriesFramesLoader::Handle); + loader->Register(stone.GetOracleObservable(), &SeriesFramesLoader::Handle); + +#if ORTHANC_ENABLE_DCMTK == 1 + loader->Register(stone.GetOracleObservable(), &SeriesFramesLoader::Handle); +#endif + + return loader; + } + + + void SeriesFramesLoader::ScheduleLoadFrame(int priority, + const DicomSource& source, + size_t index, + unsigned int quality, + Orthanc::IDynamicObject* userPayload) + { + std::auto_ptr protection(userPayload); + + if (index >= frames_.GetFramesCount() || + quality >= source.GetQualityCount()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + const Orthanc::DicomMap& instance = frames_.GetInstance(index); + + std::string sopInstanceUid; + if (!instance.LookupStringValue(sopInstanceUid, Orthanc::DICOM_TAG_SOP_INSTANCE_UID, false)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "Missing SOPInstanceUID in a DICOM instance"); + } + + if (source.IsDicomDir()) + { + if (dicomDir_.get() == NULL) + { + // Should have been set in the factory + throw Orthanc::OrthancException( + Orthanc::ErrorCode_BadSequenceOfCalls, + "SeriesFramesLoader::Factory::SetDicomDir() should have been called"); + } + + assert(quality == 0); + + std::string file; + if (dicomDir_->LookupStringValue(file, sopInstanceUid, Orthanc::DICOM_TAG_REFERENCED_FILE_ID)) + { + std::auto_ptr command(new ParseDicomFromFileCommand(dicomDirPath_, file)); + command->SetPixelDataIncluded(true); + command->AcquirePayload(new Payload(source, index, sopInstanceUid, quality, protection.release())); + + { + std::auto_ptr lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } + } + else + { + LOG(WARNING) << "Missing tag ReferencedFileID in a DICOMDIR entry"; + } + } + else if (source.IsDicomWeb()) + { + std::string studyInstanceUid, seriesInstanceUid; + if (!instance.LookupStringValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) || + !instance.LookupStringValue(seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "Missing StudyInstanceUID or SeriesInstanceUID in a DICOM instance"); + } + + const std::string uri = ("/studies/" + studyInstanceUid + + "/series/" + seriesInstanceUid + + "/instances/" + sopInstanceUid); + + if (source.HasDicomWebRendered() && + quality == 0) + { + float c, w; + GetPreviewWindowing(c, w, index); + + std::map arguments, headers; + arguments["window"] = (boost::lexical_cast(c) + "," + + boost::lexical_cast(w) + ",linear"); + headers["Accept"] = "image/jpeg"; + + std::auto_ptr payload(new Payload(source, index, sopInstanceUid, quality, protection.release())); + payload->SetWindowing(c, w); + + { + std::auto_ptr lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, + source.CreateDicomWebCommand(uri + "/rendered", arguments, headers, payload.release())); + } + } + else + { + assert((source.HasDicomWebRendered() && quality == 1) || + (!source.HasDicomWebRendered() && quality == 0)); + +#if ORTHANC_ENABLE_DCMTK == 1 + std::auto_ptr payload(new Payload(source, index, sopInstanceUid, quality, protection.release())); + + const std::map empty; + + std::auto_ptr command( + new ParseDicomFromWadoCommand(sopInstanceUid, source.CreateDicomWebCommand(uri, empty, empty, NULL))); + command->AcquirePayload(payload.release()); + + { + std::auto_ptr lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } +#else + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, + "DCMTK is not enabled, cannot parse a DICOM instance"); +#endif + } + } + else if (source.IsOrthanc()) + { + std::string orthancId; + + { + std::string patientId, studyInstanceUid, seriesInstanceUid; + if (!instance.LookupStringValue(patientId, Orthanc::DICOM_TAG_PATIENT_ID, false) || + !instance.LookupStringValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) || + !instance.LookupStringValue(seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "Missing StudyInstanceUID or SeriesInstanceUID in a DICOM instance"); + } + + Orthanc::DicomInstanceHasher hasher(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid); + orthancId = hasher.HashInstance(); + } + + const DicomInstanceParameters& parameters = frames_.GetInstanceParameters(index); + + if (quality == 0 && source.HasOrthancWebViewer1()) + { + std::auto_ptr command(new GetOrthancWebViewerJpegCommand); + command->SetInstance(orthancId); + command->SetExpectedPixelFormat(parameters.GetExpectedPixelFormat()); + command->AcquirePayload(new Payload(source, index, sopInstanceUid, quality, protection.release())); + + { + std::auto_ptr lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } + } + else if (quality == 0 && source.HasOrthancAdvancedPreview()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + else + { + assert(quality <= 1); + assert(quality == 0 || + source.HasOrthancWebViewer1() || + source.HasOrthancAdvancedPreview()); + + std::auto_ptr command(new GetOrthancImageCommand); + command->SetFrameUri(orthancId, frames_.GetFrameIndex(index), parameters.GetExpectedPixelFormat()); + command->SetExpectedPixelFormat(parameters.GetExpectedPixelFormat()); + command->SetHttpHeader("Accept", Orthanc::MIME_PAM); + command->AcquirePayload(new Payload(source, index, sopInstanceUid, quality, protection.release())); + + { + std::auto_ptr lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } + } + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/SeriesFramesLoader.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/SeriesFramesLoader.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,176 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#if !defined(ORTHANC_ENABLE_DCMTK) +# error The macro ORTHANC_ENABLE_DCMTK must be defined +#endif + +#include "OracleScheduler.h" +#include "DicomSource.h" +#include "SeriesOrderedFrames.h" +#include "ILoaderFactory.h" + +namespace OrthancStone +{ + class SeriesFramesLoader : + public ObserverBase, + public IObservable + { + private: + class Payload; + + ILoadersContext& context_; + SeriesOrderedFrames frames_; + std::string dicomDirPath_; + boost::shared_ptr dicomDir_; + + SeriesFramesLoader(ILoadersContext& context, + LoadedDicomResources& instances, + const std::string& dicomDirPath, + boost::shared_ptr dicomDir); + + void EmitMessage(const Payload& payload, + const Orthanc::ImageAccessor& image); + +#if ORTHANC_ENABLE_DCMTK == 1 + void HandleDicom(const Payload& payload, + Orthanc::ParsedDicomFile& dicom); +#endif + + void HandleDicomWebRendered(const Payload& payload, + const std::string& body, + const std::map& headers); + +#if ORTHANC_ENABLE_DCMTK == 1 + void Handle(const ParseDicomSuccessMessage& message); +#endif + + void Handle(const GetOrthancImageCommand::SuccessMessage& message); + + void Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message); + + void Handle(const OrthancRestApiCommand::SuccessMessage& message); + + void Handle(const HttpCommand::SuccessMessage& message); + + void GetPreviewWindowing(float& center, + float& width, + size_t index) const; + + public: + class FrameLoadedMessage : public OriginMessage + { + ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); + + private: + size_t frameIndex_; + unsigned int quality_; + const Orthanc::ImageAccessor& image_; + const Orthanc::DicomMap& instance_; + const DicomInstanceParameters& parameters_; + Orthanc::IDynamicObject* userPayload_; // Ownership is maintained by the caller + + public: + FrameLoadedMessage(const SeriesFramesLoader& loader, + size_t frameIndex, + unsigned int quality, + const Orthanc::ImageAccessor& image, + const Orthanc::DicomMap& instance, + const DicomInstanceParameters& parameters, + Orthanc::IDynamicObject* userPayload) : + OriginMessage(loader), + frameIndex_(frameIndex), + quality_(quality), + image_(image), + instance_(instance), + parameters_(parameters), + userPayload_(userPayload) + { + } + + size_t GetFrameIndex() const + { + return frameIndex_; + } + + unsigned int GetQuality() const + { + return quality_; + } + + const Orthanc::ImageAccessor& GetImage() const + { + return image_; + } + + const Orthanc::DicomMap& GetInstance() const + { + return instance_; + } + + const DicomInstanceParameters& GetInstanceParameters() const + { + return parameters_; + } + + bool HasUserPayload() const + { + return userPayload_ != NULL; + } + + Orthanc::IDynamicObject& GetUserPayload() const; + }; + + + class Factory : public ILoaderFactory + { + private: + LoadedDicomResources& instances_; + std::string dicomDirPath_; + boost::shared_ptr dicomDir_; + + public: + // No "const" because "LoadedDicomResources::GetResource()" will call "Flatten()" + Factory(LoadedDicomResources& instances) : + instances_(instances) + { + } + + void SetDicomDir(const std::string& dicomDirPath, + boost::shared_ptr dicomDir); + + virtual boost::shared_ptr Create(ILoadersContext::ILock& context); + }; + + const SeriesOrderedFrames& GetOrderedFrames() const + { + return frames_; + } + + void ScheduleLoadFrame(int priority, + const DicomSource& source, + size_t index, + unsigned int quality, + Orthanc::IDynamicObject* userPayload /* transfer ownership */); + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/SeriesMetadataLoader.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/SeriesMetadataLoader.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,348 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "SeriesMetadataLoader.h" + +#include + +namespace OrthancStone +{ + SeriesMetadataLoader::SeriesMetadataLoader(boost::shared_ptr& loader) : + loader_(loader), + state_(State_Setup) + { + } + + + bool SeriesMetadataLoader::IsScheduledWithHigherPriority(const std::string& seriesInstanceUid, + int priority) const + { + if (series_.find(seriesInstanceUid) != series_.end()) + { + // This series is readily available + return true; + } + else + { + std::map::const_iterator found = scheduled_.find(seriesInstanceUid); + + return (found != scheduled_.end() && + found->second < priority); + } + } + + + void SeriesMetadataLoader::Handle(const DicomResourcesLoader::SuccessMessage& message) + { + assert(message.GetResources()); + + switch (state_) + { + case State_Setup: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + + case State_Default: + { + std::string studyInstanceUid; + std::string seriesInstanceUid; + + if (message.GetResources()->LookupTagValueConsensus(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID) && + message.GetResources()->LookupTagValueConsensus(seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID)) + { + series_[seriesInstanceUid] = message.GetResources(); + + SeriesLoadedMessage loadedMessage(*this, message.GetDicomSource(), studyInstanceUid, + seriesInstanceUid, *message.GetResources()); + BroadcastMessage(loadedMessage); + } + + break; + } + + case State_DicomDir: + { + assert(!dicomDir_); + assert(seriesSize_.empty()); + + dicomDir_ = message.GetResources(); + + for (size_t i = 0; i < message.GetResources()->GetSize(); i++) + { + std::string seriesInstanceUid; + if (message.GetResources()->GetResource(i).LookupStringValue + (seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false)) + { + boost::shared_ptr target + (new OrthancStone::LoadedDicomResources(Orthanc::DICOM_TAG_SOP_INSTANCE_UID)); + + if (loader_->ScheduleLoadDicomFile(target, message.GetPriority(), message.GetDicomSource(), dicomDirPath_, + message.GetResources()->GetResource(i), false /* no need for pixel data */, + NULL /* TODO PAYLOAD */)) + { + std::map::iterator found = seriesSize_.find(seriesInstanceUid); + if (found == seriesSize_.end()) + { + series_[seriesInstanceUid].reset + (new OrthancStone::LoadedDicomResources(Orthanc::DICOM_TAG_SOP_INSTANCE_UID)); + seriesSize_[seriesInstanceUid] = 1; + } + else + { + found->second ++; + } + } + } + } + + LOG(INFO) << "Read a DICOMDIR containing " << seriesSize_.size() << " series"; + + state_ = State_DicomFile; + break; + } + + case State_DicomFile: + { + assert(dicomDir_); + assert(message.GetResources()->GetSize() <= 1); // Could be zero if corrupted DICOM instance + + if (message.GetResources()->GetSize() == 1) + { + const Orthanc::DicomMap& instance = message.GetResources()->GetResource(0); + + std::string studyInstanceUid; + std::string seriesInstanceUid; + if (instance.LookupStringValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) && + instance.LookupStringValue(seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false)) + { + Series::const_iterator series = series_.find(seriesInstanceUid); + std::map::const_iterator size = seriesSize_.find(seriesInstanceUid); + + if (series == series_.end() || + size == seriesSize_.end()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + else + { + series->second->AddResource(instance); + + if (series->second->GetSize() > size->second) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + else if (series->second->GetSize() == size->second) + { + // The series is complete + SeriesLoadedMessage loadedMessage( + *this, message.GetDicomSource(), + studyInstanceUid, seriesInstanceUid, *series->second); + loadedMessage.SetDicomDir(dicomDirPath_, dicomDir_); + BroadcastMessage(loadedMessage); + } + } + } + } + + break; + } + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + + + SeriesMetadataLoader::SeriesLoadedMessage::SeriesLoadedMessage( + const SeriesMetadataLoader& loader, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + LoadedDicomResources& instances) : + OriginMessage(loader), + source_(source), + studyInstanceUid_(studyInstanceUid), + seriesInstanceUid_(seriesInstanceUid), + instances_(instances) + { + LOG(INFO) << "Loaded series " << seriesInstanceUid + << ", number of instances: " << instances_.GetSize(); + } + + + boost::shared_ptr SeriesMetadataLoader::Factory::Create(ILoadersContext::ILock& context) + { + DicomResourcesLoader::Factory factory; + boost::shared_ptr loader + (boost::dynamic_pointer_cast(factory.Create(context))); + + boost::shared_ptr obj(new SeriesMetadataLoader(loader)); + obj->Register(*loader, &SeriesMetadataLoader::Handle); + return obj; + } + + + SeriesMetadataLoader::Accessor::Accessor(SeriesMetadataLoader& that, + const std::string& seriesInstanceUid) + { + Series::const_iterator found = that.series_.find(seriesInstanceUid); + if (found != that.series_.end()) + { + assert(found->second != NULL); + series_ = found->second; + } + } + + + size_t SeriesMetadataLoader::Accessor::GetInstancesCount() const + { + if (IsComplete()) + { + return series_->GetSize(); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + const Orthanc::DicomMap& SeriesMetadataLoader::Accessor::GetInstance(size_t index) const + { + if (IsComplete()) + { + return series_->GetResource(index); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + void SeriesMetadataLoader::ScheduleLoadSeries(int priority, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid) + { + if (state_ != State_Setup && + state_ != State_Default) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, + "The loader is working in DICOMDIR state"); + } + + state_ = State_Default; + + // Only re-schedule the loading if the previous loading was with lower priority + if (!IsScheduledWithHigherPriority(seriesInstanceUid, priority)) + { + if (source.IsDicomWeb()) + { + boost::shared_ptr target + (new LoadedDicomResources(Orthanc::DICOM_TAG_SOP_INSTANCE_UID)); + loader_->ScheduleGetDicomWeb( + target, priority, source, + "/studies/" + studyInstanceUid + "/series/" + seriesInstanceUid + "/metadata", + NULL /* TODO PAYLOAD */); + + scheduled_[seriesInstanceUid] = priority; + } + else if (source.IsOrthanc()) + { + // This flavor of the method is only available with DICOMweb, as + // Orthanc requires the "PatientID" to be known + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, + "The PatientID must be provided on Orthanc sources"); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } + } + + + void SeriesMetadataLoader::ScheduleLoadSeries(int priority, + const DicomSource& source, + const std::string& patientId, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid) + { + if (state_ != State_Setup && + state_ != State_Default) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, + "The loader is working in DICOMDIR state"); + } + + state_ = State_Default; + + if (source.IsDicomWeb()) + { + ScheduleLoadSeries(priority, source, studyInstanceUid, seriesInstanceUid); + } + else if (!IsScheduledWithHigherPriority(seriesInstanceUid, priority)) + { + if (source.IsOrthanc()) + { + // Dummy SOP Instance UID, as we are working at the "series" level + Orthanc::DicomInstanceHasher hasher(patientId, studyInstanceUid, seriesInstanceUid, "dummy"); + + boost::shared_ptr target + (new LoadedDicomResources(Orthanc::DICOM_TAG_SOP_INSTANCE_UID)); + + loader_->ScheduleLoadOrthancResources(target, priority, source, Orthanc::ResourceType_Series, + hasher.HashSeries(), Orthanc::ResourceType_Instance, + NULL /* TODO PAYLOAD */); + + scheduled_[seriesInstanceUid] = priority; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } + } + + + void SeriesMetadataLoader::ScheduleLoadDicomDir(int priority, + const DicomSource& source, + const std::string& path) + { + if (!source.IsDicomDir()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + if (state_ != State_Setup) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, + "The loader cannot load two different DICOMDIR"); + } + + state_ = State_DicomDir; + dicomDirPath_ = path; + boost::shared_ptr dicomDir + (new LoadedDicomResources(Orthanc::DICOM_TAG_REFERENCED_SOP_INSTANCE_UID_IN_FILE)); + loader_->ScheduleLoadDicomDir(dicomDir, priority, source, path, + NULL /* TODO PAYLOAD */); + } +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/SeriesMetadataLoader.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/SeriesMetadataLoader.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,170 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "DicomResourcesLoader.h" + +namespace OrthancStone +{ + class SeriesMetadataLoader : + public ObserverBase, + public IObservable + { + private: + enum State + { + State_Setup, + State_Default, + State_DicomDir, + State_DicomFile + }; + + typedef std::map > Series; + + boost::shared_ptr loader_; + State state_; + std::map scheduled_; // Maps a "SeriesInstanceUID" to a priority + Series series_; + boost::shared_ptr dicomDir_; + std::string dicomDirPath_; + std::map seriesSize_; + + SeriesMetadataLoader(boost::shared_ptr& loader); + + bool IsScheduledWithHigherPriority(const std::string& seriesInstanceUid, + int priority) const; + + void Handle(const DicomResourcesLoader::SuccessMessage& message); + + public: + class SeriesLoadedMessage : public OriginMessage + { + ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); + + private: + const DicomSource& source_; + const std::string& studyInstanceUid_; + const std::string& seriesInstanceUid_; + LoadedDicomResources& instances_; + std::string dicomDirPath_; + boost::shared_ptr dicomDir_; + + public: + SeriesLoadedMessage(const SeriesMetadataLoader& loader, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + LoadedDicomResources& instances); + + const DicomSource& GetDicomSource() const + { + return source_; + } + + const std::string& GetStudyInstanceUid() const + { + return studyInstanceUid_; + } + + const std::string& GetSeriesInstanceUid() const + { + return seriesInstanceUid_; + } + + size_t GetInstancesCount() const + { + return instances_.GetSize(); + } + + const Orthanc::DicomMap& GetInstance(size_t index) const + { + return instances_.GetResource(index); + } + + LoadedDicomResources& GetInstances() const + { + return instances_; + } + + void SetDicomDir(const std::string& dicomDirPath, + boost::shared_ptr dicomDir) + { + dicomDirPath_ = dicomDirPath; + dicomDir_ = dicomDir; + } + + const std::string& GetDicomDirPath() const + { + return dicomDirPath_; + } + + // Will be NULL on non-DICOMDIR sources + boost::shared_ptr GetDicomDir() const + { + return dicomDir_; + } + }; + + + class Factory : public ILoaderFactory + { + public: + virtual boost::shared_ptr Create(ILoadersContext::ILock& context); + }; + + + class Accessor : public boost::noncopyable + { + private: + boost::shared_ptr series_; + + public: + Accessor(SeriesMetadataLoader& that, + const std::string& seriesInstanceUid); + + bool IsComplete() const + { + return series_ != NULL; + } + + size_t GetInstancesCount() const; + + const Orthanc::DicomMap& GetInstance(size_t index) const; + }; + + + void ScheduleLoadSeries(int priority, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid); + + void ScheduleLoadSeries(int priority, + const DicomSource& source, + const std::string& patientId, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid); + + void ScheduleLoadDicomDir(int priority, + const DicomSource& source, + const std::string& path); + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/SeriesOrderedFrames.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/SeriesOrderedFrames.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,343 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "../Toolbox/SlicesSorter.h" +#include "SeriesOrderedFrames.h" + +#include + +namespace OrthancStone +{ + class SeriesOrderedFrames::Instance : public boost::noncopyable + { + private: + std::auto_ptr dicom_; + DicomInstanceParameters parameters_; + + public: + Instance(const Orthanc::DicomMap& dicom) : + dicom_(dicom.Clone()), + parameters_(dicom) + { + } + + const Orthanc::DicomMap& GetInstance() const + { + return *dicom_; + } + + const DicomInstanceParameters& GetInstanceParameters() const + { + return parameters_; + } + + bool Lookup3DGeometry(CoordinateSystem3D& target) const + { + try + { + std::string imagePositionPatient, imageOrientationPatient; + if (dicom_->LookupStringValue(imagePositionPatient, Orthanc::DICOM_TAG_IMAGE_POSITION_PATIENT, false) && + dicom_->LookupStringValue(imageOrientationPatient, Orthanc::DICOM_TAG_IMAGE_ORIENTATION_PATIENT, false)) + { + target = CoordinateSystem3D(imagePositionPatient, imageOrientationPatient); + return true; + } + } + catch (Orthanc::OrthancException&) + { + } + + return false; + } + + bool LookupIndexInSeries(int& target) const + { + std::string value; + + if (dicom_->LookupStringValue(value, Orthanc::DICOM_TAG_INSTANCE_NUMBER, false) || + dicom_->LookupStringValue(value, Orthanc::DICOM_TAG_IMAGE_INDEX, false)) + { + try + { + target = boost::lexical_cast(value); + return true; + } + catch (boost::bad_lexical_cast&) + { + } + } + + return false; + } + }; + + + class SeriesOrderedFrames::Frame : public boost::noncopyable + { + private: + const Instance* instance_; + unsigned int frameIndex_; + + public: + Frame(const Instance& instance, + unsigned int frameIndex) : + instance_(&instance), + frameIndex_(frameIndex) + { + if (frameIndex_ >= instance.GetInstanceParameters().GetImageInformation().GetNumberOfFrames()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + + const Orthanc::DicomMap& GetInstance() const + { + assert(instance_ != NULL); + return instance_->GetInstance(); + } + + const DicomInstanceParameters& GetInstanceParameters() const + { + assert(instance_ != NULL); + return instance_->GetInstanceParameters(); + } + + unsigned int GetFrameIndex() const + { + return frameIndex_; + } + }; + + + class SeriesOrderedFrames::InstanceWithIndexInSeries + { + private: + const Instance* instance_; // Don't use a reference to make "std::sort()" happy + int index_; + + public: + InstanceWithIndexInSeries(const Instance& instance) : + instance_(&instance) + { + if (!instance_->LookupIndexInSeries(index_)) + { + index_ = std::numeric_limits::max(); + } + } + + const Instance& GetInstance() const + { + return *instance_; + } + + int GetIndexInSeries() const + { + return index_; + } + + bool operator< (const InstanceWithIndexInSeries& other) const + { + return (index_ < other.index_); + } + }; + + + void SeriesOrderedFrames::Clear() + { + for (size_t i = 0; i < instances_.size(); i++) + { + assert(instances_[i] != NULL); + delete instances_[i]; + } + + for (size_t i = 0; i < orderedFrames_.size(); i++) + { + assert(orderedFrames_[i] != NULL); + delete orderedFrames_[i]; + } + + instances_.clear(); + orderedFrames_.clear(); + } + + + bool SeriesOrderedFrames::Sort3DVolume() + { + SlicesSorter sorter; + sorter.Reserve(instances_.size()); + + for (size_t i = 0; i < instances_.size(); i++) + { + CoordinateSystem3D geometry; + if (instances_[i]->Lookup3DGeometry(geometry)) + { + sorter.AddSlice(geometry, new Orthanc::SingleValueObject(instances_[i])); + } + else + { + return false; // Not a 3D volume + } + } + + if (!sorter.Sort() || + sorter.GetSlicesCount() != instances_.size() || + !sorter.AreAllSlicesDistinct()) + { + return false; + } + else + { + for (size_t i = 0; i < sorter.GetSlicesCount(); i++) + { + assert(sorter.HasSlicePayload(i)); + + const Orthanc::SingleValueObject& payload = + dynamic_cast&>(sorter.GetSlicePayload(i)); + + assert(payload.GetValue() != NULL); + + for (size_t j = 0; j < payload.GetValue()->GetInstanceParameters().GetImageInformation().GetNumberOfFrames(); j++) + { + orderedFrames_.push_back(new Frame(*payload.GetValue(), j)); + } + } + + isRegular_ = sorter.ComputeSpacingBetweenSlices(spacingBetweenSlices_); + return true; + } + } + + + void SeriesOrderedFrames::SortIndexInSeries() + { + std::vector tmp; + tmp.reserve(instances_.size()); + + for (size_t i = 0; i < instances_.size(); i++) + { + assert(instances_[i] != NULL); + tmp.push_back(InstanceWithIndexInSeries(*instances_[i])); + } + + std::sort(tmp.begin(), tmp.end()); + + for (size_t i = 0; i < tmp.size(); i++) + { + for (size_t j = 0; j < tmp[i].GetInstance().GetInstanceParameters().GetImageInformation().GetNumberOfFrames(); j++) + { + orderedFrames_.push_back(new Frame(tmp[i].GetInstance(), j)); + } + } + } + + + const SeriesOrderedFrames::Frame& SeriesOrderedFrames::GetFrame(size_t seriesIndex) const + { + if (seriesIndex >= orderedFrames_.size()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + assert(orderedFrames_[seriesIndex] != NULL); + return *(orderedFrames_[seriesIndex]); + } + } + + + SeriesOrderedFrames::SeriesOrderedFrames(LoadedDicomResources& instances) : + isVolume_(false), + isRegular_(false), + spacingBetweenSlices_(0) + { + instances_.reserve(instances.GetSize()); + + size_t numberOfFrames = 0; + + for (size_t i = 0; i < instances.GetSize(); i++) + { + try + { + std::auto_ptr instance(new Instance(instances.GetResource(i))); + numberOfFrames += instance->GetInstanceParameters().GetImageInformation().GetNumberOfFrames(); + instances_.push_back(instance.release()); + } + catch (Orthanc::OrthancException&) + { + // The instance has not all the required DICOM tags, skip it + } + } + + orderedFrames_.reserve(numberOfFrames); + + if (Sort3DVolume()) + { + isVolume_ = true; + + if (isRegular_) + { + LOG(INFO) << "Regular 3D volume detected"; + } + else + { + LOG(INFO) << "Non-regular 3D volume detected"; + } + } + else + { + LOG(INFO) << "Series is not a 3D volume, sorting by index"; + SortIndexInSeries(); + } + + LOG(INFO) << "Number of frames: " << orderedFrames_.size(); + } + + + unsigned int SeriesOrderedFrames::GetFrameIndex(size_t seriesIndex) const + { + return GetFrame(seriesIndex).GetFrameIndex(); + } + + + const Orthanc::DicomMap& SeriesOrderedFrames::GetInstance(size_t seriesIndex) const + { + return GetFrame(seriesIndex).GetInstance(); + } + + + const DicomInstanceParameters& SeriesOrderedFrames::GetInstanceParameters(size_t seriesIndex) const + { + return GetFrame(seriesIndex).GetInstanceParameters(); + } + + + double SeriesOrderedFrames::GetSpacingBetweenSlices() const + { + if (IsRegular3DVolume()) + { + return spacingBetweenSlices_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/SeriesOrderedFrames.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/SeriesOrderedFrames.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,85 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "LoadedDicomResources.h" + +#include "../Toolbox/DicomInstanceParameters.h" + +namespace OrthancStone +{ + class SeriesOrderedFrames : public boost::noncopyable + { + private: + class Instance; + class Frame; + class InstanceWithIndexInSeries; + + std::vector instances_; + std::vector orderedFrames_; + bool isVolume_; + bool isRegular_; + double spacingBetweenSlices_; + + void Clear(); + + bool Sort3DVolume(); + + void SortIndexInSeries(); + + const Frame& GetFrame(size_t seriesIndex) const; + + public: + SeriesOrderedFrames(LoadedDicomResources& instances); + + ~SeriesOrderedFrames() + { + Clear(); + } + + size_t GetFramesCount() const + { + return orderedFrames_.size(); + } + + unsigned int GetFrameIndex(size_t seriesIndex) const; + + const Orthanc::DicomMap& GetInstance(size_t seriesIndex) const; + + const DicomInstanceParameters& GetInstanceParameters(size_t seriesIndex) const; + + // Are all frames parallel and aligned? + bool Is3DVolume() const + { + return isVolume_; + } + + // Are all frames parallel, aligned and evenly spaced? + bool IsRegular3DVolume() const + { + return isRegular_; + } + + // Only available on regular 3D volumes + double GetSpacingBetweenSlices() const; + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/SeriesThumbnailsLoader.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/SeriesThumbnailsLoader.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,566 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "SeriesThumbnailsLoader.h" + +#include +#include +#include +#include +#include + +#include + +static const unsigned int JPEG_QUALITY = 70; // Only used for Orthanc source + +namespace OrthancStone +{ + static SeriesThumbnailType ExtractSopClassUid(const std::string& sopClassUid) + { + if (sopClassUid == "1.2.840.10008.5.1.4.1.1.104.1") // Encapsulated PDF Storage + { + return SeriesThumbnailType_Pdf; + } + else if (sopClassUid == "1.2.840.10008.5.1.4.1.1.77.1.1.1" || // Video Endoscopic Image Storage + sopClassUid == "1.2.840.10008.5.1.4.1.1.77.1.2.1" || // Video Microscopic Image Storage + sopClassUid == "1.2.840.10008.5.1.4.1.1.77.1.4.1") // Video Photographic Image Storage + { + return SeriesThumbnailType_Video; + } + else + { + return SeriesThumbnailType_Unsupported; + } + } + + + SeriesThumbnailsLoader::Thumbnail::Thumbnail(const std::string& image, + const std::string& mime) : + type_(SeriesThumbnailType_Image), + image_(image), + mime_(mime) + { + } + + + SeriesThumbnailsLoader::Thumbnail::Thumbnail(SeriesThumbnailType type) : + type_(type) + { + if (type == SeriesThumbnailType_Image) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + + + void SeriesThumbnailsLoader::AcquireThumbnail(const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + SeriesThumbnailsLoader::Thumbnail* thumbnail) + { + assert(thumbnail != NULL); + + std::auto_ptr protection(thumbnail); + + Thumbnails::iterator found = thumbnails_.find(seriesInstanceUid); + if (found == thumbnails_.end()) + { + thumbnails_[seriesInstanceUid] = protection.release(); + } + else + { + assert(found->second != NULL); + delete found->second; + found->second = protection.release(); + } + + ThumbnailLoadedMessage message(*this, source, studyInstanceUid, seriesInstanceUid, *thumbnail); + BroadcastMessage(message); + } + + + class SeriesThumbnailsLoader::Handler : public Orthanc::IDynamicObject + { + private: + boost::shared_ptr loader_; + DicomSource source_; + std::string studyInstanceUid_; + std::string seriesInstanceUid_; + + public: + Handler(boost::shared_ptr loader, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid) : + loader_(loader), + source_(source), + studyInstanceUid_(studyInstanceUid), + seriesInstanceUid_(seriesInstanceUid) + { + if (!loader) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + } + + boost::shared_ptr GetLoader() + { + return loader_; + } + + const DicomSource& GetSource() const + { + return source_; + } + + const std::string& GetStudyInstanceUid() const + { + return studyInstanceUid_; + } + + const std::string& GetSeriesInstanceUid() const + { + return seriesInstanceUid_; + } + + virtual void HandleSuccess(const std::string& body, + const std::map& headers) = 0; + + virtual void HandleError() + { + LOG(INFO) << "Cannot generate thumbnail for SeriesInstanceUID: " << seriesInstanceUid_; + } + }; + + + class SeriesThumbnailsLoader::DicomWebSopClassHandler : public SeriesThumbnailsLoader::Handler + { + private: + static bool GetSopClassUid(std::string& sopClassUid, + const Json::Value& json) + { + Orthanc::DicomMap dicom; + dicom.FromDicomWeb(json); + + return dicom.LookupStringValue(sopClassUid, Orthanc::DICOM_TAG_SOP_CLASS_UID, false); + } + + public: + DicomWebSopClassHandler(boost::shared_ptr loader, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid) : + Handler(loader, source, studyInstanceUid, seriesInstanceUid) + { + } + + virtual void HandleSuccess(const std::string& body, + const std::map& headers) + { + Json::Reader reader; + Json::Value value; + + if (!reader.parse(body, value) || + value.type() != Json::arrayValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + else + { + SeriesThumbnailType type = SeriesThumbnailType_Unsupported; + + std::string sopClassUid; + if (value.size() > 0 && + GetSopClassUid(sopClassUid, value[0])) + { + bool ok = true; + + for (Json::Value::ArrayIndex i = 1; i < value.size() && ok; i++) + { + std::string s; + if (!GetSopClassUid(s, value[i]) || + s != sopClassUid) + { + ok = false; + } + } + + if (ok) + { + type = ExtractSopClassUid(sopClassUid); + } + } + + GetLoader()->AcquireThumbnail(GetSource(), GetStudyInstanceUid(), + GetSeriesInstanceUid(), new Thumbnail(type)); + } + } + }; + + + class SeriesThumbnailsLoader::DicomWebThumbnailHandler : public SeriesThumbnailsLoader::Handler + { + public: + DicomWebThumbnailHandler(boost::shared_ptr loader, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid) : + Handler(loader, source, studyInstanceUid, seriesInstanceUid) + { + } + + virtual void HandleSuccess(const std::string& body, + const std::map& headers) + { + std::string mime = Orthanc::MIME_JPEG; + for (std::map::const_iterator + it = headers.begin(); it != headers.end(); ++it) + { + if (boost::iequals(it->first, "content-type")) + { + mime = it->second; + } + } + + GetLoader()->AcquireThumbnail(GetSource(), GetStudyInstanceUid(), + GetSeriesInstanceUid(), new Thumbnail(body, mime)); + } + + virtual void HandleError() + { + // The DICOMweb wasn't able to generate a thumbnail, try to + // retrieve the SopClassUID tag using QIDO-RS + + std::map arguments, headers; + arguments["0020000D"] = GetStudyInstanceUid(); + arguments["0020000E"] = GetSeriesInstanceUid(); + arguments["includefield"] = "00080016"; + + std::auto_ptr command( + GetSource().CreateDicomWebCommand( + "/instances", arguments, headers, new DicomWebSopClassHandler( + GetLoader(), GetSource(), GetStudyInstanceUid(), GetSeriesInstanceUid()))); + GetLoader()->Schedule(command.release()); + } + }; + + + class SeriesThumbnailsLoader::ThumbnailInformation : public Orthanc::IDynamicObject + { + private: + DicomSource source_; + std::string studyInstanceUid_; + std::string seriesInstanceUid_; + + public: + ThumbnailInformation(const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid) : + source_(source), + studyInstanceUid_(studyInstanceUid), + seriesInstanceUid_(seriesInstanceUid) + { + } + + const DicomSource& GetDicomSource() const + { + return source_; + } + + const std::string& GetStudyInstanceUid() const + { + return studyInstanceUid_; + } + + const std::string& GetSeriesInstanceUid() const + { + return seriesInstanceUid_; + } + }; + + + class SeriesThumbnailsLoader::OrthancSopClassHandler : public SeriesThumbnailsLoader::Handler + { + private: + std::string instanceId_; + + public: + OrthancSopClassHandler(boost::shared_ptr loader, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + const std::string& instanceId) : + Handler(loader, source, studyInstanceUid, seriesInstanceUid), + instanceId_(instanceId) + { + } + + virtual void HandleSuccess(const std::string& body, + const std::map& headers) + { + SeriesThumbnailType type = ExtractSopClassUid(body); + + if (type == SeriesThumbnailType_Pdf || + type == SeriesThumbnailType_Video) + { + GetLoader()->AcquireThumbnail(GetSource(), GetStudyInstanceUid(), + GetSeriesInstanceUid(), new Thumbnail(type)); + } + else + { + std::auto_ptr command(new GetOrthancImageCommand); + command->SetUri("/instances/" + instanceId_ + "/preview"); + command->SetHttpHeader("Accept", Orthanc::MIME_JPEG); + command->AcquirePayload(new ThumbnailInformation( + GetSource(), GetStudyInstanceUid(), GetSeriesInstanceUid())); + GetLoader()->Schedule(command.release()); + } + } + }; + + + class SeriesThumbnailsLoader::SelectOrthancInstanceHandler : public SeriesThumbnailsLoader::Handler + { + public: + SelectOrthancInstanceHandler(boost::shared_ptr loader, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid) : + Handler(loader, source, studyInstanceUid, seriesInstanceUid) + { + } + + virtual void HandleSuccess(const std::string& body, + const std::map& headers) + { + static const char* const INSTANCES = "Instances"; + + Json::Value json; + Json::Reader reader; + if (!reader.parse(body, json) || + json.type() != Json::objectValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + + if (json.isMember(INSTANCES) && + json[INSTANCES].type() == Json::arrayValue && + json[INSTANCES].size() > 0) + { + // Select one instance of the series to generate the thumbnail + Json::Value::ArrayIndex index = json[INSTANCES].size() / 2; + if (json[INSTANCES][index].type() == Json::stringValue) + { + std::map arguments, headers; + arguments["quality"] = boost::lexical_cast(JPEG_QUALITY); + headers["Accept"] = Orthanc::MIME_JPEG; + + const std::string instance = json[INSTANCES][index].asString(); + + std::auto_ptr command(new OrthancRestApiCommand); + command->SetUri("/instances/" + instance + "/metadata/SopClassUid"); + command->AcquirePayload( + new OrthancSopClassHandler( + GetLoader(), GetSource(), GetStudyInstanceUid(), GetSeriesInstanceUid(), instance)); + GetLoader()->Schedule(command.release()); + } + } + } + }; + + + void SeriesThumbnailsLoader::Schedule(IOracleCommand* command) + { + std::auto_ptr lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority_, command); + } + + + void SeriesThumbnailsLoader::Handle(const HttpCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + dynamic_cast(message.GetOrigin().GetPayload()).HandleSuccess(message.GetAnswer(), message.GetAnswerHeaders()); + } + + + void SeriesThumbnailsLoader::Handle(const OrthancRestApiCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + dynamic_cast(message.GetOrigin().GetPayload()).HandleSuccess(message.GetAnswer(), message.GetAnswerHeaders()); + } + + + void SeriesThumbnailsLoader::Handle(const GetOrthancImageCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + + std::auto_ptr resized(Orthanc::ImageProcessing::FitSize(message.GetImage(), width_, height_)); + + std::string jpeg; + Orthanc::JpegWriter writer; + writer.SetQuality(JPEG_QUALITY); + writer.WriteToMemory(jpeg, *resized); + + const ThumbnailInformation& info = dynamic_cast(message.GetOrigin().GetPayload()); + AcquireThumbnail(info.GetDicomSource(), info.GetStudyInstanceUid(), + info.GetSeriesInstanceUid(), new Thumbnail(jpeg, Orthanc::MIME_JPEG)); + } + + + void SeriesThumbnailsLoader::Handle(const OracleCommandExceptionMessage& message) + { + const OracleCommandBase& command = dynamic_cast(message.GetOrigin()); + assert(command.HasPayload()); + + if (command.GetType() == IOracleCommand::Type_GetOrthancImage) + { + // This is presumably a HTTP status 301 (Moved permanently) + // because of an unsupported DICOM file in "/preview" + const ThumbnailInformation& info = dynamic_cast(command.GetPayload()); + AcquireThumbnail(info.GetDicomSource(), info.GetStudyInstanceUid(), + info.GetSeriesInstanceUid(), new Thumbnail(SeriesThumbnailType_Unsupported)); + } + else + { + dynamic_cast(command.GetPayload()).HandleError(); + } + } + + + SeriesThumbnailsLoader::SeriesThumbnailsLoader(ILoadersContext& context, + int priority) : + context_(context), + priority_(priority), + width_(128), + height_(128) + { + } + + + boost::shared_ptr SeriesThumbnailsLoader::Factory::Create(ILoadersContext::ILock& stone) + { + boost::shared_ptr result(new SeriesThumbnailsLoader(stone.GetContext(), priority_)); + result->Register(stone.GetOracleObservable(), &SeriesThumbnailsLoader::Handle); + result->Register(stone.GetOracleObservable(), &SeriesThumbnailsLoader::Handle); + result->Register(stone.GetOracleObservable(), &SeriesThumbnailsLoader::Handle); + result->Register(stone.GetOracleObservable(), &SeriesThumbnailsLoader::Handle); + return result; + } + + + void SeriesThumbnailsLoader::SetThumbnailSize(unsigned int width, + unsigned int height) + { + if (width <= 0 || + height <= 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + width_ = width; + height_ = height; + } + } + + + void SeriesThumbnailsLoader::Clear() + { + for (Thumbnails::iterator it = thumbnails_.begin(); it != thumbnails_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + + thumbnails_.clear(); + } + + + SeriesThumbnailType SeriesThumbnailsLoader::GetSeriesThumbnail(std::string& image, + std::string& mime, + const std::string& seriesInstanceUid) const + { + Thumbnails::const_iterator found = thumbnails_.find(seriesInstanceUid); + + if (found == thumbnails_.end()) + { + return SeriesThumbnailType_NotLoaded; + } + else + { + assert(found->second != NULL); + image.assign(found->second->GetImage()); + mime.assign(found->second->GetMime()); + return found->second->GetType(); + } + } + + + void SeriesThumbnailsLoader::ScheduleLoadThumbnail(const DicomSource& source, + const std::string& patientId, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid) + { + if (source.IsDicomWeb()) + { + if (!source.HasDicomWebRendered()) + { + // TODO - Could use DCMTK here + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol, + "DICOMweb server is not able to generate renderings of DICOM series"); + } + + const std::string uri = ("/studies/" + studyInstanceUid + + "/series/" + seriesInstanceUid + "/rendered"); + + std::map arguments, headers; + arguments["viewport"] = (boost::lexical_cast(width_) + "," + + boost::lexical_cast(height_)); + + // Needed to set this header explicitly, as long as emscripten + // does not include macro "EMSCRIPTEN_FETCH_RESPONSE_HEADERS" + // https://github.com/emscripten-core/emscripten/pull/8486 + headers["Accept"] = Orthanc::MIME_JPEG; + + std::auto_ptr command( + source.CreateDicomWebCommand( + uri, arguments, headers, new DicomWebThumbnailHandler( + shared_from_this(), source, studyInstanceUid, seriesInstanceUid))); + Schedule(command.release()); + } + else if (source.IsOrthanc()) + { + // Dummy SOP Instance UID, as we are working at the "series" level + Orthanc::DicomInstanceHasher hasher(patientId, studyInstanceUid, seriesInstanceUid, "dummy"); + + std::auto_ptr command(new OrthancRestApiCommand); + command->SetUri("/series/" + hasher.HashSeries()); + command->AcquirePayload(new SelectOrthancInstanceHandler( + shared_from_this(), source, studyInstanceUid, seriesInstanceUid)); + Schedule(command.release()); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, + "Can only load thumbnails from Orthanc or DICOMweb"); + } + } +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/SeriesThumbnailsLoader.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/SeriesThumbnailsLoader.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,210 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../Oracle/GetOrthancImageCommand.h" +#include "../Oracle/HttpCommand.h" +#include "../Oracle/OracleCommandExceptionMessage.h" +#include "../Oracle/OrthancRestApiCommand.h" +#include "DicomSource.h" +#include "ILoaderFactory.h" +#include "OracleScheduler.h" + + +namespace OrthancStone +{ + enum SeriesThumbnailType + { + SeriesThumbnailType_NotLoaded = 1, // The remote server cannot decode this image + SeriesThumbnailType_Unsupported = 2, // The remote server cannot decode this image + SeriesThumbnailType_Pdf = 3, + SeriesThumbnailType_Video = 4, + SeriesThumbnailType_Image = 5 + }; + + + class SeriesThumbnailsLoader : + public IObservable, + public ObserverBase + { + private: + class Thumbnail : public boost::noncopyable + { + private: + SeriesThumbnailType type_; + std::string image_; + std::string mime_; + + public: + Thumbnail(const std::string& image, + const std::string& mime); + + Thumbnail(SeriesThumbnailType type); + + SeriesThumbnailType GetType() const + { + return type_; + } + + const std::string& GetImage() const + { + return image_; + } + + const std::string& GetMime() const + { + return mime_; + } + }; + + public: + class ThumbnailLoadedMessage : public OriginMessage + { + ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); + + private: + const DicomSource& source_; + const std::string& studyInstanceUid_; + const std::string& seriesInstanceUid_; + const Thumbnail& thumbnail_; + + public: + ThumbnailLoadedMessage(const SeriesThumbnailsLoader& origin, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + const Thumbnail& thumbnail) : + OriginMessage(origin), + source_(source), + studyInstanceUid_(studyInstanceUid), + seriesInstanceUid_(seriesInstanceUid), + thumbnail_(thumbnail) + { + } + + const DicomSource& GetDicomSource() const + { + return source_; + } + + SeriesThumbnailType GetType() const + { + return thumbnail_.GetType(); + } + + const std::string& GetStudyInstanceUid() const + { + return studyInstanceUid_; + } + + const std::string& GetSeriesInstanceUid() const + { + return seriesInstanceUid_; + } + + const std::string& GetEncodedImage() const + { + return thumbnail_.GetImage(); + } + + const std::string& GetMime() const + { + return thumbnail_.GetMime(); + } + }; + + private: + class Handler; + class DicomWebSopClassHandler; + class DicomWebThumbnailHandler; + class ThumbnailInformation; + class OrthancSopClassHandler; + class SelectOrthancInstanceHandler; + + // Maps a "Series Instance UID" to a thumbnail + typedef std::map Thumbnails; + + ILoadersContext& context_; + Thumbnails thumbnails_; + int priority_; + unsigned int width_; + unsigned int height_; + + void AcquireThumbnail(const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + Thumbnail* thumbnail /* takes ownership */); + + void Schedule(IOracleCommand* command); + + void Handle(const HttpCommand::SuccessMessage& message); + + void Handle(const OrthancRestApiCommand::SuccessMessage& message); + + void Handle(const GetOrthancImageCommand::SuccessMessage& message); + + void Handle(const OracleCommandExceptionMessage& message); + + SeriesThumbnailsLoader(ILoadersContext& context, + int priority); + + public: + class Factory : public ILoaderFactory + { + private: + int priority_; + + public: + Factory() : + priority_(0) + { + } + + void SetPriority(int priority) + { + priority_ = priority; + } + + virtual boost::shared_ptr Create(ILoadersContext::ILock& context); + }; + + + virtual ~SeriesThumbnailsLoader() + { + Clear(); + } + + void SetThumbnailSize(unsigned int width, + unsigned int height); + + void Clear(); + + SeriesThumbnailType GetSeriesThumbnail(std::string& image, + std::string& mime, + const std::string& seriesInstanceUid) const; + + void ScheduleLoadThumbnail(const DicomSource& source, + const std::string& patientId, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid); + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/WebAssemblyLoadersContext.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/WebAssemblyLoadersContext.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,96 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "WebAssemblyLoadersContext.h" + +namespace OrthancStone +{ + class WebAssemblyLoadersContext::Locker : public ILoadersContext::ILock + { + private: + WebAssemblyLoadersContext& that_; + + public: + Locker(WebAssemblyLoadersContext& that) : + that_(that) + { + } + + virtual ILoadersContext& GetContext() const ORTHANC_OVERRIDE + { + return that_; + } + + virtual IObservable& GetOracleObservable() const ORTHANC_OVERRIDE + { + return that_.oracle_.GetOracleObservable(); + } + + virtual void Schedule(boost::shared_ptr receiver, + int priority, + IOracleCommand* command /* Takes ownership */) ORTHANC_OVERRIDE + { + that_.scheduler_->Schedule(receiver, priority, command); + } + + virtual void CancelRequests(boost::shared_ptr receiver) ORTHANC_OVERRIDE + { + that_.scheduler_->CancelRequests(receiver); + } + + virtual void CancelAllRequests() ORTHANC_OVERRIDE + { + that_.scheduler_->CancelAllRequests(); + } + + virtual void AddLoader(boost::shared_ptr loader) ORTHANC_OVERRIDE + { + that_.loaders_.push_back(loader); + } + + virtual void GetStatistics(uint64_t& scheduledCommands, + uint64_t& processedCommands) ORTHANC_OVERRIDE + { + scheduledCommands = that_.scheduler_->GetTotalScheduled(); + processedCommands = that_.scheduler_->GetTotalProcessed(); + } + }; + + + WebAssemblyLoadersContext::WebAssemblyLoadersContext(unsigned int maxHighPriority, + unsigned int maxStandardPriority, + unsigned int maxLowPriority) + { + scheduler_ = OracleScheduler::Create(oracle_, oracle_.GetOracleObservable(), oracle_, + maxHighPriority, maxStandardPriority, maxLowPriority); + + if (!scheduler_) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + + + ILoadersContext::ILock* WebAssemblyLoadersContext::Lock() + { + return new Locker(*this); + } +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Loaders/WebAssemblyLoadersContext.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/WebAssemblyLoadersContext.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,61 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "ILoadersContext.h" +#include "../Oracle/WebAssemblyOracle.h" +#include "OracleScheduler.h" + +namespace OrthancStone +{ + class WebAssemblyLoadersContext : public ILoadersContext + { + private: + class Locker; + + WebAssemblyOracle oracle_; + boost::shared_ptr scheduler_; + std::list< boost::shared_ptr > loaders_; + + public: + WebAssemblyLoadersContext(unsigned int maxHighPriority, + unsigned int maxStandardPriority, + unsigned int maxLowPriority); + + void SetLocalOrthanc(const std::string& root) + { + oracle_.SetLocalOrthanc(root); + } + + void SetRemoteOrthanc(const Orthanc::WebServiceParameters& orthanc) + { + oracle_.SetRemoteOrthanc(orthanc); + } + + void SetDicomCacheSize(size_t size) + { + oracle_.SetDicomCacheSize(size); + } + + virtual ILock* Lock() ORTHANC_OVERRIDE; + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Messages/ICallable.h --- a/Framework/Messages/ICallable.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Messages/ICallable.h Tue Feb 04 15:24:03 2020 +0100 @@ -22,21 +22,21 @@ #pragma once #include "IMessage.h" +#include "IObserver.h" #include #include +#include #include #include -namespace OrthancStone { - - class IObserver; - +namespace OrthancStone +{ // This is referencing an object and member function that can be notified // by an IObservable. The object must derive from IO - // The member functions must be of type "void Function(const IMessage& message)" or reference a derived class of IMessage + // The member functions must be of type "void Method(const IMessage& message)" or reference a derived class of IMessage class ICallable : public boost::noncopyable { public: @@ -48,61 +48,38 @@ virtual const MessageIdentifier& GetMessageIdentifier() = 0; - virtual IObserver* GetObserver() const = 0; - }; - - template - class MessageHandler: public ICallable - { + // TODO - Is this needed? + virtual boost::weak_ptr GetObserver() const = 0; }; template - class Callable : public MessageHandler + class Callable : public ICallable { private: - typedef void (TObserver::* MemberFunction) (const TMessage&); + typedef void (TObserver::* MemberMethod) (const TMessage&); - TObserver& observer_; - MemberFunction function_; - int64_t observerFingerprint_; + boost::weak_ptr observer_; + MemberMethod function_; public: - Callable(TObserver& observer, - MemberFunction function) : + Callable(boost::shared_ptr observer, + MemberMethod function) : observer_(observer), - function_(function), - observerFingerprint_(observer.GetFingerprint()) - { - } - - void ApplyInternal(const TMessage& message) + function_(function) { - int64_t currentFingerprint(observer_.GetFingerprint()); - if (observerFingerprint_ != currentFingerprint) - { - LOG(TRACE) << "The observer at address " << - std::hex << &observer_ << std::dec << - ") has a different fingerprint than the one recorded at callback " << - "registration time. This means that it is not the same object as " << - "the one recorded, even though their addresses are the same. " << - "Callback will NOT be sent!"; - LOG(TRACE) << " recorded fingerprint = " << observerFingerprint_ << - " current fingerprint = " << currentFingerprint; - } - else - { - LOG(TRACE) << "The recorded fingerprint is " << observerFingerprint_ - << " and the current fingerprint is " << currentFingerprint - << " -- callable will be called."; - (observer_.*function_) (message); - } } virtual void Apply(const IMessage& message) { - ApplyInternal(dynamic_cast(message)); + boost::shared_ptr lock(observer_); + if (lock) + { + TObserver& observer = dynamic_cast(*lock); + const TMessage& typedMessage = dynamic_cast(message); + (observer.*function_) (typedMessage); + } } virtual const MessageIdentifier& GetMessageIdentifier() @@ -110,41 +87,9 @@ return TMessage::GetStaticIdentifier(); } - virtual IObserver* GetObserver() const + virtual boost::weak_ptr GetObserver() const { - return &observer_; + return observer_; } }; - -#if 0 /* __cplusplus >= 201103L*/ - -#include - - template - class LambdaCallable : public MessageHandler - { - private: - - IObserver& observer_; - std::function lambda_; - - public: - LambdaCallable(IObserver& observer, - std::function lambdaFunction) : - observer_(observer), - lambda_(lambdaFunction) - { - } - - virtual void Apply(const IMessage& message) - { - lambda_(dynamic_cast(message)); - } - - virtual IObserver* GetObserver() const - { - return &observer_; - } - }; -#endif //__cplusplus >= 201103L } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Messages/IMessage.h --- a/Framework/Messages/IMessage.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Messages/IMessage.h Tue Feb 04 15:24:03 2020 +0100 @@ -21,6 +21,7 @@ #pragma once +#include #include #include @@ -33,6 +34,12 @@ const char* file_; int line_; + bool IsEqual(const MessageIdentifier& other) const + { + return (line_ == other.line_ && + strcmp(file_, other.file_) == 0); + } + public: MessageIdentifier(const char* file, int line) : @@ -47,6 +54,11 @@ { } + std::string AsString() const + { + return std::string(file_) + ":" + boost::lexical_cast(line_); + } + bool operator< (const MessageIdentifier& other) const { if (file_ == NULL) @@ -62,6 +74,16 @@ return strcmp(file_, other.file_) < 0; } } + + bool operator== (const MessageIdentifier& other) const + { + return IsEqual(other); + } + + bool operator!= (const MessageIdentifier& other) const + { + return !IsEqual(other); + } }; diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Messages/IMessageEmitter.h --- a/Framework/Messages/IMessageEmitter.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Messages/IMessageEmitter.h Tue Feb 04 15:24:03 2020 +0100 @@ -24,6 +24,8 @@ #include "IObserver.h" #include "IMessage.h" +#include + namespace OrthancStone { /** @@ -39,7 +41,7 @@ { } - virtual void EmitMessage(const IObserver& observer, + virtual void EmitMessage(boost::weak_ptr observer, const IMessage& message) = 0; }; } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Messages/IObservable.cpp --- a/Framework/Messages/IObservable.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Messages/IObservable.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -21,8 +21,9 @@ #include "IObservable.h" +#include "../StoneException.h" + #include -#include #include @@ -40,20 +41,10 @@ delete *it2; } } - - // unregister the forwarders but don't delete them (they'll be - // deleted by the observable they are observing as any other - // callable) - for (Forwarders::iterator it = forwarders_.begin(); - it != forwarders_.end(); ++it) - { - IMessageForwarder* fw = *it; - broker_.Unregister(dynamic_cast(*fw)); - } } - void IObservable::RegisterObserverCallback(ICallable* callable) + void IObservable::RegisterCallable(ICallable* callable) { if (callable == NULL) { @@ -64,35 +55,10 @@ callables_[id].insert(callable); } - void IObservable::Unregister(IObserver *observer) - { - LOG(TRACE) << "IObservable::Unregister for IObserver at addr: " - << std::hex << observer << std::dec; - // delete all callables from this observer - for (Callables::iterator itCallableSet = callables_.begin(); - itCallableSet != callables_.end(); ++itCallableSet) - { - for (std::set::const_iterator - itCallable = itCallableSet->second.begin(); itCallable != itCallableSet->second.end(); ) - { - if ((*itCallable)->GetObserver() == observer) - { - LOG(TRACE) << " ** IObservable::Unregister : deleting callable: " - << std::hex << (*itCallable) << std::dec; - delete *itCallable; - itCallableSet->second.erase(itCallable++); - } - else - ++itCallable; - } - } - } - void IObservable::EmitMessageInternal(const IObserver* receiver, const IMessage& message) { - LOG(TRACE) << "IObservable::EmitMessageInternal receiver = " - << std::hex << receiver << std::dec; + //LOG(TRACE) << "IObservable::EmitMessageInternal receiver = " << std::hex << receiver << std::dec; Callables::const_iterator found = callables_.find(message.GetIdentifier()); if (found != callables_.end()) @@ -102,15 +68,36 @@ { assert(*it != NULL); - const IObserver* observer = (*it)->GetObserver(); - if (broker_.IsActive(*observer)) + boost::shared_ptr observer((*it)->GetObserver().lock()); + + if (observer) { if (receiver == NULL || // Are we broadcasting? - observer == receiver) // Not broadcasting, but this is the receiver + observer.get() == receiver) // Not broadcasting, but this is the receiver { - (*it)->Apply(message); + try + { + (*it)->Apply(message); + } + catch (Orthanc::OrthancException& e) + { + LOG(ERROR) << "Exception on callable: " << e.What(); + } + catch (StoneException& e) + { + LOG(ERROR) << "Exception on callable: " << e.What(); + } + catch (...) + { + LOG(ERROR) << "Native exception on callable"; + } } } + else + { + // TODO => Remove "it" from the list of callables => This + // allows to suppress the need for "Unregister()" + } } } } @@ -122,21 +109,15 @@ } - void IObservable::EmitMessage(const IObserver& observer, + void IObservable::EmitMessage(boost::weak_ptr observer, const IMessage& message) { - LOG(TRACE) << "IObservable::EmitMessage observer = " - << std::hex << &observer << std::dec; - EmitMessageInternal(&observer, message); - } - - void IObservable::RegisterForwarder(IMessageForwarder* forwarder) - { - if (forwarder == NULL) + //LOG(TRACE) << "IObservable::EmitMessage observer = " << std::hex << &observer << std::dec; + + boost::shared_ptr lock(observer.lock()); + if (lock) { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + EmitMessageInternal(lock.get(), message); } - - forwarders_.insert(forwarder); } } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Messages/IObservable.h --- a/Framework/Messages/IObservable.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Messages/IObservable.h Tue Feb 04 15:24:03 2020 +0100 @@ -24,8 +24,6 @@ #include "../StoneEnumerations.h" #include "ICallable.h" #include "IObserver.h" -#include "MessageBroker.h" -#include "MessageForwarder.h" #include #include @@ -37,39 +35,20 @@ private: typedef std::map > Callables; - typedef std::set Forwarders; - - MessageBroker& broker_; Callables callables_; - Forwarders forwarders_; void EmitMessageInternal(const IObserver* receiver, const IMessage& message); public: - IObservable(MessageBroker& broker) : - broker_(broker) - { - } - virtual ~IObservable(); - MessageBroker& GetBroker() const - { - return broker_; - } - - // Takes ownsership - void RegisterObserverCallback(ICallable* callable); - - void Unregister(IObserver* observer); + // Takes ownsership of the callable + void RegisterCallable(ICallable* callable); void BroadcastMessage(const IMessage& message); - void EmitMessage(const IObserver& observer, + void EmitMessage(boost::weak_ptr observer, const IMessage& message); - - // Takes ownsership - void RegisterForwarder(IMessageForwarder* forwarder); }; } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Messages/IObserver.cpp --- a/Framework/Messages/IObserver.cpp Tue Feb 04 15:23:33 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,89 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - - -#include "IObserver.h" - -#include "IMessage.h" -#include "../StoneException.h" - -#include -#include - -namespace OrthancStone -{ - static const uint64_t IObserver_FIRST_UNIQUE_ID = 10973; - static uint64_t IObserver_nextUniqueId = IObserver_FIRST_UNIQUE_ID; - - IObserver::IObserver(MessageBroker& broker) - : broker_(broker) - { - AssignFingerprint(); - broker_.Register(*this); - } - - IObserver::~IObserver() - { - try - { - LOG(TRACE) << "IObserver(" << std::hex << this << std::dec << ")::~IObserver : fingerprint_[0] == " << fingerprint_[0]; - fingerprint_[0] = 0xdeadbeef; - fingerprint_[1] = 0xdeadbeef; - fingerprint_[2] = 0xdeadbeef; - broker_.Unregister(*this); - } - catch (const Orthanc::OrthancException& e) - { - if (e.HasDetails()) - { - LOG(ERROR) << "OrthancException in ~IObserver: " << e.What() << " Details: " << e.GetDetails(); - } - else - { - LOG(ERROR) << "OrthancException in ~IObserver: " << e.What(); - } - } - catch (const std::exception& e) - { - LOG(ERROR) << "std::exception in ~IObserver: " << e.what(); - } - catch (...) - { - LOG(ERROR) << "Unknown exception in ~IObserver"; - } - } - - static const int64_t IObserver_UNIQUE_ID_MAGIC_NUMBER = 2742024; - - void IObserver::AssignFingerprint() - { - fingerprint_[0] = IObserver_nextUniqueId; - fingerprint_[1] = fingerprint_[0] / 2; - fingerprint_[2] = fingerprint_[1] + IObserver_UNIQUE_ID_MAGIC_NUMBER; - IObserver_nextUniqueId++; - } - - bool IObserver::DoesFingerprintLookGood() const - { - return (fingerprint_[0] >= IObserver_FIRST_UNIQUE_ID) && - (fingerprint_[1] == fingerprint_[0] / 2) && - (fingerprint_[2] == fingerprint_[1] + IObserver_UNIQUE_ID_MAGIC_NUMBER); - } -} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Messages/IObserver.h --- a/Framework/Messages/IObserver.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Messages/IObserver.h Tue Feb 04 15:24:03 2020 +0100 @@ -21,7 +21,7 @@ #pragma once -#include "MessageBroker.h" +#include #include @@ -29,29 +29,13 @@ { class IObserver : public boost::noncopyable { - private: - MessageBroker& broker_; - // the following is a int64_t with some checks that is used to - // disambiguate different observers that may have the same address - int64_t fingerprint_[3]; - - void AssignFingerprint(); - public: - IObserver(MessageBroker& broker); - - virtual ~IObserver(); - - int64_t GetFingerprint() const + IObserver() { - return fingerprint_[0]; } - bool DoesFingerprintLookGood() const; - - MessageBroker& GetBroker() const + virtual ~IObserver() { - return broker_; } }; } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Messages/LockingEmitter.h --- a/Framework/Messages/LockingEmitter.h Tue Feb 04 15:23:33 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,114 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - -#pragma once - -#include -#include - -#include "IMessageEmitter.h" -#include "IObservable.h" - -#include - -namespace OrthancStone -{ - /** - * This class is used when using the ThreadedOracle : since messages - * can be sent from multiple Oracle threads, this IMessageEmitter - * implementation serializes the callbacks. - * - * The internal mutex used in Oracle messaging can also be used to - * protect the application data. Thus, this class can be used as a single - * application-wide mutex. - */ - class LockingEmitter : public IMessageEmitter - { - private: - boost::shared_mutex mutex_; - MessageBroker broker_; - IObservable oracleObservable_; - - public: - LockingEmitter() : - oracleObservable_(broker_) - { - } - - MessageBroker& GetBroker() - { - return broker_; - } - - virtual void EmitMessage(const IObserver& observer, - const IMessage& message) ORTHANC_OVERRIDE - { - try - { - boost::unique_lock lock(mutex_); - oracleObservable_.EmitMessage(observer, message); - } - catch (Orthanc::OrthancException& e) - { - LOG(ERROR) << "Exception while emitting a message: " << e.What(); - } - } - - - class ReaderLock : public boost::noncopyable - { - private: - LockingEmitter& that_; - boost::shared_lock lock_; - - public: - ReaderLock(LockingEmitter& that) : - that_(that), - lock_(that.mutex_) - { - } - }; - - - class WriterLock : public boost::noncopyable - { - private: - LockingEmitter& that_; - boost::unique_lock lock_; - - public: - WriterLock(LockingEmitter& that) : - that_(that), - lock_(that.mutex_) - { - } - - MessageBroker& GetBroker() - { - return that_.broker_; - } - - IObservable& GetOracleObservable() - { - return that_.oracleObservable_; - } - }; - }; -} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Messages/MessageBroker.h --- a/Framework/Messages/MessageBroker.h Tue Feb 04 15:23:33 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,61 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - -#pragma once - -#include "boost/noncopyable.hpp" - -#include - -namespace OrthancStone -{ - class IObserver; - - /* - * This is a central message broker. It keeps track of all observers and knows - * when an observer is deleted. - * This way, it can prevent an observable to send a message to a deleted observer. - */ - class MessageBroker : public boost::noncopyable - { - private: - std::set activeObservers_; // the list of observers that are currently alive (that have not been deleted) - - public: - MessageBroker() - { - } - - void Register(const IObserver& observer) - { - activeObservers_.insert(&observer); - } - - void Unregister(const IObserver& observer) - { - activeObservers_.erase(&observer); - } - - bool IsActive(const IObserver& observer) - { - return activeObservers_.find(&observer) != activeObservers_.end(); - } - }; -} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Messages/MessageForwarder.cpp --- a/Framework/Messages/MessageForwarder.cpp Tue Feb 04 15:23:33 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,38 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - - -#include "MessageForwarder.h" - -#include "IObservable.h" - -namespace OrthancStone -{ - - void IMessageForwarder::ForwardMessageInternal(const IMessage& message) - { - emitter_.BroadcastMessage(message); - } - - void IMessageForwarder::RegisterForwarderInEmitter() - { - emitter_.RegisterForwarder(this); - } -} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Messages/MessageForwarder.h --- a/Framework/Messages/MessageForwarder.h Tue Feb 04 15:23:33 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,87 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "ICallable.h" -#include "IObserver.h" - -#include - -namespace OrthancStone -{ - - class IObservable; - - class IMessageForwarder : public IObserver - { - IObservable& emitter_; - public: - IMessageForwarder(MessageBroker& broker, IObservable& emitter) - : IObserver(broker), - emitter_(emitter) - {} - virtual ~IMessageForwarder() {} - - protected: - void ForwardMessageInternal(const IMessage& message); - void RegisterForwarderInEmitter(); - - }; - - /* When an Observer (B) simply needs to re-emit a message it has received, instead of implementing - * a specific member function to forward the message, it can create a MessageForwarder. - * The MessageForwarder will re-emit the message "in the name of (B)" - * - * Consider the chain where - * A is an observable - * | - * B is an observer of A and observable - * | - * C is an observer of B and knows that B is re-emitting many messages from A - * - * instead of implementing a callback, B will create a MessageForwarder that will emit the messages in his name: - * A.RegisterObserverCallback(new MessageForwarder(broker, *this) // where "this" is B - * - * in C: - * B.RegisterObserverCallback(new Callable(*this, &B::MyCallback)) // where "this" is C - */ - template - class MessageForwarder : public IMessageForwarder, public Callable, TMessage> - { - public: - MessageForwarder(MessageBroker& broker, - IObservable& emitter // the object that will emit the messages to forward - ) - : IMessageForwarder(broker, emitter), - Callable, TMessage>(*this, &MessageForwarder::ForwardMessage) - { - RegisterForwarderInEmitter(); - } - -protected: - void ForwardMessage(const TMessage& message) - { - ForwardMessageInternal(message); - } - - }; -} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Messages/ObserverBase.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Messages/ObserverBase.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,68 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "ICallable.h" +#include "IObserver.h" +#include "IObservable.h" + +#include + +#include + +namespace OrthancStone +{ + template + class ObserverBase : + public IObserver, + public boost::enable_shared_from_this + { + public: + boost::shared_ptr GetSharedObserver() + { + try + { + return this->shared_from_this(); + } + catch (boost::bad_weak_ptr&) + { + throw Orthanc::OrthancException( + Orthanc::ErrorCode_InternalError, + "Cannot get a shared pointer to an observer from its constructor, " + "or the observer is not created as a shared pointer"); + } + } + + template + ICallable* CreateCallable(void (TObserver::* MemberMethod) (const TMessage&)) + { + return new Callable(GetSharedObserver(), MemberMethod); + } + + template + void Register(IObservable& observable, + void (TObserver::* MemberMethod) (const TMessage&)) + { + observable.RegisterCallable(CreateCallable(MemberMethod)); + } + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/OpenGL/OpenGLIncludes.h --- a/Framework/OpenGL/OpenGLIncludes.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/OpenGL/OpenGLIncludes.h Tue Feb 04 15:24:03 2020 +0100 @@ -32,6 +32,8 @@ #if defined(__APPLE__) # include # include +#elif defined(QT_VERSION_MAJOR) && (QT_VERSION >= 5) +// Qt5 takes care of the inclusions #elif defined(_WIN32) // On Windows, use the compatibility headers provided by glew # include diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/OpenGL/SdlOpenGLContext.cpp --- a/Framework/OpenGL/SdlOpenGLContext.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/OpenGL/SdlOpenGLContext.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -68,6 +68,7 @@ GLenum err = glewInit(); if (GLEW_OK != err) { + LOG(ERROR) << glewGetErrorString(err); throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "Cannot initialize glew"); } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/OpenGL/SdlOpenGLContext.h --- a/Framework/OpenGL/SdlOpenGLContext.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/OpenGL/SdlOpenGLContext.h Tue Feb 04 15:24:03 2020 +0100 @@ -26,6 +26,8 @@ #include "IOpenGLContext.h" #include "../Viewport/SdlWindow.h" +#include + #include namespace OrthancStone @@ -62,6 +64,11 @@ virtual unsigned int GetCanvasWidth() const ORTHANC_OVERRIDE; virtual unsigned int GetCanvasHeight() const ORTHANC_OVERRIDE; + + void ToggleMaximize() + { + window_.ToggleMaximize(); + } }; } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Oracle/GenericOracleRunner.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Oracle/GenericOracleRunner.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,521 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "GenericOracleRunner.h" + +#if !defined(ORTHANC_ENABLE_DCMTK) +# error The macro ORTHANC_ENABLE_DCMTK must be defined +#endif + +#include "GetOrthancImageCommand.h" +#include "GetOrthancWebViewerJpegCommand.h" +#include "HttpCommand.h" +#include "OracleCommandExceptionMessage.h" +#include "OrthancRestApiCommand.h" +#include "ParseDicomFromFileCommand.h" +#include "ParseDicomFromWadoCommand.h" +#include "ReadFileCommand.h" + +#if ORTHANC_ENABLE_DCMTK == 1 +# include "ParseDicomSuccessMessage.h" +# include +# include +static unsigned int BUCKET_DICOMDIR = 0; +static unsigned int BUCKET_SOP = 1; +#endif + +#include +#include +#include +#include +#include + +#include + + + +namespace OrthancStone +{ + static void CopyHttpHeaders(Orthanc::HttpClient& client, + const Orthanc::HttpClient::HttpHeaders& headers) + { + for (Orthanc::HttpClient::HttpHeaders::const_iterator + it = headers.begin(); it != headers.end(); it++ ) + { + client.AddHeader(it->first, it->second); + } + } + + + static void DecodeAnswer(std::string& answer, + const Orthanc::HttpClient::HttpHeaders& headers) + { + Orthanc::HttpCompression contentEncoding = Orthanc::HttpCompression_None; + + for (Orthanc::HttpClient::HttpHeaders::const_iterator it = headers.begin(); + it != headers.end(); ++it) + { + std::string s; + Orthanc::Toolbox::ToLowerCase(s, it->first); + + if (s == "content-encoding") + { + if (it->second == "gzip") + { + contentEncoding = Orthanc::HttpCompression_Gzip; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol, + "Unsupported HTTP Content-Encoding: " + it->second); + } + + break; + } + } + + if (contentEncoding == Orthanc::HttpCompression_Gzip) + { + std::string compressed; + answer.swap(compressed); + + Orthanc::GzipCompressor compressor; + compressor.Uncompress(answer, compressed.c_str(), compressed.size()); + + LOG(INFO) << "Uncompressing gzip Encoding: from " << compressed.size() + << " to " << answer.size() << " bytes"; + } + } + + + static void RunHttpCommand(std::string& answer, + Orthanc::HttpClient::HttpHeaders& answerHeaders, + const HttpCommand& command) + { + Orthanc::HttpClient client; + client.SetUrl(command.GetUrl()); + client.SetMethod(command.GetMethod()); + client.SetTimeout(command.GetTimeout()); + + CopyHttpHeaders(client, command.GetHttpHeaders()); + + if (command.HasCredentials()) + { + client.SetCredentials(command.GetUsername().c_str(), command.GetPassword().c_str()); + } + + if (command.GetMethod() == Orthanc::HttpMethod_Post || + command.GetMethod() == Orthanc::HttpMethod_Put) + { + client.SetBody(command.GetBody()); + } + + client.ApplyAndThrowException(answer, answerHeaders); + DecodeAnswer(answer, answerHeaders); + } + + + static void RunInternal(boost::weak_ptr receiver, + IMessageEmitter& emitter, + const HttpCommand& command) + { + std::string answer; + Orthanc::HttpClient::HttpHeaders answerHeaders; + RunHttpCommand(answer, answerHeaders, command); + + HttpCommand::SuccessMessage message(command, answerHeaders, answer); + emitter.EmitMessage(receiver, message); + } + + + static void RunOrthancRestApiCommand(std::string& answer, + Orthanc::HttpClient::HttpHeaders& answerHeaders, + const Orthanc::WebServiceParameters& orthanc, + const OrthancRestApiCommand& command) + { + Orthanc::HttpClient client(orthanc, command.GetUri()); + client.SetRedirectionFollowed(false); + client.SetMethod(command.GetMethod()); + client.SetTimeout(command.GetTimeout()); + + CopyHttpHeaders(client, command.GetHttpHeaders()); + + if (command.GetMethod() == Orthanc::HttpMethod_Post || + command.GetMethod() == Orthanc::HttpMethod_Put) + { + client.SetBody(command.GetBody()); + } + + client.ApplyAndThrowException(answer, answerHeaders); + DecodeAnswer(answer, answerHeaders); + } + + + static void RunInternal(boost::weak_ptr receiver, + IMessageEmitter& emitter, + const Orthanc::WebServiceParameters& orthanc, + const OrthancRestApiCommand& command) + { + std::string answer; + Orthanc::HttpClient::HttpHeaders answerHeaders; + RunOrthancRestApiCommand(answer, answerHeaders, orthanc, command); + + OrthancRestApiCommand::SuccessMessage message(command, answerHeaders, answer); + emitter.EmitMessage(receiver, message); + } + + + static void RunInternal(boost::weak_ptr receiver, + IMessageEmitter& emitter, + const Orthanc::WebServiceParameters& orthanc, + const GetOrthancImageCommand& command) + { + Orthanc::HttpClient client(orthanc, command.GetUri()); + client.SetRedirectionFollowed(false); + client.SetTimeout(command.GetTimeout()); + + CopyHttpHeaders(client, command.GetHttpHeaders()); + + std::string answer; + Orthanc::HttpClient::HttpHeaders answerHeaders; + client.ApplyAndThrowException(answer, answerHeaders); + + DecodeAnswer(answer, answerHeaders); + + command.ProcessHttpAnswer(receiver, emitter, answer, answerHeaders); + } + + + static void RunInternal(boost::weak_ptr receiver, + IMessageEmitter& emitter, + const Orthanc::WebServiceParameters& orthanc, + const GetOrthancWebViewerJpegCommand& command) + { + Orthanc::HttpClient client(orthanc, command.GetUri()); + client.SetRedirectionFollowed(false); + client.SetTimeout(command.GetTimeout()); + + CopyHttpHeaders(client, command.GetHttpHeaders()); + + std::string answer; + Orthanc::HttpClient::HttpHeaders answerHeaders; + client.ApplyAndThrowException(answer, answerHeaders); + + DecodeAnswer(answer, answerHeaders); + + command.ProcessHttpAnswer(receiver, emitter, answer); + } + + + static std::string GetPath(const std::string& root, + const std::string& file) + { + boost::filesystem::path a(root); + boost::filesystem::path b(file); + + boost::filesystem::path c; + if (b.is_absolute()) + { + c = b; + } + else + { + c = a / b; + } + + return c.string(); + } + + + static void RunInternal(boost::weak_ptr receiver, + IMessageEmitter& emitter, + const std::string& root, + const ReadFileCommand& command) + { + std::string path = GetPath(root, command.GetPath()); + LOG(TRACE) << "Oracle reading file: " << path; + + std::string content; + Orthanc::SystemToolbox::ReadFile(content, path, true /* log */); + + ReadFileCommand::SuccessMessage message(command, content); + emitter.EmitMessage(receiver, message); + } + + +#if ORTHANC_ENABLE_DCMTK == 1 + static Orthanc::ParsedDicomFile* ParseDicom(uint64_t& fileSize, /* OUT */ + const std::string& path, + bool isPixelData) + { + if (!Orthanc::SystemToolbox::IsRegularFile(path)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentFile); + } + + LOG(TRACE) << "Parsing DICOM file, " << (isPixelData ? "with" : "without") + << " pixel data: " << path; + + boost::posix_time::ptime start = boost::posix_time::microsec_clock::local_time(); + + fileSize = Orthanc::SystemToolbox::GetFileSize(path); + + // Check for 32bit systems + if (fileSize != static_cast(static_cast(fileSize))) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotEnoughMemory); + } + + DcmFileFormat dicom; + bool ok; + + if (isPixelData) + { + ok = dicom.loadFile(path.c_str()).good(); + } + else + { +#if DCMTK_VERSION_NUMBER >= 362 + /** + * NB : We could stop at (0x3007, 0x0000) instead of + * DCM_PixelData as the Stone framework does not use further + * tags (cf. the Orthanc::DICOM_TAG_* constants), but we still + * use "PixelData" as this does not change the runtime much, and + * as it is more explicit. + **/ + static const DcmTagKey STOP = DCM_PixelData; + //static const DcmTagKey STOP(0x3007, 0x0000); + + ok = dicom.loadFileUntilTag(path.c_str(), EXS_Unknown, EGL_noChange, + DCM_MaxReadLength, ERM_autoDetect, STOP).good(); +#else + // The primitive "loadFileUntilTag" was introduced in DCMTK 3.6.2 + ok = dicom.loadFile(path.c_str()).good(); +#endif + } + + if (ok) + { + std::auto_ptr result(new Orthanc::ParsedDicomFile(dicom)); + + boost::posix_time::ptime end = boost::posix_time::microsec_clock::local_time(); + LOG(TRACE) << path << ": parsed in " << (end-start).total_milliseconds() << " ms"; + + return result.release(); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "Cannot parse file: " + path); + } + } + + + static void RunInternal(boost::weak_ptr receiver, + IMessageEmitter& emitter, + boost::shared_ptr cache, + const std::string& root, + const ParseDicomFromFileCommand& command) + { + const std::string path = GetPath(root, command.GetPath()); + + if (cache) + { + ParsedDicomCache::Reader reader(*cache, BUCKET_DICOMDIR, path); + if (reader.IsValid() && + (!command.IsPixelDataIncluded() || + reader.HasPixelData())) + { + // Reuse the DICOM file from the cache + ParseDicomSuccessMessage message(command, reader.GetDicom(), reader.GetFileSize(), reader.HasPixelData()); + emitter.EmitMessage(receiver, message); + return; + } + } + + uint64_t fileSize; + std::auto_ptr parsed(ParseDicom(fileSize, path, command.IsPixelDataIncluded())); + + if (fileSize != static_cast(fileSize)) + { + // Cannot load such a large file on 32-bit architecture + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotEnoughMemory); + } + + { + ParseDicomSuccessMessage message + (command, *parsed, static_cast(fileSize), command.IsPixelDataIncluded()); + emitter.EmitMessage(receiver, message); + } + + if (cache) + { + // Store it into the cache for future use + + // Invalidate to overwrite DICOM instance that would already + // be stored without pixel data + cache->Invalidate(BUCKET_DICOMDIR, path); + + cache->Acquire(BUCKET_DICOMDIR, path, parsed.release(), + static_cast(fileSize), command.IsPixelDataIncluded()); + } + } + + + static void RunInternal(boost::weak_ptr receiver, + IMessageEmitter& emitter, + boost::shared_ptr cache, + const Orthanc::WebServiceParameters& orthanc, + const ParseDicomFromWadoCommand& command) + { + if (cache) + { + ParsedDicomCache::Reader reader(*cache, BUCKET_SOP, command.GetSopInstanceUid()); + if (reader.IsValid() && + reader.HasPixelData()) + { + // Reuse the DICOM file from the cache + ParseDicomSuccessMessage message(command, reader.GetDicom(), reader.GetFileSize(), reader.HasPixelData()); + emitter.EmitMessage(receiver, message); + return; + } + } + + std::string answer; + Orthanc::HttpClient::HttpHeaders answerHeaders; + + switch (command.GetRestCommand().GetType()) + { + case IOracleCommand::Type_Http: + RunHttpCommand(answer, answerHeaders, dynamic_cast(command.GetRestCommand())); + break; + + case IOracleCommand::Type_OrthancRestApi: + RunOrthancRestApiCommand(answer, answerHeaders, orthanc, + dynamic_cast(command.GetRestCommand())); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + size_t fileSize; + std::auto_ptr parsed(ParseDicomSuccessMessage::ParseWadoAnswer(fileSize, answer, answerHeaders)); + + { + ParseDicomSuccessMessage message(command, *parsed, fileSize, + true /* pixel data always is included in WADO-RS */); + emitter.EmitMessage(receiver, message); + } + + if (cache) + { + // Store it into the cache for future use + cache->Acquire(BUCKET_SOP, command.GetSopInstanceUid(), parsed.release(), fileSize, true); + } + } +#endif + + + void GenericOracleRunner::Run(boost::weak_ptr receiver, + IMessageEmitter& emitter, + const IOracleCommand& command) + { + Orthanc::ErrorCode error = Orthanc::ErrorCode_Success; + + try + { + switch (command.GetType()) + { + case IOracleCommand::Type_Sleep: + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType, + "Sleep command cannot be executed by the runner"); + + case IOracleCommand::Type_Http: + RunInternal(receiver, emitter, dynamic_cast(command)); + break; + + case IOracleCommand::Type_OrthancRestApi: + RunInternal(receiver, emitter, orthanc_, + dynamic_cast(command)); + break; + + case IOracleCommand::Type_GetOrthancImage: + RunInternal(receiver, emitter, orthanc_, + dynamic_cast(command)); + break; + + case IOracleCommand::Type_GetOrthancWebViewerJpeg: + RunInternal(receiver, emitter, orthanc_, + dynamic_cast(command)); + break; + + case IOracleCommand::Type_ReadFile: + RunInternal(receiver, emitter, rootDirectory_, + dynamic_cast(command)); + break; + + case IOracleCommand::Type_ParseDicomFromFile: + case IOracleCommand::Type_ParseDicomFromWado: +#if ORTHANC_ENABLE_DCMTK == 1 + switch (command.GetType()) + { + case IOracleCommand::Type_ParseDicomFromFile: + RunInternal(receiver, emitter, dicomCache_, rootDirectory_, + dynamic_cast(command)); + break; + + case IOracleCommand::Type_ParseDicomFromWado: + RunInternal(receiver, emitter, dicomCache_, orthanc_, + dynamic_cast(command)); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + break; +#else + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, + "DCMTK must be enabled to parse DICOM files"); +#endif + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } + catch (Orthanc::OrthancException& e) + { + LOG(ERROR) << "Exception within the oracle: " << e.What(); + error = e.GetErrorCode(); + } + catch (...) + { + LOG(ERROR) << "Threaded exception within the oracle"; + error = Orthanc::ErrorCode_InternalError; + } + + if (error != Orthanc::ErrorCode_Success) + { + OracleCommandExceptionMessage message(command, error); + emitter.EmitMessage(receiver, message); + } + } +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Oracle/GenericOracleRunner.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Oracle/GenericOracleRunner.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,87 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#if !defined(ORTHANC_ENABLE_DCMTK) +# error The macro ORTHANC_ENABLE_DCMTK must be defined +#endif + +#if ORTHANC_ENABLE_DCMTK == 1 +# include "../Toolbox/ParsedDicomCache.h" +#endif + +#include "IOracleCommand.h" +#include "../Messages/IMessageEmitter.h" + +#include // For ORTHANC_OVERRIDE +#include + +namespace OrthancStone +{ + class GenericOracleRunner : public boost::noncopyable + { + private: + Orthanc::WebServiceParameters orthanc_; + std::string rootDirectory_; + +#if ORTHANC_ENABLE_DCMTK == 1 + boost::shared_ptr dicomCache_; +#endif + + public: + GenericOracleRunner() : + rootDirectory_(".") + { + } + + void SetOrthanc(const Orthanc::WebServiceParameters& orthanc) + { + orthanc_ = orthanc; + } + + const Orthanc::WebServiceParameters& GetOrthanc() const + { + return orthanc_; + } + + void SetRootDirectory(const std::string& rootDirectory) + { + rootDirectory_ = rootDirectory; + } + + const std::string GetRootDirectory() const + { + return rootDirectory_; + } + +#if ORTHANC_ENABLE_DCMTK == 1 + void SetDicomCache(boost::shared_ptr cache) + { + dicomCache_ = cache; + } +#endif + + void Run(boost::weak_ptr receiver, + IMessageEmitter& emitter, + const IOracleCommand& command); + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Oracle/GetOrthancImageCommand.cpp --- a/Framework/Oracle/GetOrthancImageCommand.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Oracle/GetOrthancImageCommand.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -29,20 +29,6 @@ namespace OrthancStone { - GetOrthancImageCommand::SuccessMessage::SuccessMessage(const GetOrthancImageCommand& command, - Orthanc::ImageAccessor* image, // Takes ownership - Orthanc::MimeType mime) : - OriginMessage(command), - image_(image), - mime_(mime) - { - if (image == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - } - - GetOrthancImageCommand::GetOrthancImageCommand() : uri_("/"), timeout_(600), @@ -58,35 +44,59 @@ } - void GetOrthancImageCommand::SetInstanceUri(const std::string& instance, - Orthanc::PixelFormat pixelFormat) + static std::string GetFormatSuffix(Orthanc::PixelFormat pixelFormat) { - uri_ = "/instances/" + instance; - switch (pixelFormat) { case Orthanc::PixelFormat_RGB24: - uri_ += "/preview"; - break; + return "preview"; case Orthanc::PixelFormat_Grayscale16: - uri_ += "/image-uint16"; - break; + return "image-uint16"; case Orthanc::PixelFormat_SignedGrayscale16: - uri_ += "/image-int16"; - break; + return "image-int16"; default: throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } } - void GetOrthancImageCommand::ProcessHttpAnswer(IMessageEmitter& emitter, - const IObserver& receiver, + + void GetOrthancImageCommand::SetInstanceUri(const std::string& instance, + Orthanc::PixelFormat pixelFormat) + { + uri_ = "/instances/" + instance + "/" + GetFormatSuffix(pixelFormat); + } + + + void GetOrthancImageCommand::SetFrameUri(const std::string& instance, + unsigned int frame, + Orthanc::PixelFormat pixelFormat) + { + uri_ = ("/instances/" + instance + "/frames/" + + boost::lexical_cast(frame) + "/" + GetFormatSuffix(pixelFormat)); + } + + + void GetOrthancImageCommand::ProcessHttpAnswer(boost::weak_ptr receiver, + IMessageEmitter& emitter, const std::string& answer, const HttpHeaders& answerHeaders) const { + for (HttpHeaders::const_iterator it = answerHeaders.begin(); it != answerHeaders.end(); ++it) + { + std::string key = Orthanc::Toolbox::StripSpaces(it->first); + Orthanc::Toolbox::ToLowerCase(key); + + if (key == "content-disposition" && + it->second == "filename=\"unsupported.png\"") + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat, + "Orthanc cannot decode this image"); + } + } + Orthanc::MimeType contentType = Orthanc::MimeType_Binary; for (HttpHeaders::const_iterator it = answerHeaders.begin(); @@ -147,7 +157,7 @@ } } - SuccessMessage message(*this, image.release(), contentType); + SuccessMessage message(*this, *image, contentType); emitter.EmitMessage(receiver, message); } } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Oracle/GetOrthancImageCommand.h --- a/Framework/Oracle/GetOrthancImageCommand.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Oracle/GetOrthancImageCommand.h Tue Feb 04 15:24:03 2020 +0100 @@ -22,7 +22,7 @@ #pragma once #include "../Messages/IMessageEmitter.h" -#include "OracleCommandWithPayload.h" +#include "OracleCommandBase.h" #include @@ -30,7 +30,7 @@ namespace OrthancStone { - class GetOrthancImageCommand : public OracleCommandWithPayload + class GetOrthancImageCommand : public OracleCommandBase { public: typedef std::map HttpHeaders; @@ -40,17 +40,22 @@ ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); private: - std::auto_ptr image_; - Orthanc::MimeType mime_; + const Orthanc::ImageAccessor& image_; + Orthanc::MimeType mime_; public: SuccessMessage(const GetOrthancImageCommand& command, - Orthanc::ImageAccessor* image, // Takes ownership - Orthanc::MimeType mime); + const Orthanc::ImageAccessor& image, + Orthanc::MimeType mime) : + OriginMessage(command), + image_(image), + mime_(mime) + { + } const Orthanc::ImageAccessor& GetImage() const { - return *image_; + return image_; } Orthanc::MimeType GetMimeType() const @@ -67,6 +72,15 @@ bool hasExpectedFormat_; Orthanc::PixelFormat expectedFormat_; + GetOrthancImageCommand(const GetOrthancImageCommand& other) : + uri_(other.uri_), + headers_(other.headers_), + timeout_(other.timeout_), + hasExpectedFormat_(other.hasExpectedFormat_), + expectedFormat_(other.expectedFormat_) + { + } + public: GetOrthancImageCommand(); @@ -75,6 +89,11 @@ return Type_GetOrthancImage; } + virtual IOracleCommand* Clone() const + { + return new GetOrthancImageCommand(*this); + } + void SetExpectedPixelFormat(Orthanc::PixelFormat format); void SetUri(const std::string& uri) @@ -85,6 +104,10 @@ void SetInstanceUri(const std::string& instance, Orthanc::PixelFormat pixelFormat); + void SetFrameUri(const std::string& instance, + unsigned int frame, + Orthanc::PixelFormat pixelFormat); + void SetHttpHeader(const std::string& key, const std::string& value) { @@ -111,8 +134,8 @@ return timeout_; } - void ProcessHttpAnswer(IMessageEmitter& emitter, - const IObserver& receiver, + void ProcessHttpAnswer(boost::weak_ptr receiver, + IMessageEmitter& emitter, const std::string& answer, const HttpHeaders& answerHeaders) const; }; diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Oracle/GetOrthancWebViewerJpegCommand.cpp --- a/Framework/Oracle/GetOrthancWebViewerJpegCommand.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Oracle/GetOrthancWebViewerJpegCommand.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -34,18 +34,6 @@ namespace OrthancStone { - GetOrthancWebViewerJpegCommand::SuccessMessage::SuccessMessage(const GetOrthancWebViewerJpegCommand& command, - Orthanc::ImageAccessor* image) : // Takes ownership - OriginMessage(command), - image_(image) - { - if (image == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - } - - GetOrthancWebViewerJpegCommand::GetOrthancWebViewerJpegCommand() : frame_(0), quality_(95), @@ -76,8 +64,8 @@ } - void GetOrthancWebViewerJpegCommand::ProcessHttpAnswer(IMessageEmitter& emitter, - const IObserver& receiver, + void GetOrthancWebViewerJpegCommand::ProcessHttpAnswer(boost::weak_ptr receiver, + IMessageEmitter& emitter, const std::string& answer) const { // This code comes from older "OrthancSlicesLoader::ParseSliceImageJpeg()" @@ -149,7 +137,7 @@ } else { - SuccessMessage message(*this, reader.release()); + SuccessMessage message(*this, *reader); emitter.EmitMessage(receiver, message); return; } @@ -168,7 +156,7 @@ } else { - SuccessMessage message(*this, reader.release()); + SuccessMessage message(*this, *reader); emitter.EmitMessage(receiver, message); return; } @@ -210,8 +198,8 @@ float offset = static_cast(stretchLow) / scaling; Orthanc::ImageProcessing::ShiftScale(*image, offset, scaling, true); } - - SuccessMessage message(*this, image.release()); + + SuccessMessage message(*this, *image); emitter.EmitMessage(receiver, message); } } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Oracle/GetOrthancWebViewerJpegCommand.h --- a/Framework/Oracle/GetOrthancWebViewerJpegCommand.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Oracle/GetOrthancWebViewerJpegCommand.h Tue Feb 04 15:24:03 2020 +0100 @@ -22,7 +22,7 @@ #pragma once #include "../Messages/IMessageEmitter.h" -#include "OracleCommandWithPayload.h" +#include "OracleCommandBase.h" #include @@ -30,7 +30,7 @@ namespace OrthancStone { - class GetOrthancWebViewerJpegCommand : public OracleCommandWithPayload + class GetOrthancWebViewerJpegCommand : public OracleCommandBase { public: typedef std::map HttpHeaders; @@ -40,15 +40,19 @@ ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); private: - std::auto_ptr image_; + const Orthanc::ImageAccessor& image_; public: SuccessMessage(const GetOrthancWebViewerJpegCommand& command, - Orthanc::ImageAccessor* image); // Takes ownership + const Orthanc::ImageAccessor& image) : + OriginMessage(command), + image_(image) + { + } const Orthanc::ImageAccessor& GetImage() const { - return *image_; + return image_; } }; @@ -60,6 +64,16 @@ unsigned int timeout_; Orthanc::PixelFormat expectedFormat_; + GetOrthancWebViewerJpegCommand(const GetOrthancWebViewerJpegCommand& other) : + instanceId_(other.instanceId_), + frame_(other.frame_), + quality_(other.quality_), + headers_(other.headers_), + timeout_(other.timeout_), + expectedFormat_(other.expectedFormat_) + { + } + public: GetOrthancWebViewerJpegCommand(); @@ -68,6 +82,11 @@ return Type_GetOrthancWebViewerJpeg; } + virtual IOracleCommand* Clone() const + { + return new GetOrthancWebViewerJpegCommand(*this); + } + void SetExpectedPixelFormat(Orthanc::PixelFormat format) { expectedFormat_ = format; @@ -128,8 +147,8 @@ std::string GetUri() const; - void ProcessHttpAnswer(IMessageEmitter& emitter, - const IObserver& receiver, + void ProcessHttpAnswer(boost::weak_ptr receiver, + IMessageEmitter& emitter, const std::string& answer) const; }; } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Oracle/HttpCommand.cpp --- a/Framework/Oracle/HttpCommand.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Oracle/HttpCommand.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -28,16 +28,6 @@ namespace OrthancStone { - HttpCommand::SuccessMessage::SuccessMessage(const HttpCommand& command, - const HttpHeaders& answerHeaders, - std::string& answer) : - OriginMessage(command), - headers_(answerHeaders), - answer_(answer) - { - } - - void HttpCommand::SuccessMessage::ParseJsonBody(Json::Value& target) const { Json::Reader reader; @@ -76,4 +66,30 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } } + + + const std::string& HttpCommand::GetUsername() const + { + if (HasCredentials()) + { + return username_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + const std::string& HttpCommand::GetPassword() const + { + if (HasCredentials()) + { + return password_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Oracle/HttpCommand.h --- a/Framework/Oracle/HttpCommand.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Oracle/HttpCommand.h Tue Feb 04 15:24:03 2020 +0100 @@ -22,7 +22,7 @@ #pragma once #include "../Messages/IMessage.h" -#include "OracleCommandWithPayload.h" +#include "OracleCommandBase.h" #include @@ -31,7 +31,7 @@ namespace OrthancStone { - class HttpCommand : public OracleCommandWithPayload + class HttpCommand : public OracleCommandBase { public: typedef std::map HttpHeaders; @@ -41,13 +41,18 @@ ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); private: - HttpHeaders headers_; - std::string answer_; + const HttpHeaders& headers_; + const std::string& answer_; public: SuccessMessage(const HttpCommand& command, const HttpHeaders& answerHeaders, - std::string& answer /* will be swapped to avoid a memcpy() */); + const std::string& answer) : + OriginMessage(command), + headers_(answerHeaders), + answer_(answer) + { + } const std::string& GetAnswer() const { @@ -56,7 +61,7 @@ void ParseJsonBody(Json::Value& target) const; - const HttpHeaders& GetAnswerHeaders() const + const HttpHeaders& GetAnswerHeaders() const { return headers_; } @@ -69,6 +74,19 @@ std::string body_; HttpHeaders headers_; unsigned int timeout_; + std::string username_; + std::string password_; + + HttpCommand(const HttpCommand& other) : + method_(other.method_), + url_(other.url_), + body_(other.body_), + headers_(other.headers_), + timeout_(other.timeout_), + username_(other.username_), + password_(other.password_) + { + } public: HttpCommand(); @@ -78,6 +96,11 @@ return Type_Http; } + virtual IOracleCommand* Clone() const + { + return new HttpCommand(*this); + } + void SetMethod(Orthanc::HttpMethod method) { method_ = method; @@ -137,5 +160,27 @@ { return timeout_; } + + void SetCredentials(const std::string& username, + const std::string& password) + { + username_ = username; + password_ = password; + } + + void ClearCredentials() + { + username_.clear(); + password_.clear(); + } + + bool HasCredentials() const + { + return !username_.empty(); + } + + const std::string& GetUsername() const; + + const std::string& GetPassword() const; }; } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Oracle/IOracle.h --- a/Framework/Oracle/IOracle.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Oracle/IOracle.h Tue Feb 04 15:24:03 2020 +0100 @@ -24,6 +24,8 @@ #include "../Messages/IObserver.h" #include "IOracleCommand.h" +#include + namespace OrthancStone { class IOracle : public boost::noncopyable @@ -33,7 +35,12 @@ { } - virtual void Schedule(const IObserver& receiver, + /** + * Returns "true" iff the command has actually been queued. If + * "false" is returned, the command has been freed, and it won't + * be processed (this is the case if the oracle is stopped). + **/ + virtual bool Schedule(boost::shared_ptr receiver, IOracleCommand* command) = 0; // Takes ownership }; } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Oracle/IOracleCommand.h --- a/Framework/Oracle/IOracleCommand.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Oracle/IOracleCommand.h Tue Feb 04 15:24:03 2020 +0100 @@ -21,7 +21,7 @@ #pragma once -#include +#include namespace OrthancStone { @@ -30,11 +30,14 @@ public: enum Type { + Type_GetOrthancImage, + Type_GetOrthancWebViewerJpeg, Type_Http, - Type_Sleep, Type_OrthancRestApi, - Type_GetOrthancImage, - Type_GetOrthancWebViewerJpeg + Type_ParseDicomFromFile, + Type_ParseDicomFromWado, + Type_ReadFile, + Type_Sleep }; virtual ~IOracleCommand() @@ -42,5 +45,8 @@ } virtual Type GetType() const = 0; + + // This only clones the command, *not* its possibly associated payload + virtual IOracleCommand* Clone() const = 0; }; } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Oracle/OracleCommandBase.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Oracle/OracleCommandBase.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,67 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "OracleCommandBase.h" + +#include + +namespace OrthancStone +{ + void OracleCommandBase::AcquirePayload(Orthanc::IDynamicObject* payload) + { + if (payload == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + else + { + payload_.reset(payload); + } + } + + + Orthanc::IDynamicObject& OracleCommandBase::GetPayload() const + { + if (HasPayload()) + { + return *payload_; + } + else + { + LOG(ERROR) << "OracleCommandBase::GetPayload(): (!HasPayload())"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + Orthanc::IDynamicObject* OracleCommandBase::ReleasePayload() + { + if (HasPayload()) + { + return payload_.release(); + } + else + { + LOG(ERROR) << "OracleCommandBase::ReleasePayload(): (!HasPayload())"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Oracle/OracleCommandBase.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Oracle/OracleCommandBase.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,47 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IOracleCommand.h" + +#include + +namespace OrthancStone +{ + class OracleCommandBase : public IOracleCommand + { + private: + std::auto_ptr payload_; + + public: + void AcquirePayload(Orthanc::IDynamicObject* payload); + + virtual bool HasPayload() const + { + return (payload_.get() != NULL); + } + + virtual Orthanc::IDynamicObject& GetPayload() const; + + Orthanc::IDynamicObject* ReleasePayload(); + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Oracle/OracleCommandExceptionMessage.h --- a/Framework/Oracle/OracleCommandExceptionMessage.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Oracle/OracleCommandExceptionMessage.h Tue Feb 04 15:24:03 2020 +0100 @@ -28,34 +28,28 @@ namespace OrthancStone { - class OracleCommandExceptionMessage : public IMessage + class OracleCommandExceptionMessage : public OriginMessage { ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); private: - const IOracleCommand& command_; - Orthanc::OrthancException exception_; + Orthanc::OrthancException exception_; public: OracleCommandExceptionMessage(const IOracleCommand& command, - const Orthanc::OrthancException& exception) : - command_(command), - exception_(exception) + const Orthanc::ErrorCode& error) : + OriginMessage(command), + exception_(error) { } OracleCommandExceptionMessage(const IOracleCommand& command, - const Orthanc::ErrorCode& error) : - command_(command), - exception_(error) + const Orthanc::OrthancException& exception) : + OriginMessage(command), + exception_(exception) { } - const IOracleCommand& GetCommand() const - { - return command_; - } - const Orthanc::OrthancException& GetException() const { return exception_; diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Oracle/OracleCommandWithPayload.cpp --- a/Framework/Oracle/OracleCommandWithPayload.cpp Tue Feb 04 15:23:33 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,67 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - - -#include "OracleCommandWithPayload.h" - -#include - -namespace OrthancStone -{ - void OracleCommandWithPayload::SetPayload(Orthanc::IDynamicObject* payload) - { - if (payload == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - else - { - payload_.reset(payload); - } - } - - - Orthanc::IDynamicObject& OracleCommandWithPayload::GetPayload() const - { - if (HasPayload()) - { - return *payload_; - } - else - { - LOG(ERROR) << "OracleCommandWithPayload::GetPayload(): (!HasPayload())"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - } - - - Orthanc::IDynamicObject* OracleCommandWithPayload::ReleasePayload() - { - if (HasPayload()) - { - return payload_.release(); - } - else - { - LOG(ERROR) << "OracleCommandWithPayload::ReleasePayload(): (!HasPayload())"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - } -} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Oracle/OracleCommandWithPayload.h --- a/Framework/Oracle/OracleCommandWithPayload.h Tue Feb 04 15:23:33 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,49 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "IOracleCommand.h" - -#include - -#include - -namespace OrthancStone -{ - class OracleCommandWithPayload : public IOracleCommand - { - private: - std::auto_ptr payload_; - - public: - void SetPayload(Orthanc::IDynamicObject* payload); - - bool HasPayload() const - { - return (payload_.get() != NULL); - } - - Orthanc::IDynamicObject& GetPayload() const; - - Orthanc::IDynamicObject* ReleasePayload(); - }; -} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Oracle/OrthancRestApiCommand.cpp --- a/Framework/Oracle/OrthancRestApiCommand.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Oracle/OrthancRestApiCommand.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -28,16 +28,6 @@ namespace OrthancStone { - OrthancRestApiCommand::SuccessMessage::SuccessMessage(const OrthancRestApiCommand& command, - const HttpHeaders& answerHeaders, - std::string& answer) : - OriginMessage(command), - headers_(answerHeaders), - answer_(answer) - { - } - - void OrthancRestApiCommand::SuccessMessage::ParseJsonBody(Json::Value& target) const { Json::Reader reader; @@ -51,7 +41,8 @@ OrthancRestApiCommand::OrthancRestApiCommand() : method_(Orthanc::HttpMethod_Get), uri_("/"), - timeout_(600) + timeout_(600), + applyPlugins_(false) { } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Oracle/OrthancRestApiCommand.h --- a/Framework/Oracle/OrthancRestApiCommand.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Oracle/OrthancRestApiCommand.h Tue Feb 04 15:24:03 2020 +0100 @@ -22,7 +22,7 @@ #pragma once #include "../Messages/IMessage.h" -#include "OracleCommandWithPayload.h" +#include "OracleCommandBase.h" #include @@ -31,7 +31,7 @@ namespace OrthancStone { - class OrthancRestApiCommand : public OracleCommandWithPayload + class OrthancRestApiCommand : public OracleCommandBase { public: typedef std::map HttpHeaders; @@ -41,14 +41,19 @@ ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); private: - HttpHeaders headers_; - std::string answer_; + const HttpHeaders& headers_; + const std::string& answer_; public: SuccessMessage(const OrthancRestApiCommand& command, const HttpHeaders& answerHeaders, - std::string& answer /* will be swapped to avoid a memcpy() */); - + const std::string& answer) : + OriginMessage(command), + headers_(answerHeaders), + answer_(answer) + { + } + const std::string& GetAnswer() const { return answer_; @@ -69,7 +74,18 @@ std::string body_; HttpHeaders headers_; unsigned int timeout_; + bool applyPlugins_; // Only makes sense for Stone as an Orthanc plugin + OrthancRestApiCommand(const OrthancRestApiCommand& other) : + method_(other.method_), + uri_(other.uri_), + body_(other.body_), + headers_(other.headers_), + timeout_(other.timeout_), + applyPlugins_(other.applyPlugins_) + { + } + public: OrthancRestApiCommand(); @@ -78,6 +94,11 @@ return Type_OrthancRestApi; } + virtual IOracleCommand* Clone() const + { + return new OrthancRestApiCommand(*this); + } + void SetMethod(Orthanc::HttpMethod method) { method_ = method; @@ -137,5 +158,15 @@ { return timeout_; } + + void SetApplyPlugins(bool applyPlugins) + { + applyPlugins_ = applyPlugins; + } + + bool IsApplyPlugins() const + { + return applyPlugins_; + } }; } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Oracle/ParseDicomFromFileCommand.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Oracle/ParseDicomFromFileCommand.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,43 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "ParseDicomFromFileCommand.h" + +#include + +#include + +namespace OrthancStone +{ + std::string ParseDicomFromFileCommand::GetDicomDirPath(const std::string& dicomDirPath, + const std::string& file) + { + std::string tmp = file; + +#if !defined(_WIN32) + std::replace(tmp.begin(), tmp.end(), '\\', '/'); +#endif + + boost::filesystem::path base = boost::filesystem::path(dicomDirPath).parent_path(); + + return (base / tmp).string(); + } +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Oracle/ParseDicomFromFileCommand.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Oracle/ParseDicomFromFileCommand.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,84 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "OracleCommandBase.h" + +#include + +namespace OrthancStone +{ + class ParseDicomFromFileCommand : public OracleCommandBase + { + private: + std::string path_; + bool pixelDataIncluded_; + + ParseDicomFromFileCommand(const ParseDicomFromFileCommand& other) : + path_(other.path_), + pixelDataIncluded_(other.pixelDataIncluded_) + { + } + + public: + ParseDicomFromFileCommand(const std::string& path) : + path_(path), + pixelDataIncluded_(true) + { + } + + ParseDicomFromFileCommand(const std::string& dicomDirPath, + const std::string& file) : + path_(GetDicomDirPath(dicomDirPath, file)), + pixelDataIncluded_(true) + { + } + + static std::string GetDicomDirPath(const std::string& dicomDirPath, + const std::string& file); + + virtual Type GetType() const + { + return Type_ParseDicomFromFile; + } + + virtual IOracleCommand* Clone() const + { + return new ParseDicomFromFileCommand(*this); + } + + const std::string& GetPath() const + { + return path_; + } + + bool IsPixelDataIncluded() const + { + return pixelDataIncluded_; + } + + void SetPixelDataIncluded(bool included) + { + pixelDataIncluded_ = included; + } + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Oracle/ParseDicomFromWadoCommand.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Oracle/ParseDicomFromWadoCommand.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,58 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "ParseDicomFromWadoCommand.h" + +#include + +namespace OrthancStone +{ + ParseDicomFromWadoCommand::ParseDicomFromWadoCommand(const std::string& sopInstanceUid, + IOracleCommand* restCommand) : + sopInstanceUid_(sopInstanceUid), + restCommand_(restCommand) + { + if (restCommand == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + if (restCommand_->GetType() != Type_Http && + restCommand_->GetType() != Type_OrthancRestApi) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType); + } + } + + + IOracleCommand* ParseDicomFromWadoCommand::Clone() const + { + assert(restCommand_.get() != NULL); + return new ParseDicomFromWadoCommand(sopInstanceUid_, restCommand_->Clone()); + } + + + const IOracleCommand& ParseDicomFromWadoCommand::GetRestCommand() const + { + assert(restCommand_.get() != NULL); + return *restCommand_; + } +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Oracle/ParseDicomFromWadoCommand.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Oracle/ParseDicomFromWadoCommand.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,54 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "OracleCommandBase.h" + +#include + +namespace OrthancStone +{ + class ParseDicomFromWadoCommand : public OracleCommandBase + { + private: + std::string sopInstanceUid_; + std::auto_ptr restCommand_; + + public: + ParseDicomFromWadoCommand(const std::string& sopInstanceUid, + IOracleCommand* restCommand); + + virtual Type GetType() const + { + return Type_ParseDicomFromWado; + } + + virtual IOracleCommand* Clone() const; + + const std::string& GetSopInstanceUid() const + { + return sopInstanceUid_; + } + + const IOracleCommand& GetRestCommand() const; + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Oracle/ParseDicomSuccessMessage.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Oracle/ParseDicomSuccessMessage.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,107 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "ParseDicomSuccessMessage.h" + +#include +#include +#include + +namespace OrthancStone +{ + class MultipartHandler : public Orthanc::MultipartStreamReader::IHandler + { + private: + std::auto_ptr dicom_; + size_t size_; + + public: + MultipartHandler() : + size_(0) + { + } + + virtual void HandlePart(const std::map& headers, + const void* part, + size_t size) + { + if (dicom_.get()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol, + "Multiple DICOM instances were contained in a WADO-RS request"); + } + else + { + dicom_.reset(new Orthanc::ParsedDicomFile(part, size)); + size_ = size; + } + } + + Orthanc::ParsedDicomFile* ReleaseDicom() + { + if (dicom_.get()) + { + return dicom_.release(); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol, + "WADO-RS request didn't contain any DICOM instance"); + } + } + + size_t GetSize() const + { + return size_; + } + }; + + + Orthanc::ParsedDicomFile* ParseDicomSuccessMessage::ParseWadoAnswer( + size_t& fileSize /* OUT */, + const std::string& answer, + const std::map& headers) + { + std::string contentType, subType, boundary, header; + if (Orthanc::MultipartStreamReader::GetMainContentType(header, headers) && + Orthanc::MultipartStreamReader::ParseMultipartContentType(contentType, subType, boundary, header) && + contentType == "multipart/related" && + subType == "application/dicom") + { + MultipartHandler handler; + + { + Orthanc::MultipartStreamReader reader(boundary); + reader.SetHandler(handler); + reader.AddChunk(answer); + reader.CloseStream(); + } + + fileSize = handler.GetSize(); + return handler.ReleaseDicom(); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol, + "Multipart/related answer of application/dicom was expected from DICOMweb server"); + } + } +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Oracle/ParseDicomSuccessMessage.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Oracle/ParseDicomSuccessMessage.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,85 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#if !defined(ORTHANC_ENABLE_DCMTK) +# error The macro ORTHANC_ENABLE_DCMTK must be defined +#endif + +#if ORTHANC_ENABLE_DCMTK != 1 +# error Support for DCMTK must be enabled to use ParseDicomFromFileCommand +#endif + +#include "OracleCommandBase.h" +#include "../Messages/IMessageEmitter.h" +#include "../Messages/IObserver.h" + +#include + +namespace Orthanc +{ + class ParsedDicomFile; +} + +namespace OrthancStone +{ + class ParseDicomSuccessMessage : public OriginMessage + { + ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); + + private: + Orthanc::ParsedDicomFile& dicom_; + size_t fileSize_; + bool hasPixelData_; + + public: + ParseDicomSuccessMessage(const OracleCommandBase& command, + Orthanc::ParsedDicomFile& dicom, + size_t fileSize, + bool hasPixelData) : + OriginMessage(command), + dicom_(dicom), + fileSize_(fileSize), + hasPixelData_(hasPixelData) + { + } + + Orthanc::ParsedDicomFile& GetDicom() const + { + return dicom_; + } + + size_t GetFileSize() const + { + return fileSize_; + } + + bool HasPixelData() const + { + return hasPixelData_; + } + + static Orthanc::ParsedDicomFile* ParseWadoAnswer(size_t& fileSize /* OUT */, + const std::string& answer, + const std::map& headers); + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Oracle/ReadFileCommand.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Oracle/ReadFileCommand.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,78 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../Messages/IMessage.h" +#include "OracleCommandBase.h" + +namespace OrthancStone +{ + class ReadFileCommand : public OracleCommandBase + { + public: + class SuccessMessage : public OriginMessage + { + ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); + + private: + const std::string& content_; + + public: + SuccessMessage(const ReadFileCommand& command, + const std::string& content) : + OriginMessage(command), + content_(content) + { + } + + const std::string& GetContent() const + { + return content_; + } + }; + + + private: + std::string path_; + + public: + ReadFileCommand(const std::string& path) : + path_(path) + { + } + + virtual Type GetType() const + { + return Type_ReadFile; + } + + virtual IOracleCommand* Clone() const + { + return new ReadFileCommand(path_); + } + + const std::string& GetPath() const + { + return path_; + } + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Oracle/SleepOracleCommand.h --- a/Framework/Oracle/SleepOracleCommand.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Oracle/SleepOracleCommand.h Tue Feb 04 15:24:03 2020 +0100 @@ -22,11 +22,11 @@ #pragma once #include "../Messages/IMessage.h" -#include "OracleCommandWithPayload.h" +#include "OracleCommandBase.h" namespace OrthancStone { - class SleepOracleCommand : public OracleCommandWithPayload + class SleepOracleCommand : public OracleCommandBase { private: unsigned int milliseconds_; @@ -44,6 +44,11 @@ return Type_Sleep; } + virtual IOracleCommand* Clone() const + { + return new SleepOracleCommand(milliseconds_); + } + unsigned int GetDelay() const { return milliseconds_; diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Oracle/ThreadedOracle.cpp --- a/Framework/Oracle/ThreadedOracle.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Oracle/ThreadedOracle.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -21,29 +21,21 @@ #include "ThreadedOracle.h" -#include "GetOrthancImageCommand.h" -#include "GetOrthancWebViewerJpegCommand.h" -#include "HttpCommand.h" -#include "OrthancRestApiCommand.h" #include "SleepOracleCommand.h" -#include "OracleCommandExceptionMessage.h" -#include -#include +#include #include -#include - namespace OrthancStone { class ThreadedOracle::Item : public Orthanc::IDynamicObject { private: - const IObserver& receiver_; + boost::weak_ptr receiver_; std::auto_ptr command_; public: - Item(const IObserver& receiver, + Item(boost::weak_ptr receiver, IOracleCommand* command) : receiver_(receiver), command_(command) @@ -54,7 +46,7 @@ } } - const IObserver& GetReceiver() const + boost::weak_ptr GetReceiver() { return receiver_; } @@ -73,12 +65,12 @@ class Item { private: - const IObserver& receiver_; + boost::weak_ptr receiver_; std::auto_ptr command_; boost::posix_time::ptime expiration_; public: - Item(const IObserver& receiver, + Item(boost::weak_ptr receiver, SleepOracleCommand* command) : receiver_(receiver), command_(command) @@ -123,7 +115,7 @@ } } - void Add(const IObserver& receiver, + void Add(boost::weak_ptr receiver, SleepOracleCommand* command) // Takes ownership { boost::mutex::scoped_lock lock(mutex_); @@ -160,154 +152,6 @@ }; - static void CopyHttpHeaders(Orthanc::HttpClient& client, - const Orthanc::HttpClient::HttpHeaders& headers) - { - for (Orthanc::HttpClient::HttpHeaders::const_iterator - it = headers.begin(); it != headers.end(); it++ ) - { - client.AddHeader(it->first, it->second); - } - } - - - static void DecodeAnswer(std::string& answer, - const Orthanc::HttpClient::HttpHeaders& headers) - { - Orthanc::HttpCompression contentEncoding = Orthanc::HttpCompression_None; - - for (Orthanc::HttpClient::HttpHeaders::const_iterator it = headers.begin(); - it != headers.end(); ++it) - { - std::string s; - Orthanc::Toolbox::ToLowerCase(s, it->first); - - if (s == "content-encoding") - { - if (it->second == "gzip") - { - contentEncoding = Orthanc::HttpCompression_Gzip; - } - else - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol, - "Unsupported HTTP Content-Encoding: " + it->second); - } - - break; - } - } - - if (contentEncoding == Orthanc::HttpCompression_Gzip) - { - std::string compressed; - answer.swap(compressed); - - Orthanc::GzipCompressor compressor; - compressor.Uncompress(answer, compressed.c_str(), compressed.size()); - - LOG(INFO) << "Uncompressing gzip Encoding: from " << compressed.size() - << " to " << answer.size() << " bytes"; - } - } - - - static void Execute(IMessageEmitter& emitter, - const IObserver& receiver, - const HttpCommand& command) - { - Orthanc::HttpClient client; - client.SetUrl(command.GetUrl()); - client.SetMethod(command.GetMethod()); - client.SetTimeout(command.GetTimeout()); - - CopyHttpHeaders(client, command.GetHttpHeaders()); - - if (command.GetMethod() == Orthanc::HttpMethod_Post || - command.GetMethod() == Orthanc::HttpMethod_Put) - { - client.SetBody(command.GetBody()); - } - - std::string answer; - Orthanc::HttpClient::HttpHeaders answerHeaders; - client.ApplyAndThrowException(answer, answerHeaders); - - DecodeAnswer(answer, answerHeaders); - - HttpCommand::SuccessMessage message(command, answerHeaders, answer); - emitter.EmitMessage(receiver, message); - } - - - static void Execute(IMessageEmitter& emitter, - const Orthanc::WebServiceParameters& orthanc, - const IObserver& receiver, - const OrthancRestApiCommand& command) - { - Orthanc::HttpClient client(orthanc, command.GetUri()); - client.SetMethod(command.GetMethod()); - client.SetTimeout(command.GetTimeout()); - - CopyHttpHeaders(client, command.GetHttpHeaders()); - - if (command.GetMethod() == Orthanc::HttpMethod_Post || - command.GetMethod() == Orthanc::HttpMethod_Put) - { - client.SetBody(command.GetBody()); - } - - std::string answer; - Orthanc::HttpClient::HttpHeaders answerHeaders; - client.ApplyAndThrowException(answer, answerHeaders); - - DecodeAnswer(answer, answerHeaders); - - OrthancRestApiCommand::SuccessMessage message(command, answerHeaders, answer); - emitter.EmitMessage(receiver, message); - } - - - static void Execute(IMessageEmitter& emitter, - const Orthanc::WebServiceParameters& orthanc, - const IObserver& receiver, - const GetOrthancImageCommand& command) - { - Orthanc::HttpClient client(orthanc, command.GetUri()); - client.SetTimeout(command.GetTimeout()); - - CopyHttpHeaders(client, command.GetHttpHeaders()); - - std::string answer; - Orthanc::HttpClient::HttpHeaders answerHeaders; - client.ApplyAndThrowException(answer, answerHeaders); - - DecodeAnswer(answer, answerHeaders); - - command.ProcessHttpAnswer(emitter, receiver, answer, answerHeaders); - } - - - static void Execute(IMessageEmitter& emitter, - const Orthanc::WebServiceParameters& orthanc, - const IObserver& receiver, - const GetOrthancWebViewerJpegCommand& command) - { - Orthanc::HttpClient client(orthanc, command.GetUri()); - client.SetTimeout(command.GetTimeout()); - - CopyHttpHeaders(client, command.GetHttpHeaders()); - - std::string answer; - Orthanc::HttpClient::HttpHeaders answerHeaders; - client.ApplyAndThrowException(answer, answerHeaders); - - DecodeAnswer(answer, answerHeaders); - - command.ProcessHttpAnswer(emitter, receiver, answer); - } - - void ThreadedOracle::Step() { std::auto_ptr object(queue_.Dequeue(100)); @@ -316,60 +160,37 @@ { Item& item = dynamic_cast(*object); - try + if (item.GetCommand().GetType() == IOracleCommand::Type_Sleep) { - switch (item.GetCommand().GetType()) + SleepOracleCommand& command = dynamic_cast(item.GetCommand()); + + std::auto_ptr copy(new SleepOracleCommand(command.GetDelay())); + + if (command.HasPayload()) { - case IOracleCommand::Type_Sleep: - { - SleepOracleCommand& command = dynamic_cast(item.GetCommand()); - - std::auto_ptr copy(new SleepOracleCommand(command.GetDelay())); - - if (command.HasPayload()) - { - copy->SetPayload(command.ReleasePayload()); - } - - sleepingCommands_->Add(item.GetReceiver(), copy.release()); - - break; - } - - case IOracleCommand::Type_Http: - Execute(emitter_, item.GetReceiver(), - dynamic_cast(item.GetCommand())); - break; + copy->AcquirePayload(command.ReleasePayload()); + } + + sleepingCommands_->Add(item.GetReceiver(), copy.release()); + } + else + { + GenericOracleRunner runner; - case IOracleCommand::Type_OrthancRestApi: - Execute(emitter_, orthanc_, item.GetReceiver(), - dynamic_cast(item.GetCommand())); - break; - - case IOracleCommand::Type_GetOrthancImage: - Execute(emitter_, orthanc_, item.GetReceiver(), - dynamic_cast(item.GetCommand())); - break; - - case IOracleCommand::Type_GetOrthancWebViewerJpeg: - Execute(emitter_, orthanc_, item.GetReceiver(), - dynamic_cast(item.GetCommand())); - break; + { + boost::mutex::scoped_lock lock(mutex_); + runner.SetOrthanc(orthanc_); + runner.SetRootDirectory(rootDirectory_); - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); +#if ORTHANC_ENABLE_DCMTK == 1 + if (dicomCache_) + { + runner.SetDicomCache(dicomCache_); + } +#endif } - } - catch (Orthanc::OrthancException& e) - { - LOG(ERROR) << "Exception within the oracle: " << e.What(); - emitter_.EmitMessage(item.GetReceiver(), OracleCommandExceptionMessage(item.GetCommand(), e)); - } - catch (...) - { - LOG(ERROR) << "Threaded exception within the oracle"; - emitter_.EmitMessage(item.GetReceiver(), OracleCommandExceptionMessage - (item.GetCommand(), Orthanc::ErrorCode_InternalError)); + + runner.Run(item.GetReceiver(), emitter_, item.GetCommand()); } } } @@ -453,6 +274,7 @@ ThreadedOracle::ThreadedOracle(IMessageEmitter& emitter) : emitter_(emitter), + rootDirectory_("."), state_(State_Setup), workers_(4), sleepingCommands_(new SleepingCommands), @@ -480,23 +302,21 @@ catch (...) { LOG(ERROR) << "Native exception while stopping the threaded oracle"; - } + } } void ThreadedOracle::SetOrthancParameters(const Orthanc::WebServiceParameters& orthanc) { boost::mutex::scoped_lock lock(mutex_); + orthanc_ = orthanc; + } - if (state_ != State_Setup) - { - LOG(ERROR) << "ThreadedOracle::SetOrthancParameters(): (state_ != State_Setup)"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - else - { - orthanc_ = orthanc; - } + + void ThreadedOracle::SetRootDirectory(const std::string& rootDirectory) + { + boost::mutex::scoped_lock lock(mutex_); + rootDirectory_ = rootDirectory; } @@ -540,6 +360,31 @@ } + void ThreadedOracle::SetDicomCacheSize(size_t size) + { +#if ORTHANC_ENABLE_DCMTK == 1 + boost::mutex::scoped_lock lock(mutex_); + + if (state_ != State_Setup) + { + LOG(ERROR) << "ThreadedOracle::SetDicomCacheSize(): (state_ != State_Setup)"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + if (size == 0) + { + dicomCache_.reset(); + } + else + { + dicomCache_.reset(new ParsedDicomCache(size)); + } + } +#endif + } + + void ThreadedOracle::Start() { boost::mutex::scoped_lock lock(mutex_); @@ -551,6 +396,7 @@ } else { + LOG(WARNING) << "Starting oracle with " << workers_.size() << " worker threads"; state_ = State_Running; for (unsigned int i = 0; i < workers_.size(); i++) @@ -563,9 +409,25 @@ } - void ThreadedOracle::Schedule(const IObserver& receiver, + bool ThreadedOracle::Schedule(boost::shared_ptr receiver, IOracleCommand* command) { - queue_.Enqueue(new Item(receiver, command)); + std::auto_ptr item(new Item(receiver, command)); + + { + boost::mutex::scoped_lock lock(mutex_); + + if (state_ == State_Running) + { + //LOG(INFO) << "New oracle command queued"; + queue_.Enqueue(item.release()); + return true; + } + else + { + LOG(TRACE) << "Command not enqueued, as the oracle has stopped"; + return false; + } + } } } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Oracle/ThreadedOracle.h --- a/Framework/Oracle/ThreadedOracle.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Oracle/ThreadedOracle.h Tue Feb 04 15:24:03 2020 +0100 @@ -25,14 +25,22 @@ # error The macro ORTHANC_ENABLE_THREADS must be defined #endif +#if !defined(ORTHANC_ENABLE_DCMTK) +# error The macro ORTHANC_ENABLE_DCMTK must be defined +#endif + #if ORTHANC_ENABLE_THREADS != 1 # error This file can only compiled for native targets #endif -#include "../Messages/IMessageEmitter.h" +#if ORTHANC_ENABLE_DCMTK == 1 +# include "../Toolbox/ParsedDicomCache.h" +#endif + #include "IOracle.h" +#include "GenericOracleRunner.h" +#include "../Messages/IMessageEmitter.h" -#include #include @@ -53,6 +61,7 @@ IMessageEmitter& emitter_; Orthanc::WebServiceParameters orthanc_; + std::string rootDirectory_; Orthanc::SharedMessageQueue queue_; State state_; boost::mutex mutex_; @@ -61,6 +70,10 @@ boost::thread sleepingWorker_; unsigned int sleepingTimeResolution_; +#if ORTHANC_ENABLE_DCMTK == 1 + boost::shared_ptr dicomCache_; +#endif + void Step(); static void Worker(ThreadedOracle* that); @@ -74,13 +87,16 @@ virtual ~ThreadedOracle(); - // The reference is not stored. void SetOrthancParameters(const Orthanc::WebServiceParameters& orthanc); + void SetRootDirectory(const std::string& rootDirectory); + void SetThreadsCount(unsigned int count); void SetSleepingTimeResolution(unsigned int milliseconds); + void SetDicomCacheSize(size_t size); + void Start(); void Stop() @@ -88,7 +104,7 @@ StopInternal(); } - virtual void Schedule(const IObserver& receiver, - IOracleCommand* command); + virtual bool Schedule(boost::shared_ptr receiver, + IOracleCommand* command) ORTHANC_OVERRIDE; }; } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Oracle/WebAssemblyOracle.cpp --- a/Framework/Oracle/WebAssemblyOracle.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Oracle/WebAssemblyOracle.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -21,8 +21,13 @@ #include "WebAssemblyOracle.h" +#include "OracleCommandExceptionMessage.h" #include "SleepOracleCommand.h" -#include "OracleCommandExceptionMessage.h" + +#if ORTHANC_ENABLE_DCMTK == 1 +# include "ParseDicomSuccessMessage.h" +static unsigned int BUCKET_SOP = 1; +#endif #include #include @@ -31,23 +36,18 @@ #include #include -#if 0 -extern bool logbgo233; -extern bool logbgo115; -#endif - namespace OrthancStone { class WebAssemblyOracle::TimeoutContext { private: WebAssemblyOracle& oracle_; - const IObserver& receiver_; + boost::weak_ptr receiver_; std::auto_ptr command_; public: TimeoutContext(WebAssemblyOracle& oracle, - const IObserver& receiver, + boost::weak_ptr receiver, IOracleCommand* command) : oracle_(oracle), receiver_(receiver) @@ -63,7 +63,9 @@ } void EmitMessage() - { + { + assert(command_.get() != NULL); + SleepOracleCommand::TimeoutMessage message(*command_); oracle_.EmitMessage(receiver_, message); } @@ -76,26 +78,6 @@ }; - class WebAssemblyOracle::Emitter : public IMessageEmitter - { - private: - WebAssemblyOracle& oracle_; - - public: - Emitter(WebAssemblyOracle& oracle) : - oracle_(oracle) - { - } - - virtual void EmitMessage(const IObserver& receiver, - const IMessage& message) - { - LOG(TRACE) << "WebAssemblyOracle::Emitter::EmitMessage receiver = " - << std::hex << &receiver << std::dec; - oracle_.EmitMessage(receiver, message); - } - }; - /** This object is created on the heap for every http request. It is deleted in the success (or error) callbacks. @@ -108,26 +90,23 @@ class WebAssemblyOracle::FetchContext : public boost::noncopyable { private: - Emitter emitter_; - const IObserver& receiver_; - std::auto_ptr command_; - std::string expectedContentType_; - int64_t receiverFingerprint_; + WebAssemblyOracle& oracle_; + boost::weak_ptr receiver_; + std::auto_ptr command_; + std::string expectedContentType_; public: FetchContext(WebAssemblyOracle& oracle, - const IObserver& receiver, + boost::weak_ptr receiver, IOracleCommand* command, const std::string& expectedContentType) : - emitter_(oracle), + oracle_(oracle), receiver_(receiver), command_(command), - expectedContentType_(expectedContentType), - receiverFingerprint_(receiver.GetFingerprint()) + expectedContentType_(expectedContentType) { LOG(TRACE) << "WebAssemblyOracle::FetchContext::FetchContext() | " - << "receiver address = " << std::hex << &receiver << std::dec - << " with fingerprint = " << receiverFingerprint_; + << "receiver address = " << std::hex << &receiver; if (command == NULL) { @@ -140,21 +119,21 @@ return expectedContentType_; } + IMessageEmitter& GetEmitter() const + { + return oracle_; + } + + boost::weak_ptr GetReceiver() const + { + return receiver_; + } + void EmitMessage(const IMessage& message) { LOG(TRACE) << "WebAssemblyOracle::FetchContext::EmitMessage receiver_ = " << std::hex << &receiver_ << std::dec; - emitter_.EmitMessage(receiver_, message); - } - - IMessageEmitter& GetEmitter() - { - return emitter_; - } - - const IObserver& GetReceiver() const - { - return receiver_; + oracle_.EmitMessage(receiver_, message); } IOracleCommand& GetCommand() const @@ -168,41 +147,17 @@ return dynamic_cast(*command_); } -#if 0 - static std::string ToString(Orthanc::HttpMethod method) - { - switch (method) { - case Orthanc::HttpMethod_Get: - return "GET"; - break; - case Orthanc::HttpMethod_Post: - return "POST"; - break; - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - break; - } - } - static void DumpCommand(emscripten_fetch_t* fetch, std::string answer) +#if ORTHANC_ENABLE_DCMTK == 1 + void StoreInCache(const std::string& sopInstanceUid, + std::auto_ptr& dicom, + size_t fileSize) { - FetchContext* context = reinterpret_cast(fetch->userData); - - const auto& command = context->GetTypedCommand(); - auto commandStr = ToString(command.GetMethod()); - LOG(TRACE) << "SuccessCallback for REST command. Method is : " << commandStr; - switch (command.GetMethod()) { - case Orthanc::HttpMethod_Get: - LOG(TRACE) << " * SuccessCallback GET URI = " << command.GetUri() << " timeout = " << command.GetTimeout(); - LOG(TRACE) << " * SuccessCallback GET RESPONSE = " << answer; - break; - case Orthanc::HttpMethod_Post: - LOG(TRACE) << " * SuccessCallback POST URI = " << command.GetUri() << " body = " << command.GetBody() << " timeout = " << command.GetTimeout(); - LOG(TRACE) << " * SuccessCallback POST RESPONSE = " << answer; - break; - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - break; - } + if (oracle_.dicomCache_.get()) + { + // Store it into the cache for future use + oracle_.dicomCache_->Acquire(BUCKET_SOP, sopInstanceUid, + dicom.release(), fileSize, true); + } } #endif @@ -213,64 +168,43 @@ * free data associated with the fetch. **/ - std::auto_ptr context(reinterpret_cast(fetch->userData)); - - // an UUID is 36 chars : 32 hex chars + 4 hyphens: char #0 --> char #35 - // char #36 is \0. - bool callHandler = true; - - // TODO: remove this line because we are NOT allowed to call methods on GetReceiver that is maybe a dangling ref - if (context->GetReceiver().DoesFingerprintLookGood()) - { - callHandler = true; - int64_t currentFingerprint(context->GetReceiver().GetFingerprint()); - - LOG(TRACE) << "SuccessCallback for object at address (" << std::hex - << &(context->GetReceiver()) << std::dec - << " with current fingerprint = " << currentFingerprint - << ". Fingerprint looks OK"; - - if (currentFingerprint != context->receiverFingerprint_) - { - LOG(TRACE) << " ** SuccessCallback: BUT currentFingerprint != " - << "receiverFingerprint_(" << context->receiverFingerprint_ << ")"; - callHandler = false; - } - else - { - LOG(TRACE) << " ** SuccessCallback: FetchContext-level " - << "fingerprints are the same: " - << context->receiverFingerprint_ - << " ---> oracle will dispatch the response to observer: " - << std::hex << &(context->GetReceiver()) << std::dec; - } - } - else { - LOG(TRACE) << "SuccessCallback for object at address (" << std::hex << &(context->GetReceiver()) << std::dec << " with current fingerprint is XXXXX -- NOT A VALID FINGERPRINT! OBJECT IS READ ! CALLBACK WILL NOT BE CALLED!"; - callHandler = false; - } - if (fetch->userData == NULL) { LOG(ERROR) << "WebAssemblyOracle::FetchContext::SuccessCallback fetch->userData is NULL!!!!!!!"; + return; } + std::auto_ptr context(reinterpret_cast(fetch->userData)); + std::string answer; if (fetch->numBytes > 0) { answer.assign(fetch->data, fetch->numBytes); } + /** - * TODO - HACK - As of emscripten-1.38.31, the fetch API does - * not contain a way to retrieve the HTTP headers of the - * answer. We make the assumption that the "Content-Type" header - * of the response is the same as the "Accept" header of the - * query. This should be fixed in future versions of emscripten. + * Retrieving the headers of the HTTP answer. + **/ + HttpHeaders headers; + +#if (__EMSCRIPTEN_major__ < 1 || \ + (__EMSCRIPTEN_major__ == 1 && __EMSCRIPTEN_minor__ < 38) || \ + (__EMSCRIPTEN_major__ == 1 && __EMSCRIPTEN_minor__ == 38 && __EMSCRIPTEN_tiny__ < 37)) +# warning Consider upgrading Emscripten to a version above 1.38.37, incomplete support of Fetch API + + /** + * HACK - If emscripten < 1.38.37, the fetch API does not + * contain a way to retrieve the HTTP headers of the answer. We + * make the assumption that the "Content-Type" header of the + * response is the same as the "Accept" header of the + * query. This is fixed thanks to the + * "emscripten_fetch_get_response_headers()" function that was + * added to "fetch.h" at emscripten-1.38.37 on 2019-06-26. + * + * https://github.com/emscripten-core/emscripten/blob/1.38.37/system/include/emscripten/fetch.h * https://github.com/emscripten-core/emscripten/pull/8486 **/ - - HttpHeaders headers; if (fetch->userData != NULL) { if (!context->GetExpectedContentType().empty()) @@ -278,13 +212,29 @@ headers["Content-Type"] = context->GetExpectedContentType(); } } - -#if 0 - if (context->GetCommand().GetType() == IOracleCommand::Type_OrthancRestApi) { - //if (logbgo115) - DumpCommand(fetch, answer); +#else + { + size_t size = emscripten_fetch_get_response_headers_length(fetch); + + std::string plainHeaders(size + 1, '\0'); + emscripten_fetch_get_response_headers(fetch, &plainHeaders[0], size + 1); + + std::vector tokens; + Orthanc::Toolbox::TokenizeString(tokens, plainHeaders, '\n'); + + for (size_t i = 0; i < tokens.size(); i++) + { + size_t p = tokens[i].find(':'); + if (p != std::string::npos) + { + std::string key = Orthanc::Toolbox::StripSpaces(tokens[i].substr(0, p)); + std::string value = Orthanc::Toolbox::StripSpaces(tokens[i].substr(p + 1)); + headers[key] = value; + } + } } #endif + LOG(TRACE) << "About to call emscripten_fetch_close"; emscripten_fetch_close(fetch); LOG(TRACE) << "Successfully called emscripten_fetch_close"; @@ -303,50 +253,75 @@ } else { - if (callHandler) + switch (context->GetCommand().GetType()) { - switch (context->GetCommand().GetType()) + case IOracleCommand::Type_Http: + { + HttpCommand::SuccessMessage message(context->GetTypedCommand(), headers, answer); + context->EmitMessage(message); + break; + } + + case IOracleCommand::Type_OrthancRestApi: + { + LOG(TRACE) << "WebAssemblyOracle::FetchContext::SuccessCallback. About to call context->EmitMessage(message);"; + OrthancRestApiCommand::SuccessMessage message + (context->GetTypedCommand(), headers, answer); + context->EmitMessage(message); + break; + } + + case IOracleCommand::Type_GetOrthancImage: { - case IOracleCommand::Type_Http: + context->GetTypedCommand().ProcessHttpAnswer + (context->GetReceiver(), context->GetEmitter(), answer, headers); + break; + } + + case IOracleCommand::Type_GetOrthancWebViewerJpeg: + { + context->GetTypedCommand().ProcessHttpAnswer + (context->GetReceiver(), context->GetEmitter(), answer); + break; + } + + case IOracleCommand::Type_ParseDicomFromWado: + { +#if ORTHANC_ENABLE_DCMTK == 1 + const ParseDicomFromWadoCommand& command = + context->GetTypedCommand(); + + size_t fileSize; + std::auto_ptr dicom + (ParseDicomSuccessMessage::ParseWadoAnswer(fileSize, answer, headers)); + { - HttpCommand::SuccessMessage message(context->GetTypedCommand(), headers, answer); + ParseDicomSuccessMessage message(command, *dicom, fileSize, true); context->EmitMessage(message); - break; } - case IOracleCommand::Type_OrthancRestApi: - { - LOG(TRACE) << "WebAssemblyOracle::FetchContext::SuccessCallback. About to call context->EmitMessage(message);"; - OrthancRestApiCommand::SuccessMessage message - (context->GetTypedCommand(), headers, answer); - context->EmitMessage(message); - break; - } + context->StoreInCache(command.GetSopInstanceUid(), dicom, fileSize); +#else + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); +#endif + break; + } - case IOracleCommand::Type_GetOrthancImage: - { - context->GetTypedCommand().ProcessHttpAnswer - (context->GetEmitter(), context->GetReceiver(), answer, headers); - break; - } - - case IOracleCommand::Type_GetOrthancWebViewerJpeg: - { - context->GetTypedCommand().ProcessHttpAnswer - (context->GetEmitter(), context->GetReceiver(), answer); - break; - } - - default: - LOG(ERROR) << "Command type not implemented by the WebAssembly Oracle: " - << context->GetCommand().GetType(); - } + default: + LOG(ERROR) << "Command type not implemented by the WebAssembly Oracle (in SuccessCallback): " + << context->GetCommand().GetType(); + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); } } } catch (Orthanc::OrthancException& e) { - LOG(ERROR) << "Error while processing a fetch answer in the oracle: " << e.What(); + LOG(INFO) << "Error while processing a fetch answer in the oracle: " << e.What(); + + { + OracleCommandExceptionMessage message(context->GetCommand(), e); + context->EmitMessage(message); + } } } @@ -388,7 +363,7 @@ { private: WebAssemblyOracle& oracle_; - const IObserver& receiver_; + boost::weak_ptr receiver_; std::auto_ptr command_; Orthanc::HttpMethod method_; std::string url_; @@ -396,16 +371,20 @@ HttpHeaders headers_; unsigned int timeout_; std::string expectedContentType_; + bool hasCredentials_; + std::string username_; + std::string password_; public: FetchCommand(WebAssemblyOracle& oracle, - const IObserver& receiver, + boost::weak_ptr receiver, IOracleCommand* command) : oracle_(oracle), receiver_(receiver), command_(command), method_(Orthanc::HttpMethod_Get), - timeout_(0) + timeout_(0), + hasCredentials_(false) { if (command == NULL) { @@ -418,11 +397,6 @@ method_ = method; } - void SetOrthancUri(const std::string& uri) - { - url_ = oracle_.orthancRoot_ + uri; - } - void SetUrl(const std::string& url) { url_ = url; @@ -433,9 +407,12 @@ body_.swap(body); } - void SetHttpHeaders(const HttpHeaders& headers) + void AddHttpHeaders(const HttpHeaders& headers) { - headers_ = headers; + for (HttpHeaders::const_iterator it = headers.begin(); it != headers.end(); ++it) + { + headers_[it->first] = it->second; + } } void SetTimeout(unsigned int timeout) @@ -443,15 +420,16 @@ timeout_ = timeout; } + void SetCredentials(const std::string& username, + const std::string& password) + { + hasCredentials_ = true; + username_ = username; + password_ = password; + } + void Execute() { -#if 0 - if (logbgo233) { - if (logbgo115) - LOG(TRACE) << " WebAssemblyOracle::Execute () command addr " << - std::hex << command_.get() << std::dec; - } -#endif if (command_.get() == NULL) { // Cannot call Execute() twice @@ -493,6 +471,13 @@ attr.onerror = FetchContext::FailureCallback; attr.timeoutMSecs = timeout_ * 1000; + if (hasCredentials_) + { + attr.withCredentials = EM_TRUE; + attr.userName = username_.c_str(); + attr.password = password_.c_str(); + } + std::vector headers; headers.reserve(2 * headers_.size() + 1); @@ -534,9 +519,6 @@ attr.userData = new FetchContext(oracle_, receiver_, command_.release(), expectedContentType); // Must be the last call to prevent memory leak on error -#if 0 - LOG(TRACE) << "Performing " << method << " request on URI: \"" << url_ << "\""; -#endif emscripten_fetch(&attr, url_.c_str()); } catch(...) @@ -548,34 +530,35 @@ } }; -#if 0 - static void DumpCommand(OrthancRestApiCommand* pCommand) + + void WebAssemblyOracle::SetOrthancUrl(FetchCommand& command, + const std::string& uri) const { - OrthancRestApiCommand& command = *pCommand; - LOG(TRACE) << "WebAssemblyOracle::Execute for REST command."; - switch (command.GetMethod()) { - case Orthanc::HttpMethod_Get: - LOG(TRACE) << " * WebAssemblyOracle::Execute GET URI = " << command.GetUri() << " timeout = " << command.GetTimeout(); - break; - case Orthanc::HttpMethod_Post: - LOG(TRACE) << " * WebAssemblyOracle::Execute POST URI = " << command.GetUri() << " body = " << command.GetBody() << " timeout = " << command.GetTimeout(); - break; - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - break; + if (isLocalOrthanc_) + { + command.SetUrl(localOrthancRoot_ + uri); + } + else + { + command.SetUrl(remoteOrthanc_.GetUrl() + uri); + command.AddHttpHeaders(remoteOrthanc_.GetHttpHeaders()); + + if (!remoteOrthanc_.GetUsername().empty()) + { + command.SetCredentials(remoteOrthanc_.GetUsername(), remoteOrthanc_.GetPassword()); + } } } -#endif + - - void WebAssemblyOracle::Execute(const IObserver& receiver, + void WebAssemblyOracle::Execute(boost::weak_ptr receiver, HttpCommand* command) { FetchCommand fetch(*this, receiver, command); fetch.SetMethod(command->GetMethod()); fetch.SetUrl(command->GetUrl()); - fetch.SetHttpHeaders(command->GetHttpHeaders()); + fetch.AddHttpHeaders(command->GetHttpHeaders()); fetch.SetTimeout(command->GetTimeout()); if (command->GetMethod() == Orthanc::HttpMethod_Post || @@ -590,19 +573,9 @@ } - void WebAssemblyOracle::Execute(const IObserver& receiver, + void WebAssemblyOracle::Execute(boost::weak_ptr receiver, OrthancRestApiCommand* command) { -#if 0 - DumpCommand(command); - - if (logbgo233) { - if (logbgo115) - LOG(TRACE) << " WebAssemblyOracle::Execute (OrthancRestApiCommand) command addr " << - std::hex << command << std::dec; - } -#endif - try { //LOG(TRACE) << "*********** WebAssemblyOracle::Execute."; @@ -610,8 +583,8 @@ FetchCommand fetch(*this, receiver, command); fetch.SetMethod(command->GetMethod()); - fetch.SetOrthancUri(command->GetUri()); - fetch.SetHttpHeaders(command->GetHttpHeaders()); + SetOrthancUrl(fetch, command->GetUri()); + fetch.AddHttpHeaders(command->GetHttpHeaders()); fetch.SetTimeout(command->GetTimeout()); if (command->GetMethod() == Orthanc::HttpMethod_Post || @@ -653,54 +626,112 @@ } - void WebAssemblyOracle::Execute(const IObserver& receiver, + void WebAssemblyOracle::Execute(boost::weak_ptr receiver, GetOrthancImageCommand* command) { -#if 0 - if (logbgo233) { - if (logbgo115) - LOG(TRACE) << " WebAssemblyOracle::Execute (GetOrthancImageCommand) command addr " << - std::hex << command << std::dec; - } -#endif - FetchCommand fetch(*this, receiver, command); - fetch.SetOrthancUri(command->GetUri()); - fetch.SetHttpHeaders(command->GetHttpHeaders()); + SetOrthancUrl(fetch, command->GetUri()); + fetch.AddHttpHeaders(command->GetHttpHeaders()); fetch.SetTimeout(command->GetTimeout()); fetch.Execute(); } - void WebAssemblyOracle::Execute(const IObserver& receiver, + void WebAssemblyOracle::Execute(boost::weak_ptr receiver, GetOrthancWebViewerJpegCommand* command) { -#if 0 - if (logbgo233) { - if (logbgo115) - LOG(TRACE) << " WebAssemblyOracle::Execute (GetOrthancWebViewerJpegCommand) command addr " << std::hex << command << std::dec; - } -#endif - FetchCommand fetch(*this, receiver, command); - fetch.SetOrthancUri(command->GetUri()); - fetch.SetHttpHeaders(command->GetHttpHeaders()); + SetOrthancUrl(fetch, command->GetUri()); + fetch.AddHttpHeaders(command->GetHttpHeaders()); fetch.SetTimeout(command->GetTimeout()); fetch.Execute(); } + void WebAssemblyOracle::Execute(boost::weak_ptr receiver, + ParseDicomFromWadoCommand* command) + { + std::auto_ptr protection(command); + +#if ORTHANC_ENABLE_DCMTK == 1 + if (dicomCache_.get()) + { + ParsedDicomCache::Reader reader(*dicomCache_, BUCKET_SOP, protection->GetSopInstanceUid()); + if (reader.IsValid() && + reader.HasPixelData()) + { + // Reuse the DICOM file from the cache + ParseDicomSuccessMessage message(*protection, reader.GetDicom(), + reader.GetFileSize(), reader.HasPixelData()); + EmitMessage(receiver, message); + return; + } + } +#endif - void WebAssemblyOracle::Schedule(const IObserver& receiver, + switch (command->GetRestCommand().GetType()) + { + case IOracleCommand::Type_Http: + { + const HttpCommand& rest = + dynamic_cast(protection->GetRestCommand()); + + FetchCommand fetch(*this, receiver, protection.release()); + + fetch.SetMethod(rest.GetMethod()); + fetch.SetUrl(rest.GetUrl()); + fetch.AddHttpHeaders(rest.GetHttpHeaders()); + fetch.SetTimeout(rest.GetTimeout()); + + if (rest.GetMethod() == Orthanc::HttpMethod_Post || + rest.GetMethod() == Orthanc::HttpMethod_Put) + { + std::string body = rest.GetBody(); + fetch.SetBody(body); + } + + fetch.Execute(); + break; + } + + case IOracleCommand::Type_OrthancRestApi: + { + const OrthancRestApiCommand& rest = + dynamic_cast(protection->GetRestCommand()); + + FetchCommand fetch(*this, receiver, protection.release()); + + fetch.SetMethod(rest.GetMethod()); + SetOrthancUrl(fetch, rest.GetUri()); + fetch.AddHttpHeaders(rest.GetHttpHeaders()); + fetch.SetTimeout(rest.GetTimeout()); + + if (rest.GetMethod() == Orthanc::HttpMethod_Post || + rest.GetMethod() == Orthanc::HttpMethod_Put) + { + std::string body = rest.GetBody(); + fetch.SetBody(body); + } + + fetch.Execute(); + break; + } + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } + + + bool WebAssemblyOracle::Schedule(boost::shared_ptr receiver, IOracleCommand* command) { LOG(TRACE) << "WebAssemblyOracle::Schedule : receiver = " - << std::hex << &receiver << std::dec - << " | Current fingerprint is " << receiver.GetFingerprint(); + << std::hex << &receiver; std::auto_ptr protection(command); @@ -716,37 +747,14 @@ break; case IOracleCommand::Type_OrthancRestApi: - //// DIAGNOSTIC. PLEASE REMOVE IF IT HAS BEEN COMMITTED BY MISTAKE - //{ - // const IObserver* pReceiver = &receiver; - // LOG(TRACE) << "WebAssemblyOracle::Schedule | pReceiver is " << pReceiver; - // LOG(TRACE) << "WebAssemblyOracle::Schedule | command = " << command; - // OrthancRestApiCommand* rac = dynamic_cast(protection.get()); - // LOG(TRACE) << "WebAssemblyOracle::Schedule | typed command = " << rac; - // LOG(TRACE) << "WebAssemblyOracle::Schedule" << rac->GetUri(); - //} - //// END OF BLOCK TO REMOVE Execute(receiver, dynamic_cast(protection.release())); break; case IOracleCommand::Type_GetOrthancImage: - //// DIAGNOSTIC. PLEASE REMOVE IF IT HAS BEEN COMMITTED BY MISTAKE - //{ - // GetOrthancImageCommand* rac = dynamic_cast(protection.get()); - // LOG(TRACE) << "WebAssemblyOracle::Schedule" << rac->GetUri(); - //} - //// END OF BLOCK TO REMOVE Execute(receiver, dynamic_cast(protection.release())); break; case IOracleCommand::Type_GetOrthancWebViewerJpeg: - //// DIAGNOSTIC. PLEASE REMOVE IF IT HAS BEEN COMMITTED BY MISTAKE - //{ - // GetOrthancWebViewerJpegCommand* rac = dynamic_cast(protection.get()); - // LOG(TRACE) << "WebAssemblyOracle::Schedule" << rac->GetUri(); - //} - //// END OF BLOCK TO REMOVE - Execute(receiver, dynamic_cast(protection.release())); break; case IOracleCommand::Type_Sleep: @@ -757,8 +765,38 @@ break; } + case IOracleCommand::Type_ParseDicomFromWado: +#if ORTHANC_ENABLE_DCMTK == 1 + Execute(receiver, dynamic_cast(protection.release())); +#else + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, + "DCMTK must be enabled to parse DICOM files"); +#endif + break; + default: - LOG(ERROR) << "Command type not implemented by the WebAssembly Oracle: " << command->GetType(); + LOG(ERROR) << "Command type not implemented by the WebAssembly Oracle (in Schedule): " + << command->GetType(); + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); } + + return true; + } + + + void WebAssemblyOracle::SetDicomCacheSize(size_t size) + { +#if ORTHANC_ENABLE_DCMTK == 1 + if (size == 0) + { + dicomCache_.reset(); + } + else + { + dicomCache_.reset(new ParsedDicomCache(size)); + } +#else + LOG(INFO) << "DCMTK support is disabled, the DICOM cache is disabled"; +#endif } } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Oracle/WebAssemblyOracle.h --- a/Framework/Oracle/WebAssemblyOracle.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Oracle/WebAssemblyOracle.h Tue Feb 04 15:24:03 2020 +0100 @@ -35,48 +35,87 @@ #include "HttpCommand.h" #include "IOracle.h" #include "OrthancRestApiCommand.h" +#include "ParseDicomFromWadoCommand.h" + +#if ORTHANC_ENABLE_DCMTK == 1 +# include "../Toolbox/ParsedDicomCache.h" +#endif + +#include namespace OrthancStone { class WebAssemblyOracle : public IOracle, - public IObservable + public IMessageEmitter { private: typedef std::map HttpHeaders; class TimeoutContext; - class Emitter; class FetchContext; - class FetchCommand; + class FetchCommand; + + void SetOrthancUrl(FetchCommand& command, + const std::string& uri) const; - void Execute(const IObserver& receiver, + void Execute(boost::weak_ptr receiver, HttpCommand* command); - void Execute(const IObserver& receiver, + void Execute(boost::weak_ptr receiver, OrthancRestApiCommand* command); - void Execute(const IObserver& receiver, + void Execute(boost::weak_ptr receiver, GetOrthancImageCommand* command); - void Execute(const IObserver& receiver, + void Execute(boost::weak_ptr receiver, GetOrthancWebViewerJpegCommand* command); + + void Execute(boost::weak_ptr receiver, + ParseDicomFromWadoCommand* command); - std::string orthancRoot_; + IObservable oracleObservable_; + bool isLocalOrthanc_; + std::string localOrthancRoot_; + Orthanc::WebServiceParameters remoteOrthanc_; + +#if ORTHANC_ENABLE_DCMTK == 1 + std::auto_ptr dicomCache_; +#endif public: - WebAssemblyOracle(MessageBroker& broker) : - IObservable(broker) + WebAssemblyOracle() : + isLocalOrthanc_(false) { } - - void SetOrthancRoot(const std::string& root) + + virtual void EmitMessage(boost::weak_ptr observer, + const IMessage& message) ORTHANC_OVERRIDE { - orthancRoot_ = root; + oracleObservable_.EmitMessage(observer, message); } - virtual void Schedule(const IObserver& receiver, - IOracleCommand* command); + virtual bool Schedule(boost::shared_ptr receiver, + IOracleCommand* command) ORTHANC_OVERRIDE; + + IObservable& GetOracleObservable() + { + return oracleObservable_; + } + + void SetLocalOrthanc(const std::string& root) + { + isLocalOrthanc_ = true; + localOrthancRoot_ = root; + } + + void SetRemoteOrthanc(const Orthanc::WebServiceParameters& orthanc) + { + isLocalOrthanc_ = false; + remoteOrthanc_ = orthanc; + } + + void SetDicomCacheSize(size_t size); }; } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Radiography/RadiographyAlphaLayer.h --- a/Framework/Radiography/RadiographyAlphaLayer.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Radiography/RadiographyAlphaLayer.h Tue Feb 04 15:24:03 2020 +0100 @@ -37,8 +37,8 @@ float foreground_; // in the range [0.0, 65535.0] public: - RadiographyAlphaLayer(MessageBroker& broker, const RadiographyScene& scene) : - RadiographyLayer(broker, scene), + RadiographyAlphaLayer(const RadiographyScene& scene) : + RadiographyLayer(scene), foreground_(0) { } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Radiography/RadiographyDicomLayer.cpp --- a/Framework/Radiography/RadiographyDicomLayer.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Radiography/RadiographyDicomLayer.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -48,7 +48,8 @@ } - RadiographyDicomLayer::RadiographyDicomLayer(MessageBroker& broker, const RadiographyScene& scene) : RadiographyLayer(broker, scene) + RadiographyDicomLayer::RadiographyDicomLayer(const RadiographyScene& scene) : + RadiographyLayer(scene) { } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Radiography/RadiographyDicomLayer.h --- a/Framework/Radiography/RadiographyDicomLayer.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Radiography/RadiographyDicomLayer.h Tue Feb 04 15:24:03 2020 +0100 @@ -42,7 +42,7 @@ void ApplyConverter(); public: - RadiographyDicomLayer(MessageBroker& broker, const RadiographyScene& scene); + RadiographyDicomLayer(const RadiographyScene& scene); void SetInstance(const std::string& instanceId, unsigned int frame) { diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Radiography/RadiographyLayer.cpp --- a/Framework/Radiography/RadiographyLayer.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Radiography/RadiographyLayer.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -119,8 +119,7 @@ } - RadiographyLayer::RadiographyLayer(MessageBroker& broker, const RadiographyScene& scene) : - IObservable(broker), + RadiographyLayer::RadiographyLayer(const RadiographyScene& scene) : index_(0), hasSize_(false), width_(0), diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Radiography/RadiographyLayer.h --- a/Framework/Radiography/RadiographyLayer.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Radiography/RadiographyLayer.h Tue Feb 04 15:24:03 2020 +0100 @@ -238,7 +238,7 @@ double zoom); public: - RadiographyLayer(MessageBroker& broker, const RadiographyScene& scene); + RadiographyLayer(const RadiographyScene& scene); virtual ~RadiographyLayer() { diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Radiography/RadiographyMaskLayer.h --- a/Framework/Radiography/RadiographyMaskLayer.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Radiography/RadiographyMaskLayer.h Tue Feb 04 15:24:03 2020 +0100 @@ -40,9 +40,9 @@ mutable std::auto_ptr mask_; public: - RadiographyMaskLayer(MessageBroker& broker, const RadiographyScene& scene, const RadiographyDicomLayer& dicomLayer, + RadiographyMaskLayer(const RadiographyScene& scene, const RadiographyDicomLayer& dicomLayer, float foreground) : - RadiographyLayer(broker, scene), + RadiographyLayer(scene), dicomLayer_(dicomLayer), invalidated_(true), foreground_(foreground) diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Radiography/RadiographyScene.cpp --- a/Framework/Radiography/RadiographyScene.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Radiography/RadiographyScene.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -147,7 +147,7 @@ BroadcastMessage(GeometryChangedMessage(*this, *layer)); BroadcastMessage(ContentChangedMessage(*this, *layer)); - layer->RegisterObserverCallback(new Callable(*this, &RadiographyScene::OnLayerEdited)); + Register(*layer, &RadiographyScene::OnLayerEdited); return *layer; } @@ -167,9 +167,8 @@ BroadcastMessage(RadiographyScene::LayerEditedMessage(*this, message.GetOrigin())); } - RadiographyScene::RadiographyScene(MessageBroker& broker) : - IObserver(broker), - IObservable(broker), + + RadiographyScene::RadiographyScene() : nextLayerIndex_(0), hasWindowing_(false), windowingCenter_(0), // Dummy initialization @@ -325,7 +324,7 @@ RadiographyLayer::Geometry* centerGeometry, bool isCenterGeometry) { - std::auto_ptr alpha(new RadiographyTextLayer(IObservable::GetBroker(), *this)); + std::auto_ptr alpha(new RadiographyTextLayer(*this)); alpha->SetText(utf8, font, fontSize, foreground); if (centerGeometry != NULL) { @@ -382,7 +381,7 @@ float foreground, RadiographyLayer::Geometry* geometry) { - std::auto_ptr mask(new RadiographyMaskLayer(IObservable::GetBroker(), *this, dicomLayer, foreground)); + std::auto_ptr mask(new RadiographyMaskLayer(*this, dicomLayer, foreground)); mask->SetCorners(corners); if (geometry != NULL) { @@ -395,7 +394,7 @@ RadiographyLayer& RadiographyScene::LoadAlphaBitmap(Orthanc::ImageAccessor* bitmap, RadiographyLayer::Geometry *geometry) { - std::auto_ptr alpha(new RadiographyAlphaLayer(IObservable::GetBroker(), *this)); + std::auto_ptr alpha(new RadiographyAlphaLayer(*this)); alpha->SetAlpha(bitmap); if (geometry != NULL) { @@ -412,7 +411,7 @@ RadiographyPhotometricDisplayMode preferredPhotometricDisplayMode, RadiographyLayer::Geometry* geometry) { - RadiographyDicomLayer& layer = dynamic_cast(RegisterLayer(new RadiographyDicomLayer(IObservable::GetBroker(), *this))); + RadiographyDicomLayer& layer = dynamic_cast(RegisterLayer(new RadiographyDicomLayer(*this))); layer.SetInstance(instance, frame); @@ -434,7 +433,7 @@ bool httpCompression, RadiographyLayer::Geometry* geometry) { - RadiographyDicomLayer& layer = dynamic_cast(RegisterLayer(new RadiographyDicomLayer(IObservable::GetBroker(), *this))); + RadiographyDicomLayer& layer = dynamic_cast(RegisterLayer(new RadiographyDicomLayer( *this))); layer.SetInstance(instance, frame); if (geometry != NULL) @@ -448,8 +447,8 @@ orthanc.GetBinaryAsync( uri, headers, - new Callable - (*this, &RadiographyScene::OnTagsReceived), NULL, + new Deprecated::DeprecatedCallable + (GetSharedObserver(), &RadiographyScene::OnTagsReceived), NULL, new Orthanc::SingleValueObject(layer.GetIndex())); } @@ -467,8 +466,8 @@ orthanc.GetBinaryAsync( uri, headers, - new Callable - (*this, &RadiographyScene::OnFrameReceived), NULL, + new Deprecated::DeprecatedCallable + (GetSharedObserver(), &RadiographyScene::OnFrameReceived), NULL, new Orthanc::SingleValueObject(layer.GetIndex())); } @@ -478,7 +477,7 @@ RadiographyLayer& RadiographyScene::LoadDicomWebFrame(Deprecated::IWebService& web) { - RadiographyLayer& layer = RegisterLayer(new RadiographyDicomLayer(IObservable::GetBroker(), *this)); + RadiographyLayer& layer = RegisterLayer(new RadiographyDicomLayer(*this)); return layer; @@ -849,8 +848,8 @@ orthanc.PostJsonAsyncExpectJson( "/tools/create-dicom", createDicomRequestContent, - new Callable - (*this, &RadiographyScene::OnDicomExported), + new Deprecated::DeprecatedCallable + (GetSharedObserver(), &RadiographyScene::OnDicomExported), NULL, NULL); } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Radiography/RadiographyScene.h --- a/Framework/Radiography/RadiographyScene.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Radiography/RadiographyScene.h Tue Feb 04 15:24:03 2020 +0100 @@ -22,6 +22,7 @@ #pragma once #include "RadiographyLayer.h" +#include "../Messages/ObserverBase.h" #include "../Deprecated/Toolbox/DicomFrameConverter.h" #include "../Deprecated/Toolbox/OrthancApiClient.h" #include "../StoneEnumerations.h" @@ -33,8 +34,8 @@ class RadiographyDicomLayer; class RadiographyScene : - public IObserver, - public IObservable + public ObserverBase, + public IObservable { friend class RadiographySceneGeometryReader; public: @@ -191,7 +192,7 @@ virtual void OnLayerEdited(const RadiographyLayer::LayerEditedMessage& message); public: - RadiographyScene(MessageBroker& broker); + RadiographyScene(); virtual ~RadiographyScene(); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Radiography/RadiographySceneReader.cpp --- a/Framework/Radiography/RadiographySceneReader.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Radiography/RadiographySceneReader.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -55,7 +55,7 @@ RadiographyDicomLayer* RadiographySceneGeometryReader::LoadDicom(const std::string& instanceId, unsigned int frame, RadiographyLayer::Geometry* geometry) { - std::auto_ptr layer(new RadiographyPlaceholderLayer(dynamic_cast(scene_).GetBroker(), scene_)); + std::auto_ptr layer(new RadiographyPlaceholderLayer(scene_)); layer->SetGeometry(*geometry); layer->SetSize(dicomImageWidth_, dicomImageHeight_); scene_.RegisterLayer(layer.get()); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Radiography/RadiographySceneReader.h --- a/Framework/Radiography/RadiographySceneReader.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Radiography/RadiographySceneReader.h Tue Feb 04 15:24:03 2020 +0100 @@ -37,8 +37,8 @@ class RadiographyPlaceholderLayer : public RadiographyDicomLayer { public: - RadiographyPlaceholderLayer(MessageBroker& broker, const RadiographyScene& scene) : - RadiographyDicomLayer(broker, scene) + RadiographyPlaceholderLayer(const RadiographyScene& scene) : + RadiographyDicomLayer(scene) { } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Radiography/RadiographyTextLayer.h --- a/Framework/Radiography/RadiographyTextLayer.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Radiography/RadiographyTextLayer.h Tue Feb 04 15:24:03 2020 +0100 @@ -37,8 +37,8 @@ static std::map fonts_; public: - RadiographyTextLayer(MessageBroker& broker, const RadiographyScene& scene) : - RadiographyAlphaLayer(broker, scene) + RadiographyTextLayer(const RadiographyScene& scene) : + RadiographyAlphaLayer(scene) { } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Radiography/RadiographyWidget.cpp --- a/Framework/Radiography/RadiographyWidget.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Radiography/RadiographyWidget.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -174,12 +174,9 @@ } - RadiographyWidget::RadiographyWidget(MessageBroker& broker, - boost::shared_ptr scene, + RadiographyWidget::RadiographyWidget(boost::shared_ptr scene, const std::string& name) : WorldSceneWidget(name), - IObserver(broker), - IObservable(broker), invert_(false), interpolation_(ImageInterpolation_Nearest), hasSelection_(false), @@ -271,24 +268,11 @@ void RadiographyWidget::SetScene(boost::shared_ptr scene) { - if (scene_ != NULL) - { - scene_->Unregister(this); - } - scene_ = scene; - scene_->RegisterObserverCallback( - new Callable - (*this, &RadiographyWidget::OnGeometryChanged)); - - scene_->RegisterObserverCallback( - new Callable - (*this, &RadiographyWidget::OnContentChanged)); - - scene_->RegisterObserverCallback( - new Callable - (*this, &RadiographyWidget::OnLayerRemoved)); + Register(*scene_, &RadiographyWidget::OnGeometryChanged); + Register(*scene_, &RadiographyWidget::OnContentChanged); + Register(*scene_, &RadiographyWidget::OnLayerRemoved); Unselect(); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Radiography/RadiographyWidget.h --- a/Framework/Radiography/RadiographyWidget.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Radiography/RadiographyWidget.h Tue Feb 04 15:24:03 2020 +0100 @@ -22,6 +22,7 @@ #pragma once #include "../Deprecated/Widgets/WorldSceneWidget.h" +#include "../Messages/ObserverBase.h" #include "RadiographyScene.h" @@ -31,7 +32,7 @@ class RadiographyWidget : public Deprecated::WorldSceneWidget, - public IObserver, + public ObserverBase, public IObservable { public: @@ -64,8 +65,7 @@ bool IsInvertedInternal() const; public: - RadiographyWidget(MessageBroker& broker, - boost::shared_ptr scene, // TODO: check how we can avoid boost::shared_ptr here since we don't want them in the public API (app is keeping a boost::shared_ptr to this right now) + RadiographyWidget(boost::shared_ptr scene, // TODO: check how we can avoid boost::shared_ptr here since we don't want them in the public API (app is keeping a boost::shared_ptr to this right now) const std::string& name); RadiographyScene& GetScene() const diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2D/CairoCompositor.cpp --- a/Framework/Scene2D/CairoCompositor.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2D/CairoCompositor.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -85,11 +85,10 @@ } - CairoCompositor::CairoCompositor(const Scene2D& scene, - unsigned int canvasWidth, - unsigned int canvasHeight) : - helper_(scene, *this) + CairoCompositor::CairoCompositor(unsigned int canvasWidth, + unsigned int canvasHeight) { + ResetScene(); UpdateSize(canvasWidth, canvasHeight); } @@ -154,7 +153,7 @@ #endif - void CairoCompositor::Refresh() + void CairoCompositor::Refresh(const Scene2D& scene) { context_.reset(new CairoContext(canvas_)); @@ -162,7 +161,7 @@ cairo_set_source_rgba(context_->GetObject(), 0, 0, 0, 255); cairo_paint(context_->GetObject()); - helper_.Refresh(canvas_.GetWidth(), canvas_.GetHeight()); + helper_->Refresh(scene, canvas_.GetWidth(), canvas_.GetHeight()); context_.reset(); } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2D/CairoCompositor.h --- a/Framework/Scene2D/CairoCompositor.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2D/CairoCompositor.h Tue Feb 04 15:24:03 2020 +0100 @@ -37,7 +37,7 @@ private: typedef std::map Fonts; - Internals::CompositorHelper helper_; + std::auto_ptr helper_; CairoSurface canvas_; Fonts fonts_; @@ -49,8 +49,7 @@ virtual Internals::CompositorHelper::ILayerRenderer* Create(const ISceneLayer& layer) ORTHANC_OVERRIDE; public: - CairoCompositor(const Scene2D& scene, - unsigned int canvasWidth, + CairoCompositor(unsigned int canvasWidth, unsigned int canvasHeight); virtual ~CairoCompositor(); @@ -80,7 +79,12 @@ Orthanc::Encoding codepage) ORTHANC_OVERRIDE; #endif - virtual void Refresh() ORTHANC_OVERRIDE; + virtual void Refresh(const Scene2D& scene) ORTHANC_OVERRIDE; + + virtual void ResetScene() ORTHANC_OVERRIDE + { + helper_.reset(new Internals::CompositorHelper(*this)); + } void UpdateSize(unsigned int canvasWidth, unsigned int canvasHeight); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2D/FloatTextureSceneLayer.cpp --- a/Framework/Scene2D/FloatTextureSceneLayer.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2D/FloatTextureSceneLayer.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -28,7 +28,8 @@ namespace OrthancStone { FloatTextureSceneLayer::FloatTextureSceneLayer(const Orthanc::ImageAccessor& texture) : - inverted_(false) + inverted_(false), + applyLog_(false) { { std::auto_ptr t( @@ -92,6 +93,14 @@ IncrementRevision(); } + + void FloatTextureSceneLayer::SetApplyLog(bool apply) + { + applyLog_ = apply; + IncrementRevision(); + } + + void FloatTextureSceneLayer::FitRange() { float minValue, maxValue; @@ -123,6 +132,7 @@ cloned->customCenter_ = customCenter_; cloned->customWidth_ = customWidth_; cloned->inverted_ = inverted_; + cloned->applyLog_ = applyLog_; return cloned.release(); } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2D/FloatTextureSceneLayer.h --- a/Framework/Scene2D/FloatTextureSceneLayer.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2D/FloatTextureSceneLayer.h Tue Feb 04 15:24:03 2020 +0100 @@ -32,6 +32,7 @@ float customCenter_; float customWidth_; bool inverted_; + bool applyLog_; public: // The pixel format must be convertible to "Float32" @@ -60,6 +61,13 @@ void FitRange(); + void SetApplyLog(bool apply); + + bool IsApplyLog() const + { + return applyLog_; + } + virtual ISceneLayer* Clone() const; virtual Type GetType() const diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2D/GrayscaleStyleConfigurator.cpp --- a/Framework/Scene2D/GrayscaleStyleConfigurator.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2D/GrayscaleStyleConfigurator.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -27,6 +27,18 @@ namespace OrthancStone { + GrayscaleStyleConfigurator::GrayscaleStyleConfigurator() : + revision_(0), + linearInterpolation_(false), + hasWindowingOverride_(false), + customWindowWidth_(0), + customWindowCenter_(0), + hasInversionOverride_(false), + inverted_(false), + applyLog_(false) + { + } + void GrayscaleStyleConfigurator::SetWindowing(ImageWindowing windowing) { hasWindowingOverride_ = true; @@ -60,6 +72,12 @@ revision_++; } + void GrayscaleStyleConfigurator::SetApplyLog(bool apply) + { + applyLog_ = apply; + revision_++; + } + TextureBaseSceneLayer* GrayscaleStyleConfigurator::CreateTextureFromImage( const Orthanc::ImageAccessor& image) const { @@ -100,9 +118,12 @@ l.SetCustomWindowing(customWindowCenter_, customWindowWidth_); } } + if (hasInversionOverride_) { l.SetInverted(inverted_); } + + l.SetApplyLog(applyLog_); } } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2D/GrayscaleStyleConfigurator.h --- a/Framework/Scene2D/GrayscaleStyleConfigurator.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2D/GrayscaleStyleConfigurator.h Tue Feb 04 15:24:03 2020 +0100 @@ -40,18 +40,10 @@ float customWindowCenter_; bool hasInversionOverride_; bool inverted_; + bool applyLog_; public: - GrayscaleStyleConfigurator() : - revision_(0), - linearInterpolation_(false), - hasWindowingOverride_(false), - customWindowWidth_(0), - customWindowCenter_(0), - hasInversionOverride_(false), - inverted_(false) - { - } + GrayscaleStyleConfigurator(); void SetWindowing(ImageWindowing windowing); @@ -68,6 +60,13 @@ return linearInterpolation_; } + void SetApplyLog(bool apply); + + bool IsApplyLog() const + { + return applyLog_; + } + virtual uint64_t GetRevision() const { return revision_; diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2D/ICompositor.h --- a/Framework/Scene2D/ICompositor.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2D/ICompositor.h Tue Feb 04 15:24:03 2020 +0100 @@ -1,8 +1,9 @@ #pragma once -#include +#include "Scene2D.h" +#include "ScenePoint2D.h" + #include -#include namespace OrthancStone { @@ -17,7 +18,14 @@ virtual unsigned int GetCanvasHeight() const = 0; - virtual void Refresh() = 0; + /** + * WARNING: "Refresh()" must always be called with the same + * scene. If the scene changes, a call to "ResetScene()" must be + * done to reset the tracking of the revisions of the layers. + **/ + virtual void Refresh(const Scene2D& scene) = 0; + + virtual void ResetScene() = 0; #if ORTHANC_ENABLE_LOCALE == 1 virtual void SetFont(size_t index, @@ -25,5 +33,18 @@ unsigned int fontSize, Orthanc::Encoding codepage) = 0; #endif + + // Get the center of the given pixel, in canvas coordinates + ScenePoint2D GetPixelCenterCoordinates(int x, int y) const + { + return ScenePoint2D( + static_cast(x) + 0.5 - static_cast(GetCanvasWidth()) / 2.0, + static_cast(y) + 0.5 - static_cast(GetCanvasHeight()) / 2.0); + } + + void FitContent(Scene2D& scene) const + { + scene.FitContent(GetCanvasWidth(), GetCanvasHeight()); + } }; } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2D/Internals/CairoFloatTextureRenderer.cpp --- a/Framework/Scene2D/Internals/CairoFloatTextureRenderer.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2D/Internals/CairoFloatTextureRenderer.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -53,6 +53,8 @@ target.GetFormat() == Orthanc::PixelFormat_BGRA32 && sizeof(float) == 4); + static const float LOG_NORMALIZATION = 255.0f / log(1.0f + 255.0f); + for (unsigned int y = 0; y < height; y++) { const float* p = reinterpret_cast(source.GetConstRow(y)); @@ -70,6 +72,14 @@ v = 255; } + if (l.IsApplyLog()) + { + // https://theailearner.com/2019/01/01/log-transformation/ + v = LOG_NORMALIZATION * log(1.0f + static_cast(v)); + } + + assert(v >= 0.0f && v <= 255.0f); + uint8_t vv = static_cast(v); if (l.IsInverted()) diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2D/Internals/CairoLookupTableTextureRenderer.cpp --- a/Framework/Scene2D/Internals/CairoLookupTableTextureRenderer.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2D/Internals/CairoLookupTableTextureRenderer.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -37,18 +37,6 @@ textureTransform_ = l.GetTransform(); isLinearInterpolation_ = l.IsLinearInterpolation(); - const float a = l.GetMinValue(); - float slope; - - if (l.GetMinValue() >= l.GetMaxValue()) - { - slope = 0; - } - else - { - slope = 256.0f / (l.GetMaxValue() - l.GetMinValue()); - } - const Orthanc::ImageAccessor& source = l.GetTexture(); const unsigned int width = source.GetWidth(); const unsigned int height = source.GetHeight(); @@ -56,46 +44,8 @@ Orthanc::ImageAccessor target; texture_.GetWriteableAccessor(target); - - const std::vector& lut = l.GetLookupTable(); - if (lut.size() != 4 * 256) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - - assert(source.GetFormat() == Orthanc::PixelFormat_Float32 && - target.GetFormat() == Orthanc::PixelFormat_BGRA32 && - sizeof(float) == 4); - - for (unsigned int y = 0; y < height; y++) - { - const float* p = reinterpret_cast(source.GetConstRow(y)); - uint8_t* q = reinterpret_cast(target.GetRow(y)); - - for (unsigned int x = 0; x < width; x++) - { - float v = (*p - a) * slope; - if (v <= 0) - { - v = 0; - } - else if (v >= 255) - { - v = 255; - } - - uint8_t vv = static_cast(v); - - q[0] = lut[4 * vv + 2]; // B - q[1] = lut[4 * vv + 1]; // G - q[2] = lut[4 * vv + 0]; // R - q[3] = lut[4 * vv + 3]; // A - - p++; - q += 4; - } - } - + l.Render(target); + cairo_surface_mark_dirty(texture_.GetObject()); } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2D/Internals/CompositorHelper.cpp --- a/Framework/Scene2D/Internals/CompositorHelper.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2D/Internals/CompositorHelper.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -80,12 +80,13 @@ }; - void CompositorHelper::Visit(const ISceneLayer& layer, + void CompositorHelper::Visit(const Scene2D& scene, + const ISceneLayer& layer, uint64_t layerIdentifier, int depth) { // "Visit()" is only applied to layers existing in the scene - assert(scene_.HasLayer(depth)); + assert(scene.HasLayer(depth)); Content::iterator found = content_.find(depth); @@ -115,7 +116,7 @@ { // This layer has already been rendered assert(found->second->GetLastRevision() <= layer.GetRevision()); - + if (found->second->GetLastRevision() < layer.GetRevision()) { found->second->UpdateRenderer(); @@ -141,18 +142,34 @@ } - void CompositorHelper::Refresh(unsigned int canvasWidth, + void CompositorHelper::Refresh(const Scene2D& scene, + unsigned int canvasWidth, unsigned int canvasHeight) { + /** + * Safeguard mechanism to enforce the fact that the same scene + * is always used with the compositor. Note that the safeguard + * is not 100% bullet-proof, as a new scene might reuse the same + * address as a previous scene. + **/ + if (lastScene_ != NULL && + lastScene_ != &scene) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, + "ICompositor::ResetScene() should have been called"); + } + + lastScene_ = &scene; + // Bring coordinate (0,0) to the center of the canvas AffineTransform2D offset = AffineTransform2D::CreateOffset( static_cast(canvasWidth) / 2.0, static_cast(canvasHeight) / 2.0); - sceneTransform_ = AffineTransform2D::Combine(offset, scene_.GetSceneToCanvasTransform()); + sceneTransform_ = AffineTransform2D::Combine(offset, scene.GetSceneToCanvasTransform()); canvasWidth_ = canvasWidth; canvasHeight_ = canvasHeight; - scene_.Apply(*this); + scene.Apply(*this); } } } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2D/Internals/CompositorHelper.h --- a/Framework/Scene2D/Internals/CompositorHelper.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2D/Internals/CompositorHelper.h Tue Feb 04 15:24:03 2020 +0100 @@ -64,9 +64,9 @@ typedef std::map Content; - const Scene2D& scene_; IRendererFactory& factory_; Content content_; + const Scene2D* lastScene_; // This is only a safeguard, don't use it! // Only valid during a call to Refresh() AffineTransform2D sceneTransform_; @@ -74,21 +74,22 @@ unsigned int canvasHeight_; protected: - virtual void Visit(const ISceneLayer& layer, + virtual void Visit(const Scene2D& scene, + const ISceneLayer& layer, uint64_t layerIdentifier, int depth); public: - CompositorHelper(const Scene2D& scene, - IRendererFactory& factory) : - scene_(scene), - factory_(factory) + CompositorHelper(IRendererFactory& factory) : + factory_(factory), + lastScene_(NULL) { } ~CompositorHelper(); - void Refresh(unsigned int canvasWidth, + void Refresh(const Scene2D& scene, + unsigned int canvasWidth, unsigned int canvasHeight); }; } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2D/Internals/OpenGLFloatTextureRenderer.cpp --- a/Framework/Scene2D/Internals/OpenGLFloatTextureRenderer.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2D/Internals/OpenGLFloatTextureRenderer.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -21,6 +21,8 @@ #include "OpenGLFloatTextureRenderer.h" +#include + namespace OrthancStone { namespace Internals @@ -32,6 +34,11 @@ { if (loadTexture) { + if (layer.IsApplyLog()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + context_.MakeCurrent(); texture_.reset(new OpenGLFloatTextureProgram::Data( context_, layer.GetTexture(), layer.IsLinearInterpolation())); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2D/Internals/OpenGLLookupTableTextureRenderer.cpp --- a/Framework/Scene2D/Internals/OpenGLLookupTableTextureRenderer.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2D/Internals/OpenGLLookupTableTextureRenderer.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -36,74 +36,14 @@ const unsigned int width = source.GetWidth(); const unsigned int height = source.GetHeight(); - if ((texture_.get() == NULL) || - (texture_->GetWidth() != width) || - (texture_->GetHeight() != height)) + if (texture_.get() == NULL || + texture_->GetWidth() != width || + texture_->GetHeight() != height) { - - texture_.reset(new Orthanc::Image( - Orthanc::PixelFormat_RGBA32, - width, - height, - false)); + texture_.reset(new Orthanc::Image(Orthanc::PixelFormat_RGBA32, width, height, false)); } - { - - const float a = layer.GetMinValue(); - float slope = 0; - - if (layer.GetMinValue() >= layer.GetMaxValue()) - { - slope = 0; - } - else - { - slope = 256.0f / (layer.GetMaxValue() - layer.GetMinValue()); - } - - Orthanc::ImageAccessor target; - texture_->GetWriteableAccessor(target); - - const std::vector& lut = layer.GetLookupTable(); - if (lut.size() != 4 * 256) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - - assert(source.GetFormat() == Orthanc::PixelFormat_Float32 && - target.GetFormat() == Orthanc::PixelFormat_RGBA32 && - sizeof(float) == 4); - - for (unsigned int y = 0; y < height; y++) - { - const float* p = reinterpret_cast(source.GetConstRow(y)); - uint8_t* q = reinterpret_cast(target.GetRow(y)); - - for (unsigned int x = 0; x < width; x++) - { - float v = (*p - a) * slope; - if (v <= 0) - { - v = 0; - } - else if (v >= 255) - { - v = 255; - } - - uint8_t vv = static_cast(v); - - q[0] = lut[4 * vv + 0]; // R - q[1] = lut[4 * vv + 1]; // G - q[2] = lut[4 * vv + 2]; // B - q[3] = lut[4 * vv + 3]; // A - - p++; - q += 4; - } - } - } + layer.Render(*texture_); context_.MakeCurrent(); glTexture_.reset(new OpenGL::OpenGLTexture(context_)); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2D/LookupTableStyleConfigurator.cpp --- a/Framework/Scene2D/LookupTableStyleConfigurator.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2D/LookupTableStyleConfigurator.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -39,7 +39,8 @@ LookupTableStyleConfigurator::LookupTableStyleConfigurator() : revision_(0), hasLut_(false), - hasRange_(false) + hasRange_(false), + applyLog_(false) { } @@ -82,6 +83,12 @@ } } + void LookupTableStyleConfigurator::SetApplyLog(bool apply) + { + applyLog_ = apply; + revision_++; + } + TextureBaseSceneLayer* LookupTableStyleConfigurator::CreateTextureFromImage(const Orthanc::ImageAccessor& image) const { throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); @@ -104,5 +111,7 @@ { l.FitRange(); } + + l.SetApplyLog(applyLog_); } } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2D/LookupTableStyleConfigurator.h --- a/Framework/Scene2D/LookupTableStyleConfigurator.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2D/LookupTableStyleConfigurator.h Tue Feb 04 15:24:03 2020 +0100 @@ -39,6 +39,7 @@ bool hasRange_; float minValue_; float maxValue_; + bool applyLog_; public: LookupTableStyleConfigurator(); @@ -55,6 +56,13 @@ void SetRange(float minValue, float maxValue); + void SetApplyLog(bool apply); + + bool IsApplyLog() const + { + return applyLog_; + } + virtual uint64_t GetRevision() const { return revision_; @@ -63,7 +71,7 @@ virtual TextureBaseSceneLayer* CreateTextureFromImage(const Orthanc::ImageAccessor& image) const; virtual TextureBaseSceneLayer* CreateTextureFromDicom(const Orthanc::ImageAccessor& frame, - const DicomInstanceParameters& parameters) const + const DicomInstanceParameters& parameters) const { return parameters.CreateLookupTableTexture(frame); } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2D/LookupTableTextureSceneLayer.cpp --- a/Framework/Scene2D/LookupTableTextureSceneLayer.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2D/LookupTableTextureSceneLayer.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -27,7 +27,8 @@ namespace OrthancStone { - LookupTableTextureSceneLayer::LookupTableTextureSceneLayer(const Orthanc::ImageAccessor& texture) + LookupTableTextureSceneLayer::LookupTableTextureSceneLayer(const Orthanc::ImageAccessor& texture) : + applyLog_(false) { { std::auto_ptr t( @@ -132,6 +133,12 @@ } } + void LookupTableTextureSceneLayer::SetApplyLog(bool apply) + { + applyLog_ = apply; + IncrementRevision(); + } + void LookupTableTextureSceneLayer::FitRange() { Orthanc::ImageProcessing::GetMinMaxFloatValue(minValue_, maxValue_, GetTexture()); @@ -158,4 +165,147 @@ return cloned.release(); } + + + // Templatized function to speed up computations, by avoiding + // testing conditions on each pixel + template + static void RenderInternal(Orthanc::ImageAccessor& target, + const Orthanc::ImageAccessor& source, + float minValue, + float slope, + const std::vector& lut) + { + static const float LOG_NORMALIZATION = 255.0f / log(1.0f + 255.0f); + + const unsigned int width = source.GetWidth(); + const unsigned int height = source.GetHeight(); + + for (unsigned int y = 0; y < height; y++) + { + const float* p = reinterpret_cast(source.GetConstRow(y)); + uint8_t* q = reinterpret_cast(target.GetRow(y)); + + for (unsigned int x = 0; x < width; x++) + { + float v = (*p - minValue) * slope; + if (v <= 0) + { + v = 0; + } + else if (v >= 255) + { + v = 255; + } + + if (IsApplyLog) + { + // https://theailearner.com/2019/01/01/log-transformation/ + v = LOG_NORMALIZATION * log(1.0f + static_cast(v)); + } + + assert(v >= 0.0f && v <= 255.0f); + + uint8_t vv = static_cast(v); + + switch (TargetFormat) + { + case Orthanc::PixelFormat_BGRA32: + // For Cairo surfaces + q[0] = lut[4 * vv + 2]; // B + q[1] = lut[4 * vv + 1]; // G + q[2] = lut[4 * vv + 0]; // R + q[3] = lut[4 * vv + 3]; // A + break; + + case Orthanc::PixelFormat_RGBA32: + // For OpenGL + q[0] = lut[4 * vv + 0]; // R + q[1] = lut[4 * vv + 1]; // G + q[2] = lut[4 * vv + 2]; // B + q[3] = lut[4 * vv + 3]; // A + break; + + default: + assert(0); + } + + p++; + q += 4; + } + } + } + + + void LookupTableTextureSceneLayer::Render(Orthanc::ImageAccessor& target) const + { + assert(sizeof(float) == 4); + + if (!HasTexture()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + const Orthanc::ImageAccessor& source = GetTexture(); + + if (source.GetFormat() != Orthanc::PixelFormat_Float32 || + (target.GetFormat() != Orthanc::PixelFormat_RGBA32 && + target.GetFormat() != Orthanc::PixelFormat_BGRA32)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat); + } + + if (source.GetWidth() != target.GetWidth() || + source.GetHeight() != target.GetHeight()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize); + } + + const float minValue = GetMinValue(); + float slope; + + if (GetMinValue() >= GetMaxValue()) + { + slope = 0; + } + else + { + slope = 256.0f / (GetMaxValue() - GetMinValue()); + } + + const std::vector& lut = GetLookupTable(); + if (lut.size() != 4 * 256) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + switch (target.GetFormat()) + { + case Orthanc::PixelFormat_RGBA32: + if (applyLog_) + { + RenderInternal(target, source, minValue, slope, lut); + } + else + { + RenderInternal(target, source, minValue, slope, lut); + } + break; + + case Orthanc::PixelFormat_BGRA32: + if (applyLog_) + { + RenderInternal(target, source, minValue, slope, lut); + } + else + { + RenderInternal(target, source, minValue, slope, lut); + } + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2D/LookupTableTextureSceneLayer.h --- a/Framework/Scene2D/LookupTableTextureSceneLayer.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2D/LookupTableTextureSceneLayer.h Tue Feb 04 15:24:03 2020 +0100 @@ -32,6 +32,7 @@ float minValue_; float maxValue_; std::vector lut_; + bool applyLog_; void SetLookupTableRgb(const std::vector& lut); @@ -66,11 +67,22 @@ return lut_; } + void SetApplyLog(bool apply); + + bool IsApplyLog() const + { + return applyLog_; + } + virtual ISceneLayer* Clone() const; virtual Type GetType() const { return Type_LookupTableTexture; } + + // Render the texture to a color image of format BGRA32 (Cairo + // surfaces) or RGBA32 (OpenGL) + void Render(Orthanc::ImageAccessor& target) const; }; } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2D/OpenGLCompositor.cpp --- a/Framework/Scene2D/OpenGLCompositor.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2D/OpenGLCompositor.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -128,10 +128,8 @@ } } - OpenGLCompositor::OpenGLCompositor(OpenGL::IOpenGLContext& context, - const Scene2D& scene) : + OpenGLCompositor::OpenGLCompositor(OpenGL::IOpenGLContext& context) : context_(context), - helper_(scene, *this), colorTextureProgram_(context), floatTextureProgram_(context), linesProgram_(context), @@ -139,6 +137,13 @@ canvasWidth_(0), canvasHeight_(0) { + if (!context_.IsContextLost()) + { + canvasWidth_ = context_.GetCanvasWidth(); + canvasHeight_ = context_.GetCanvasHeight(); + } + + ResetScene(); } OpenGLCompositor::~OpenGLCompositor() @@ -154,7 +159,7 @@ } } - void OpenGLCompositor::Refresh() + void OpenGLCompositor::Refresh(const Scene2D& scene) { if (!context_.IsContextLost()) { @@ -167,11 +172,10 @@ glClearColor(0, 0, 0, 1); glClear(GL_COLOR_BUFFER_BIT); - helper_.Refresh(canvasWidth_, canvasHeight_); + helper_->Refresh(scene, canvasWidth_, canvasHeight_); context_.SwapBuffer(); } - } void OpenGLCompositor::SetFont(size_t index, diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2D/OpenGLCompositor.h --- a/Framework/Scene2D/OpenGLCompositor.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2D/OpenGLCompositor.h Tue Feb 04 15:24:03 2020 +0100 @@ -37,27 +37,31 @@ typedef std::map Fonts; - OpenGL::IOpenGLContext& context_; - Fonts fonts_; - Internals::CompositorHelper helper_; - Internals::OpenGLColorTextureProgram colorTextureProgram_; - Internals::OpenGLFloatTextureProgram floatTextureProgram_; - Internals::OpenGLLinesProgram linesProgram_; - Internals::OpenGLTextProgram textProgram_; - unsigned int canvasWidth_; - unsigned int canvasHeight_; + OpenGL::IOpenGLContext& context_; + Fonts fonts_; + std::auto_ptr helper_; + Internals::OpenGLColorTextureProgram colorTextureProgram_; + Internals::OpenGLFloatTextureProgram floatTextureProgram_; + Internals::OpenGLLinesProgram linesProgram_; + Internals::OpenGLTextProgram textProgram_; + unsigned int canvasWidth_; + unsigned int canvasHeight_; const Font* GetFont(size_t fontIndex) const; virtual Internals::CompositorHelper::ILayerRenderer* Create(const ISceneLayer& layer) ORTHANC_OVERRIDE; public: - OpenGLCompositor(OpenGL::IOpenGLContext& context, - const Scene2D& scene); + OpenGLCompositor(OpenGL::IOpenGLContext& context); virtual ~OpenGLCompositor(); - virtual void Refresh() ORTHANC_OVERRIDE; + virtual void Refresh(const Scene2D& scene) ORTHANC_OVERRIDE; + + virtual void ResetScene() ORTHANC_OVERRIDE + { + helper_.reset(new Internals::CompositorHelper(*this)); + } void SetFont(size_t index, const GlyphBitmapAlphabet& dict); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2D/PointerEvent.cpp --- a/Framework/Scene2D/PointerEvent.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2D/PointerEvent.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -26,6 +26,7 @@ namespace OrthancStone { PointerEvent::PointerEvent() : + button_(MouseButton_None), hasAltModifier_(false), hasControlModifier_(false), hasShiftModifier_(false) diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2D/PointerEvent.h --- a/Framework/Scene2D/PointerEvent.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2D/PointerEvent.h Tue Feb 04 15:24:03 2020 +0100 @@ -31,6 +31,7 @@ class PointerEvent : public boost::noncopyable { private: + MouseButton button_; std::vector positions_; bool hasAltModifier_; bool hasControlModifier_; @@ -88,5 +89,15 @@ { return hasShiftModifier_; } + + void SetMouseButton(MouseButton button) + { + button_ = button; + } + + MouseButton GetMouseButton() const + { + return button_; + } }; } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2D/Scene2D.cpp --- a/Framework/Scene2D/Scene2D.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2D/Scene2D.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -208,7 +208,7 @@ it != content_.end(); ++it) { assert(it->second != NULL); - visitor.Visit(it->second->GetLayer(), it->second->GetIdentifier(), it->first); + visitor.Visit(*this, it->second->GetLayer(), it->second->GetIdentifier(), it->first); } } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2D/Scene2D.h --- a/Framework/Scene2D/Scene2D.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2D/Scene2D.h Tue Feb 04 15:24:03 2020 +0100 @@ -40,7 +40,8 @@ { } - virtual void Visit(const ISceneLayer& layer, + virtual void Visit(const Scene2D& scene, + const ISceneLayer& layer, uint64_t layerIdentifier, int depth) = 0; }; diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2D/ScenePoint2D.h --- a/Framework/Scene2D/ScenePoint2D.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2D/ScenePoint2D.h Tue Feb 04 15:24:03 2020 +0100 @@ -64,115 +64,115 @@ return ScenePoint2D(x, y); } - const ScenePoint2D operator-(const ScenePoint2D& a) const - { - ScenePoint2D v; - v.x_ = x_ - a.x_; - v.y_ = y_ - a.y_; - - return v; - } - - const ScenePoint2D operator+(const ScenePoint2D& a) const - { - ScenePoint2D v; - v.x_ = x_ + a.x_; - v.y_ = y_ + a.y_; - - return v; - } - - const ScenePoint2D operator*(double a) const - { - ScenePoint2D v; - v.x_ = x_ * a; - v.y_ = y_ * a; - - return v; - } - - const ScenePoint2D operator/(double a) const - { - ScenePoint2D v; - v.x_ = x_ / a; - v.y_ = y_ / a; - - return v; - } - - static void MidPoint(ScenePoint2D& result, const ScenePoint2D& a, const ScenePoint2D& b) - { - result.x_ = 0.5 * (a.x_ + b.x_); - result.y_ = 0.5 * (a.y_ + b.y_); - } - - static double Dot(const ScenePoint2D& a, const ScenePoint2D& b) - { - return a.x_ * b.x_ + a.y_ * b.y_; - } + const ScenePoint2D operator-(const ScenePoint2D& a) const + { + ScenePoint2D v; + v.x_ = x_ - a.x_; + v.y_ = y_ - a.y_; + + return v; + } + + const ScenePoint2D operator+(const ScenePoint2D& a) const + { + ScenePoint2D v; + v.x_ = x_ + a.x_; + v.y_ = y_ + a.y_; + + return v; + } + + const ScenePoint2D operator*(double a) const + { + ScenePoint2D v; + v.x_ = x_ * a; + v.y_ = y_ * a; - static double SquaredMagnitude(const ScenePoint2D& v) - { - return v.x_ * v.x_ + v.y_ * v.y_; - } + return v; + } + + const ScenePoint2D operator/(double a) const + { + ScenePoint2D v; + v.x_ = x_ / a; + v.y_ = y_ / a; + + return v; + } + + static void MidPoint(ScenePoint2D& result, const ScenePoint2D& a, const ScenePoint2D& b) + { + result.x_ = 0.5 * (a.x_ + b.x_); + result.y_ = 0.5 * (a.y_ + b.y_); + } + + static double Dot(const ScenePoint2D& a, const ScenePoint2D& b) + { + return a.x_ * b.x_ + a.y_ * b.y_; + } + + static double SquaredMagnitude(const ScenePoint2D& v) + { + return v.x_ * v.x_ + v.y_ * v.y_; + } - static double Magnitude(const ScenePoint2D& v) - { - double squaredMagnitude = SquaredMagnitude(v); - if (LinearAlgebra::IsCloseToZero(squaredMagnitude)) - return 0.0; - return sqrt(squaredMagnitude); - } + static double Magnitude(const ScenePoint2D& v) + { + double squaredMagnitude = SquaredMagnitude(v); + if (LinearAlgebra::IsCloseToZero(squaredMagnitude)) + return 0.0; + return sqrt(squaredMagnitude); + } - static double SquaredDistancePtPt(const ScenePoint2D& a, const ScenePoint2D& b) - { - ScenePoint2D n = b - a; - return Dot(n, n); - } + static double SquaredDistancePtPt(const ScenePoint2D& a, const ScenePoint2D& b) + { + ScenePoint2D n = b - a; + return Dot(n, n); + } - static double DistancePtPt(const ScenePoint2D& a, const ScenePoint2D& b) - { - double squaredDist = SquaredDistancePtPt(a, b); - return sqrt(squaredDist); - } + static double DistancePtPt(const ScenePoint2D& a, const ScenePoint2D& b) + { + double squaredDist = SquaredDistancePtPt(a, b); + return sqrt(squaredDist); + } + + /** + Distance from point p to [a,b] segment - /** - Distance from point p to [a,b] segment - - Rewritten from https://www.randygaul.net/2014/07/23/distance-point-to-line-segment/ - */ - static double SquaredDistancePtSegment(const ScenePoint2D& a, const ScenePoint2D& b, const ScenePoint2D& p) - { - ScenePoint2D n = b - a; - ScenePoint2D pa = a - p; - - double c = Dot(n, pa); - - // Closest point is a - if (c > 0.0) - return Dot(pa, pa); - - ScenePoint2D bp = p - b; - - // Closest point is b - if (Dot(n, bp) > 0.0) - return Dot(bp, bp); - - // if segment length is very short, we approximate distance to the - // distance with a - double nq = Dot(n, n); - if (LinearAlgebra::IsCloseToZero(nq)) - { - // segment is very small: approximate distance from point to segment - // with distance from p to a - return Dot(pa, pa); - } - else - { - // Closest point is between a and b - ScenePoint2D e = pa - n * (c / nq); - return Dot(e, e); - } + Rewritten from https://www.randygaul.net/2014/07/23/distance-point-to-line-segment/ + */ + static double SquaredDistancePtSegment(const ScenePoint2D& a, const ScenePoint2D& b, const ScenePoint2D& p) + { + ScenePoint2D n = b - a; + ScenePoint2D pa = a - p; + + double c = Dot(n, pa); + + // Closest point is a + if (c > 0.0) + return Dot(pa, pa); + + ScenePoint2D bp = p - b; + + // Closest point is b + if (Dot(n, bp) > 0.0) + return Dot(bp, bp); + + // if segment length is very short, we approximate distance to the + // distance with a + double nq = Dot(n, n); + if (LinearAlgebra::IsCloseToZero(nq)) + { + // segment is very small: approximate distance from point to segment + // with distance from p to a + return Dot(pa, pa); + } + else + { + // Closest point is between a and b + ScenePoint2D e = pa - n * (c / nq); + return Dot(e, e); + } } }; } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/AngleMeasureTool.cpp --- a/Framework/Scene2DViewport/AngleMeasureTool.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/AngleMeasureTool.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -42,8 +42,8 @@ // the params in the LayerHolder ctor specify the number of polyline and text // layers AngleMeasureTool::AngleMeasureTool( - MessageBroker& broker, boost::weak_ptr controllerW) - : MeasureTool(broker, controllerW) + boost::weak_ptr controllerW) + : MeasureTool(controllerW) #if ORTHANC_STONE_ENABLE_OUTLINED_TEXT == 1 , layerHolder_(boost::make_shared(controllerW,1,5)) #else @@ -131,8 +131,8 @@ AngleMeasureTool::AngleHighlightArea AngleMeasureTool::AngleHitTest(ScenePoint2D p) const { - const double pixelToScene = - GetController()->GetScene().GetCanvasToSceneTransform().ComputeZoom(); + const double pixelToScene = GetController()->GetScene().GetCanvasToSceneTransform().ComputeZoom(); + const double SQUARED_HIT_TEST_MAX_DISTANCE_SCENE_COORD = pixelToScene * HIT_TEST_MAX_DISTANCE_CANVAS_COORD * pixelToScene * HIT_TEST_MAX_DISTANCE_CANVAS_COORD; { @@ -189,8 +189,9 @@ boost::weak_ptr controllerW, const PointerEvent & e); */ + boost::shared_ptr editAngleMeasureTracker( - new EditAngleMeasureTracker(shared_from_this(), GetBroker(), GetController(), e)); + new EditAngleMeasureTracker(shared_from_this(), GetController(), e)); return editAngleMeasureTracker; } @@ -212,72 +213,75 @@ { // Fill the polyline layer with the measurement lines PolylineSceneLayer* polylineLayer = layerHolder_->GetPolylineLayer(0); - polylineLayer->ClearAllChains(); + if (polylineLayer) + { + polylineLayer->ClearAllChains(); + + const Color color(TOOL_ANGLE_LINES_COLOR_RED, TOOL_ANGLE_LINES_COLOR_GREEN, TOOL_ANGLE_LINES_COLOR_BLUE); + const Color highlightColor(TOOL_ANGLE_LINES_HL_COLOR_RED, TOOL_ANGLE_LINES_HL_COLOR_GREEN, TOOL_ANGLE_LINES_HL_COLOR_BLUE); + + // sides + { + { + PolylineSceneLayer::Chain chain; + chain.push_back(side1End_); + chain.push_back(center_); - const Color color(TOOL_ANGLE_LINES_COLOR_RED, TOOL_ANGLE_LINES_COLOR_GREEN, TOOL_ANGLE_LINES_COLOR_BLUE); - const Color highlightColor(TOOL_ANGLE_LINES_HL_COLOR_RED, TOOL_ANGLE_LINES_HL_COLOR_GREEN, TOOL_ANGLE_LINES_HL_COLOR_BLUE); + if ((angleHighlightArea_ == AngleHighlightArea_Side1) || (angleHighlightArea_ == AngleHighlightArea_Side2)) + polylineLayer->AddChain(chain, false, highlightColor); + else + polylineLayer->AddChain(chain, false, color); + } + { + PolylineSceneLayer::Chain chain; + chain.push_back(side2End_); + chain.push_back(center_); + if ((angleHighlightArea_ == AngleHighlightArea_Side1) || (angleHighlightArea_ == AngleHighlightArea_Side2)) + polylineLayer->AddChain(chain, false, highlightColor); + else + polylineLayer->AddChain(chain, false, color); + } + } - // sides - { + // Create the handles + { + { + PolylineSceneLayer::Chain chain; + //TODO: take DPI into account + AddSquare(chain, controller->GetScene(), side1End_, + GetController()->GetHandleSideLengthS()); + + if (angleHighlightArea_ == AngleHighlightArea_Side1End) + polylineLayer->AddChain(chain, true, highlightColor); + else + polylineLayer->AddChain(chain, true, color); + + } + { + PolylineSceneLayer::Chain chain; + //TODO: take DPI into account + AddSquare(chain, controller->GetScene(), side2End_, + GetController()->GetHandleSideLengthS()); + + if (angleHighlightArea_ == AngleHighlightArea_Side2End) + polylineLayer->AddChain(chain, true, highlightColor); + else + polylineLayer->AddChain(chain, true, color); + } + } + + // Create the arc { PolylineSceneLayer::Chain chain; - chain.push_back(side1End_); - chain.push_back(center_); - if ((angleHighlightArea_ == AngleHighlightArea_Side1) || (angleHighlightArea_ == AngleHighlightArea_Side2)) - polylineLayer->AddChain(chain, false, highlightColor); - else - polylineLayer->AddChain(chain, false, color); - } - { - PolylineSceneLayer::Chain chain; - chain.push_back(side2End_); - chain.push_back(center_); - if ((angleHighlightArea_ == AngleHighlightArea_Side1) || (angleHighlightArea_ == AngleHighlightArea_Side2)) + AddShortestArc(chain, side1End_, center_, side2End_, + controller->GetAngleToolArcRadiusS()); + if (angleHighlightArea_ == AngleHighlightArea_Center) polylineLayer->AddChain(chain, false, highlightColor); else polylineLayer->AddChain(chain, false, color); } } - - // Create the handles - { - { - PolylineSceneLayer::Chain chain; - //TODO: take DPI into account - AddSquare(chain, GetController()->GetScene(), side1End_, - GetController()->GetHandleSideLengthS()); - - if (angleHighlightArea_ == AngleHighlightArea_Side1End) - polylineLayer->AddChain(chain, true, highlightColor); - else - polylineLayer->AddChain(chain, true, color); - - } - { - PolylineSceneLayer::Chain chain; - //TODO: take DPI into account - AddSquare(chain, GetController()->GetScene(), side2End_, - GetController()->GetHandleSideLengthS()); - - if (angleHighlightArea_ == AngleHighlightArea_Side2End) - polylineLayer->AddChain(chain, true, highlightColor); - else - polylineLayer->AddChain(chain, true, color); - } - } - - // Create the arc - { - PolylineSceneLayer::Chain chain; - - AddShortestArc(chain, side1End_, center_, side2End_, - controller->GetAngleToolArcRadiusS()); - if (angleHighlightArea_ == AngleHighlightArea_Center) - polylineLayer->AddChain(chain, false, highlightColor); - else - polylineLayer->AddChain(chain, false, color); - } } { // Set the text layer @@ -307,10 +311,10 @@ #if ORTHANC_STONE_ENABLE_OUTLINED_TEXT == 1 SetTextLayerOutlineProperties( - GetController()->GetScene(), layerHolder_, buf, ScenePoint2D(pointX, pointY), 0); + controller->GetScene(), layerHolder_, buf, ScenePoint2D(pointX, pointY), 0); #else SetTextLayerProperties( - GetController()->GetScene(), layerHolder_, buf, ScenePoint2D(pointX, pointY) , 0); + controller->GetScene(), layerHolder_, buf, ScenePoint2D(pointX, pointY) , 0); #endif #if 0 diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/AngleMeasureTool.h --- a/Framework/Scene2DViewport/AngleMeasureTool.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/AngleMeasureTool.h Tue Feb 04 15:24:03 2020 +0100 @@ -37,10 +37,10 @@ namespace OrthancStone { - class AngleMeasureTool : public MeasureTool, public boost::enable_shared_from_this + class AngleMeasureTool : public MeasureTool { public: - AngleMeasureTool(MessageBroker& broker, boost::weak_ptr controllerW); + AngleMeasureTool(boost::weak_ptr controllerW); ~AngleMeasureTool(); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/CreateAngleMeasureCommand.cpp --- a/Framework/Scene2DViewport/CreateAngleMeasureCommand.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/CreateAngleMeasureCommand.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -26,12 +26,11 @@ namespace OrthancStone { CreateAngleMeasureCommand::CreateAngleMeasureCommand( - MessageBroker& broker, boost::weak_ptr controllerW, ScenePoint2D point) : CreateMeasureCommand(controllerW) , measureTool_( - boost::make_shared(boost::ref(broker), controllerW)) + boost::make_shared(controllerW)) { GetController()->AddMeasureTool(measureTool_); measureTool_->SetSide1End(point); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/CreateAngleMeasureCommand.h --- a/Framework/Scene2DViewport/CreateAngleMeasureCommand.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/CreateAngleMeasureCommand.h Tue Feb 04 15:24:03 2020 +0100 @@ -28,7 +28,6 @@ public: /** Ctor sets end of side 1*/ CreateAngleMeasureCommand( - MessageBroker& broker, boost::weak_ptr controllerW, ScenePoint2D point); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/CreateAngleMeasureTracker.cpp --- a/Framework/Scene2DViewport/CreateAngleMeasureTracker.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/CreateAngleMeasureTracker.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -26,17 +26,22 @@ namespace OrthancStone { CreateAngleMeasureTracker::CreateAngleMeasureTracker( - MessageBroker& broker, boost::weak_ptr controllerW, const PointerEvent& e) : CreateMeasureTracker(controllerW) , state_(CreatingSide1) { - command_.reset( - new CreateAngleMeasureCommand( - broker, - controllerW, - e.GetMainPosition().Apply(GetScene().GetCanvasToSceneTransform()))); + ScenePoint2D point = e.GetMainPosition(); + + { + boost::shared_ptr controller = controllerW.lock(); + if (controller) + { + point = e.GetMainPosition().Apply(controller->GetScene().GetCanvasToSceneTransform()); + } + } + + command_.reset(new CreateAngleMeasureCommand(controllerW, point)); } CreateAngleMeasureTracker::~CreateAngleMeasureTracker() @@ -52,24 +57,28 @@ "PointerMove: active_ == false"); } - ScenePoint2D scenePos = event.GetMainPosition().Apply( - GetScene().GetCanvasToSceneTransform()); - - switch (state_) + boost::shared_ptr controller = controllerW_.lock(); + if (controller) { - case CreatingSide1: - GetCommand()->SetCenter(scenePos); - break; - case CreatingSide2: - GetCommand()->SetSide2End(scenePos); - break; - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, - "Wrong state in CreateAngleMeasureTracker::" - "PointerMove: state_ invalid"); + ScenePoint2D scenePos = event.GetMainPosition().Apply( + controller->GetScene().GetCanvasToSceneTransform()); + + switch (state_) + { + case CreatingSide1: + GetCommand()->SetCenter(scenePos); + break; + case CreatingSide2: + GetCommand()->SetSide2End(scenePos); + break; + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, + "Wrong state in CreateAngleMeasureTracker::" + "PointerMove: state_ invalid"); + } + //LOG(TRACE) << "scenePos.GetX() = " << scenePos.GetX() << " " << + // "scenePos.GetY() = " << scenePos.GetY(); } - //LOG(TRACE) << "scenePos.GetX() = " << scenePos.GetX() << " " << - // "scenePos.GetY() = " << scenePos.GetY(); } void CreateAngleMeasureTracker::PointerUp(const PointerEvent& e) diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/CreateAngleMeasureTracker.h --- a/Framework/Scene2DViewport/CreateAngleMeasureTracker.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/CreateAngleMeasureTracker.h Tue Feb 04 15:24:03 2020 +0100 @@ -38,7 +38,6 @@ must be supplied, too */ CreateAngleMeasureTracker( - MessageBroker& broker, boost::weak_ptr controllerW, const PointerEvent& e); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/CreateLineMeasureCommand.cpp --- a/Framework/Scene2DViewport/CreateLineMeasureCommand.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/CreateLineMeasureCommand.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -26,12 +26,11 @@ namespace OrthancStone { CreateLineMeasureCommand::CreateLineMeasureCommand( - MessageBroker& broker, boost::weak_ptr controllerW, ScenePoint2D point) : CreateMeasureCommand(controllerW) , measureTool_( - boost::make_shared(boost::ref(broker), controllerW)) + boost::make_shared(controllerW)) { GetController()->AddMeasureTool(measureTool_); measureTool_->Set(point, point); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/CreateLineMeasureCommand.h --- a/Framework/Scene2DViewport/CreateLineMeasureCommand.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/CreateLineMeasureCommand.h Tue Feb 04 15:24:03 2020 +0100 @@ -27,7 +27,6 @@ { public: CreateLineMeasureCommand( - MessageBroker& broker, boost::weak_ptr controllerW, ScenePoint2D point); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/CreateLineMeasureTracker.cpp --- a/Framework/Scene2DViewport/CreateLineMeasureTracker.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/CreateLineMeasureTracker.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -26,16 +26,21 @@ namespace OrthancStone { CreateLineMeasureTracker::CreateLineMeasureTracker( - MessageBroker& broker, boost::weak_ptr controllerW, const PointerEvent& e) : CreateMeasureTracker(controllerW) { - command_.reset( - new CreateLineMeasureCommand( - broker, - controllerW, - e.GetMainPosition().Apply(GetScene().GetCanvasToSceneTransform()))); + ScenePoint2D point = e.GetMainPosition(); + + { + boost::shared_ptr controller = controllerW.lock(); + if (controller) + { + point = e.GetMainPosition().Apply(controller->GetScene().GetCanvasToSceneTransform()); + } + } + + command_.reset(new CreateLineMeasureCommand(controllerW, point)); } CreateLineMeasureTracker::~CreateLineMeasureTracker() @@ -52,16 +57,20 @@ "PointerMove: active_ == false"); } - ScenePoint2D scenePos = event.GetMainPosition().Apply( - GetScene().GetCanvasToSceneTransform()); - - //LOG(TRACE) << "scenePos.GetX() = " << scenePos.GetX() << " " << - // "scenePos.GetY() = " << scenePos.GetY(); - - CreateLineMeasureTracker* concreteThis = - dynamic_cast(this); - assert(concreteThis != NULL); - GetCommand()->SetEnd(scenePos); + boost::shared_ptr controller = controllerW_.lock(); + if (controller) + { + ScenePoint2D scenePos = event.GetMainPosition().Apply( + controller->GetScene().GetCanvasToSceneTransform()); + + //LOG(TRACE) << "scenePos.GetX() = " << scenePos.GetX() << " " << + // "scenePos.GetY() = " << scenePos.GetY(); + + CreateLineMeasureTracker* concreteThis = + dynamic_cast(this); + assert(concreteThis != NULL); + GetCommand()->SetEnd(scenePos); + } } void CreateLineMeasureTracker::PointerUp(const PointerEvent& e) @@ -84,5 +93,4 @@ { return boost::dynamic_pointer_cast(command_); } - } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/CreateLineMeasureTracker.h --- a/Framework/Scene2DViewport/CreateLineMeasureTracker.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/CreateLineMeasureTracker.h Tue Feb 04 15:24:03 2020 +0100 @@ -38,7 +38,6 @@ must be supplied, too */ CreateLineMeasureTracker( - MessageBroker& broker, boost::weak_ptr controllerW, const PointerEvent& e); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/EditAngleMeasureCommand.cpp --- a/Framework/Scene2DViewport/EditAngleMeasureCommand.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/EditAngleMeasureCommand.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -23,8 +23,7 @@ namespace OrthancStone { EditAngleMeasureCommand::EditAngleMeasureCommand( - boost::shared_ptr measureTool, - MessageBroker& broker, + boost::shared_ptr measureTool, boost::weak_ptr controllerW) : EditMeasureCommand(measureTool, controllerW) , measureTool_(measureTool) @@ -33,21 +32,21 @@ void EditAngleMeasureCommand::SetCenter(ScenePoint2D scenePos) { - measureTool_->SetCenter(scenePos); + dynamic_cast(*measureTool_).SetCenter(scenePos); mementoModified_ = measureTool_->GetMemento(); } void EditAngleMeasureCommand::SetSide1End(ScenePoint2D scenePos) { - measureTool_->SetSide1End(scenePos); + dynamic_cast(*measureTool_).SetSide1End(scenePos); mementoModified_ = measureTool_->GetMemento(); } void EditAngleMeasureCommand::SetSide2End(ScenePoint2D scenePos) { - measureTool_->SetSide2End(scenePos); + dynamic_cast(*measureTool_).SetSide2End(scenePos); mementoModified_ = measureTool_->GetMemento(); } } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/EditAngleMeasureCommand.h --- a/Framework/Scene2DViewport/EditAngleMeasureCommand.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/EditAngleMeasureCommand.h Tue Feb 04 15:24:03 2020 +0100 @@ -28,8 +28,7 @@ public: /** Ctor sets end of side 1*/ EditAngleMeasureCommand( - boost::shared_ptr measureTool, - MessageBroker& broker, + boost::shared_ptr measureTool, boost::weak_ptr controllerW); /** This method sets center*/ @@ -46,6 +45,6 @@ { return measureTool_; } - boost::shared_ptr measureTool_; + boost::shared_ptr measureTool_; }; } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/EditAngleMeasureTracker.cpp --- a/Framework/Scene2DViewport/EditAngleMeasureTracker.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/EditAngleMeasureTracker.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -26,18 +26,22 @@ namespace OrthancStone { EditAngleMeasureTracker::EditAngleMeasureTracker( - boost::shared_ptr measureTool, - MessageBroker& broker, + boost::shared_ptr measureTool, boost::weak_ptr controllerW, const PointerEvent& e) : EditMeasureTracker(controllerW, e) { - ScenePoint2D scenePos = e.GetMainPosition().Apply( - GetScene().GetCanvasToSceneTransform()); + ScenePoint2D scenePos = e.GetMainPosition(); - modifiedZone_ = measureTool->AngleHitTest(scenePos); + boost::shared_ptr controller = controllerW_.lock(); + if (controller) + { + scenePos = e.GetMainPosition().Apply(controller->GetScene().GetCanvasToSceneTransform()); + } - command_.reset(new EditAngleMeasureCommand(measureTool, broker, controllerW)); + modifiedZone_ = dynamic_cast(*measureTool).AngleHitTest(scenePos); + + command_.reset(new EditAngleMeasureCommand(measureTool, controllerW)); } EditAngleMeasureTracker::~EditAngleMeasureTracker() @@ -47,50 +51,54 @@ void EditAngleMeasureTracker::PointerMove(const PointerEvent& e) { - ScenePoint2D scenePos = e.GetMainPosition().Apply( - GetScene().GetCanvasToSceneTransform()); - - ScenePoint2D delta = scenePos - GetOriginalClickPosition(); - - boost::shared_ptr memento = - boost::dynamic_pointer_cast(command_->mementoOriginal_); - - ORTHANC_ASSERT(memento.get() != NULL); - - switch (modifiedZone_) - { - case AngleMeasureTool::AngleHighlightArea_Center: - { - ScenePoint2D newCenter = memento->center_ + delta; - GetCommand()->SetCenter(newCenter); - } - break; - case AngleMeasureTool::AngleHighlightArea_Side1: - case AngleMeasureTool::AngleHighlightArea_Side2: + boost::shared_ptr controller = controllerW_.lock(); + if (controller) { - ScenePoint2D newCenter = memento->center_ + delta; - ScenePoint2D newSide1End = memento->side1End_ + delta; - ScenePoint2D newSide2End = memento->side2End_ + delta; - GetCommand()->SetCenter(newCenter); - GetCommand()->SetSide1End(newSide1End); - GetCommand()->SetSide2End(newSide2End); - } - break; - case AngleMeasureTool::AngleHighlightArea_Side1End: - { - ScenePoint2D newSide1End = memento->side1End_ + delta; - GetCommand()->SetSide1End(newSide1End); - } - break; - case AngleMeasureTool::AngleHighlightArea_Side2End: - { - ScenePoint2D newSide2End = memento->side2End_ + delta; - GetCommand()->SetSide2End(newSide2End); - } - break; - default: - LOG(WARNING) << "Warning: please retry the measuring tool editing operation!"; - break; + ScenePoint2D scenePos = e.GetMainPosition().Apply( + controller->GetScene().GetCanvasToSceneTransform()); + + ScenePoint2D delta = scenePos - GetOriginalClickPosition(); + + boost::shared_ptr memento = + boost::dynamic_pointer_cast(command_->mementoOriginal_); + + ORTHANC_ASSERT(memento.get() != NULL); + + switch (modifiedZone_) + { + case AngleMeasureTool::AngleHighlightArea_Center: + { + ScenePoint2D newCenter = memento->center_ + delta; + GetCommand()->SetCenter(newCenter); + } + break; + case AngleMeasureTool::AngleHighlightArea_Side1: + case AngleMeasureTool::AngleHighlightArea_Side2: + { + ScenePoint2D newCenter = memento->center_ + delta; + ScenePoint2D newSide1End = memento->side1End_ + delta; + ScenePoint2D newSide2End = memento->side2End_ + delta; + GetCommand()->SetCenter(newCenter); + GetCommand()->SetSide1End(newSide1End); + GetCommand()->SetSide2End(newSide2End); + } + break; + case AngleMeasureTool::AngleHighlightArea_Side1End: + { + ScenePoint2D newSide1End = memento->side1End_ + delta; + GetCommand()->SetSide1End(newSide1End); + } + break; + case AngleMeasureTool::AngleHighlightArea_Side2End: + { + ScenePoint2D newSide2End = memento->side2End_ + delta; + GetCommand()->SetSide2End(newSide2End); + } + break; + default: + LOG(WARNING) << "Warning: please retry the measuring tool editing operation!"; + break; + } } } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/EditAngleMeasureTracker.h --- a/Framework/Scene2DViewport/EditAngleMeasureTracker.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/EditAngleMeasureTracker.h Tue Feb 04 15:24:03 2020 +0100 @@ -37,8 +37,7 @@ must be supplied, too */ EditAngleMeasureTracker( - boost::shared_ptr measureTool, - MessageBroker& broker, + boost::shared_ptr measureTool, boost::weak_ptr controllerW, const PointerEvent& e); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/EditLineMeasureCommand.cpp --- a/Framework/Scene2DViewport/EditLineMeasureCommand.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/EditLineMeasureCommand.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -23,8 +23,7 @@ namespace OrthancStone { EditLineMeasureCommand::EditLineMeasureCommand( - boost::shared_ptr measureTool, - MessageBroker& broker, + boost::shared_ptr measureTool, boost::weak_ptr controllerW) : EditMeasureCommand(measureTool, controllerW) , measureTool_(measureTool) @@ -34,14 +33,14 @@ void EditLineMeasureCommand::SetStart(ScenePoint2D scenePos) { - measureTool_->SetStart(scenePos); + dynamic_cast(*measureTool_).SetStart(scenePos); mementoModified_ = measureTool_->GetMemento(); } void EditLineMeasureCommand::SetEnd(ScenePoint2D scenePos) { - measureTool_->SetEnd(scenePos); + dynamic_cast(*measureTool_).SetEnd(scenePos); mementoModified_ = measureTool_->GetMemento(); } } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/EditLineMeasureCommand.h --- a/Framework/Scene2DViewport/EditLineMeasureCommand.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/EditLineMeasureCommand.h Tue Feb 04 15:24:03 2020 +0100 @@ -27,8 +27,7 @@ { public: EditLineMeasureCommand( - boost::shared_ptr measureTool, - MessageBroker& broker, + boost::shared_ptr measureTool, boost::weak_ptr controllerW); void SetStart(ScenePoint2D scenePos); @@ -39,7 +38,6 @@ { return measureTool_; } - boost::shared_ptr measureTool_; + boost::shared_ptr measureTool_; }; } - diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/EditLineMeasureTracker.cpp --- a/Framework/Scene2DViewport/EditLineMeasureTracker.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/EditLineMeasureTracker.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -27,22 +27,24 @@ namespace OrthancStone { EditLineMeasureTracker::EditLineMeasureTracker( - boost::shared_ptr measureTool, - MessageBroker& broker, + boost::shared_ptr measureTool, boost::weak_ptr controllerW, const PointerEvent& e) : EditMeasureTracker(controllerW, e) { - ScenePoint2D scenePos = e.GetMainPosition().Apply( - GetScene().GetCanvasToSceneTransform()); - - modifiedZone_ = measureTool->LineHitTest(scenePos); + ScenePoint2D scenePos = e.GetMainPosition(); - command_.reset( - new EditLineMeasureCommand( - measureTool, - broker, - controllerW)); + { + boost::shared_ptr controller = controllerW.lock(); + if (controller) + { + scenePos = e.GetMainPosition().Apply(controller->GetScene().GetCanvasToSceneTransform()); + } + } + + modifiedZone_ = dynamic_cast(*measureTool).LineHitTest(scenePos); + + command_.reset(new EditLineMeasureCommand(measureTool, controllerW)); } EditLineMeasureTracker::~EditLineMeasureTracker() @@ -52,44 +54,48 @@ void EditLineMeasureTracker::PointerMove(const PointerEvent& e) { - ScenePoint2D scenePos = e.GetMainPosition().Apply( - GetScene().GetCanvasToSceneTransform()); - - ScenePoint2D delta = scenePos - GetOriginalClickPosition(); - - boost::shared_ptr memento = - boost::dynamic_pointer_cast(command_->mementoOriginal_); - - ORTHANC_ASSERT(memento.get() != NULL); - - switch (modifiedZone_) - { - case LineMeasureTool::LineHighlightArea_Start: + boost::shared_ptr controller = controllerW_.lock(); + if (controller) { - ScenePoint2D newStart = memento->start_ + delta; - GetCommand()->SetStart(newStart); - } - break; - case LineMeasureTool::LineHighlightArea_End: - { - ScenePoint2D newEnd = memento->end_ + delta; - GetCommand()->SetEnd(newEnd); - } - break; - case LineMeasureTool::LineHighlightArea_Segment: - { - ScenePoint2D newStart = memento->start_ + delta; - ScenePoint2D newEnd = memento->end_ + delta; - GetCommand()->SetStart(newStart); - GetCommand()->SetEnd(newEnd); - } - break; - default: - LOG(WARNING) << "Warning: please retry the measuring tool editing operation!"; + ScenePoint2D scenePos = e.GetMainPosition().Apply( + controller->GetScene().GetCanvasToSceneTransform()); + + ScenePoint2D delta = scenePos - GetOriginalClickPosition(); + + boost::shared_ptr memento = + boost::dynamic_pointer_cast(command_->mementoOriginal_); + + ORTHANC_ASSERT(memento.get() != NULL); + + switch (modifiedZone_) + { + case LineMeasureTool::LineHighlightArea_Start: + { + ScenePoint2D newStart = memento->start_ + delta; + GetCommand()->SetStart(newStart); + } break; + case LineMeasureTool::LineHighlightArea_End: + { + ScenePoint2D newEnd = memento->end_ + delta; + GetCommand()->SetEnd(newEnd); + } + break; + case LineMeasureTool::LineHighlightArea_Segment: + { + ScenePoint2D newStart = memento->start_ + delta; + ScenePoint2D newEnd = memento->end_ + delta; + GetCommand()->SetStart(newStart); + GetCommand()->SetEnd(newEnd); + } + break; + default: + LOG(WARNING) << "Warning: please retry the measuring tool editing operation!"; + break; + } } } - + void EditLineMeasureTracker::PointerUp(const PointerEvent& e) { alive_ = false; diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/EditLineMeasureTracker.h --- a/Framework/Scene2DViewport/EditLineMeasureTracker.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/EditLineMeasureTracker.h Tue Feb 04 15:24:03 2020 +0100 @@ -37,8 +37,7 @@ must be supplied, too */ EditLineMeasureTracker( - boost::shared_ptr measureTool, - MessageBroker& broker, + boost::shared_ptr measureTool, boost::weak_ptr controllerW, const PointerEvent& e); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/LayerHolder.cpp --- a/Framework/Scene2DViewport/LayerHolder.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/LayerHolder.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -43,24 +43,28 @@ void LayerHolder::CreateLayers() { - assert(baseLayerIndex_ == -1); + boost::shared_ptr controller = controllerW_.lock(); - baseLayerIndex_ = GetScene().GetMaxDepth() + 100; + if (controller) + { + assert(baseLayerIndex_ == -1); + + baseLayerIndex_ = controller->GetScene().GetMaxDepth() + 100; - for (int i = 0; i < polylineLayerCount_; ++i) - { - std::auto_ptr layer(new PolylineSceneLayer()); - GetScene().SetLayer(baseLayerIndex_ + i, layer.release()); + for (int i = 0; i < polylineLayerCount_; ++i) + { + std::auto_ptr layer(new PolylineSceneLayer()); + controller->GetScene().SetLayer(baseLayerIndex_ + i, layer.release()); + } + + for (int i = 0; i < textLayerCount_; ++i) + { + std::auto_ptr layer(new TextSceneLayer()); + controller->GetScene().SetLayer( + baseLayerIndex_ + polylineLayerCount_ + i, + layer.release()); + } } - - for (int i = 0; i < textLayerCount_; ++i) - { - std::auto_ptr layer(new TextSceneLayer()); - GetScene().SetLayer( - baseLayerIndex_ + polylineLayerCount_ + i, - layer.release()); - } - } void LayerHolder::CreateLayersIfNeeded() @@ -74,13 +78,6 @@ return (baseLayerIndex_ != -1); } - Scene2D& LayerHolder::GetScene() - { - boost::shared_ptr controller = controllerW_.lock(); - ORTHANC_ASSERT(controller.get() != 0, "Zombie attack!"); - return controller->GetScene(); - } - void LayerHolder::DeleteLayersIfNeeded() { if (baseLayerIndex_ != -1) @@ -89,42 +86,65 @@ void LayerHolder::DeleteLayers() { - for (int i = 0; i < textLayerCount_ + polylineLayerCount_; ++i) + boost::shared_ptr controller = controllerW_.lock(); + + if (controller) { - ORTHANC_ASSERT(GetScene().HasLayer(baseLayerIndex_ + i), "No layer"); - GetScene().DeleteLayer(baseLayerIndex_ + i); + for (int i = 0; i < textLayerCount_ + polylineLayerCount_; ++i) + { + ORTHANC_ASSERT(controller->GetScene().HasLayer(baseLayerIndex_ + i), "No layer"); + controller->GetScene().DeleteLayer(baseLayerIndex_ + i); + } + baseLayerIndex_ = -1; } - baseLayerIndex_ = -1; } - + PolylineSceneLayer* LayerHolder::GetPolylineLayer(int index /*= 0*/) { - using namespace Orthanc; - ORTHANC_ASSERT(baseLayerIndex_ != -1); - ORTHANC_ASSERT(GetScene().HasLayer(GetPolylineLayerIndex(index))); - ISceneLayer* layer = - &(GetScene().GetLayer(GetPolylineLayerIndex(index))); + boost::shared_ptr controller = controllerW_.lock(); - PolylineSceneLayer* concreteLayer = - dynamic_cast(layer); - - ORTHANC_ASSERT(concreteLayer != NULL); - return concreteLayer; + if (controller) + { + using namespace Orthanc; + ORTHANC_ASSERT(baseLayerIndex_ != -1); + ORTHANC_ASSERT(controller->GetScene().HasLayer(GetPolylineLayerIndex(index))); + ISceneLayer* layer = + &(controller->GetScene().GetLayer(GetPolylineLayerIndex(index))); + + PolylineSceneLayer* concreteLayer = + dynamic_cast(layer); + + ORTHANC_ASSERT(concreteLayer != NULL); + return concreteLayer; + } + else + { + return NULL; // TODO + } } TextSceneLayer* LayerHolder::GetTextLayer(int index /*= 0*/) { - using namespace Orthanc; - ORTHANC_ASSERT(baseLayerIndex_ != -1); - ORTHANC_ASSERT(GetScene().HasLayer(GetTextLayerIndex(index))); - ISceneLayer* layer = - &(GetScene().GetLayer(GetTextLayerIndex(index))); + boost::shared_ptr controller = controllerW_.lock(); - TextSceneLayer* concreteLayer = - dynamic_cast(layer); - - ORTHANC_ASSERT(concreteLayer != NULL); - return concreteLayer; + if (controller) + { + using namespace Orthanc; + ORTHANC_ASSERT(baseLayerIndex_ != -1); + ORTHANC_ASSERT(controller->GetScene().HasLayer(GetTextLayerIndex(index))); + ISceneLayer* layer = + &(controller->GetScene().GetLayer(GetTextLayerIndex(index))); + + TextSceneLayer* concreteLayer = + dynamic_cast(layer); + + ORTHANC_ASSERT(concreteLayer != NULL); + return concreteLayer; + } + else + { + return NULL; // TODO + } } int LayerHolder::GetPolylineLayerIndex(int index /*= 0*/) diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/LayerHolder.h --- a/Framework/Scene2DViewport/LayerHolder.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/LayerHolder.h Tue Feb 04 15:24:03 2020 +0100 @@ -97,7 +97,6 @@ int GetPolylineLayerIndex(int index = 0); int GetTextLayerIndex(int index = 0); int GetInfoTextLayerIndex(int index = 0); - Scene2D& GetScene(); int textLayerCount_; int polylineLayerCount_; diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/LineMeasureTool.cpp --- a/Framework/Scene2DViewport/LineMeasureTool.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/LineMeasureTool.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -32,8 +32,8 @@ { LineMeasureTool::LineMeasureTool( - MessageBroker& broker, boost::weak_ptr controllerW) - : MeasureTool(broker, controllerW) + boost::weak_ptr controllerW) + : MeasureTool(controllerW) #if ORTHANC_STONE_ENABLE_OUTLINED_TEXT == 1 , layerHolder_(boost::make_shared(controllerW, 1, 5)) #else @@ -148,7 +148,7 @@ const PointerEvent & e); */ boost::shared_ptr editLineMeasureTracker( - new EditLineMeasureTracker(shared_from_this(), GetBroker(), GetController(), e)); + new EditLineMeasureTracker(shared_from_this(), GetController(), e)); return editLineMeasureTracker; } @@ -182,55 +182,57 @@ // Fill the polyline layer with the measurement line PolylineSceneLayer* polylineLayer = layerHolder_->GetPolylineLayer(0); - polylineLayer->ClearAllChains(); - - const Color color(TOOL_LINES_COLOR_RED, - TOOL_LINES_COLOR_GREEN, - TOOL_LINES_COLOR_BLUE); - - const Color highlightColor(TOOL_LINES_HL_COLOR_RED, - TOOL_LINES_HL_COLOR_GREEN, - TOOL_LINES_HL_COLOR_BLUE); - + if (polylineLayer) { - PolylineSceneLayer::Chain chain; - chain.push_back(start_); - chain.push_back(end_); - if(lineHighlightArea_ == LineHighlightArea_Segment) - polylineLayer->AddChain(chain, false, highlightColor); - else - polylineLayer->AddChain(chain, false, color); - } + polylineLayer->ClearAllChains(); - // handles - { - { - PolylineSceneLayer::Chain chain; - - //TODO: take DPI into account - AddSquare(chain, GetController()->GetScene(), start_, - GetController()->GetHandleSideLengthS()); - - if (lineHighlightArea_ == LineHighlightArea_Start) - polylineLayer->AddChain(chain, true, highlightColor); - else - polylineLayer->AddChain(chain, true, color); - } + const Color color(TOOL_LINES_COLOR_RED, + TOOL_LINES_COLOR_GREEN, + TOOL_LINES_COLOR_BLUE); + + const Color highlightColor(TOOL_LINES_HL_COLOR_RED, + TOOL_LINES_HL_COLOR_GREEN, + TOOL_LINES_HL_COLOR_BLUE); { PolylineSceneLayer::Chain chain; + chain.push_back(start_); + chain.push_back(end_); + if(lineHighlightArea_ == LineHighlightArea_Segment) + polylineLayer->AddChain(chain, false, highlightColor); + else + polylineLayer->AddChain(chain, false, color); + } + + // handles + { + { + PolylineSceneLayer::Chain chain; - //TODO: take DPI into account - AddSquare(chain, GetController()->GetScene(), end_, - GetController()->GetHandleSideLengthS()); + //TODO: take DPI into account + AddSquare(chain, GetController()->GetScene(), start_, + GetController()->GetHandleSideLengthS()); - if (lineHighlightArea_ == LineHighlightArea_End) - polylineLayer->AddChain(chain, true, highlightColor); - else - polylineLayer->AddChain(chain, true, color); + if (lineHighlightArea_ == LineHighlightArea_Start) + polylineLayer->AddChain(chain, true, highlightColor); + else + polylineLayer->AddChain(chain, true, color); + } + + { + PolylineSceneLayer::Chain chain; + + //TODO: take DPI into account + AddSquare(chain, GetController()->GetScene(), end_, + GetController()->GetHandleSideLengthS()); + + if (lineHighlightArea_ == LineHighlightArea_End) + polylineLayer->AddChain(chain, true, highlightColor); + else + polylineLayer->AddChain(chain, true, color); + } } } - } { // Set the text layer propreties diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/LineMeasureTool.h --- a/Framework/Scene2DViewport/LineMeasureTool.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/LineMeasureTool.h Tue Feb 04 15:24:03 2020 +0100 @@ -35,10 +35,10 @@ namespace OrthancStone { - class LineMeasureTool : public MeasureTool, public boost::enable_shared_from_this + class LineMeasureTool : public MeasureTool { public: - LineMeasureTool(MessageBroker& broker, boost::weak_ptr controllerW); + LineMeasureTool(boost::weak_ptr controllerW); ~LineMeasureTool(); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/MeasureCommands.h --- a/Framework/Scene2DViewport/MeasureCommands.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/MeasureCommands.h Tue Feb 04 15:24:03 2020 +0100 @@ -47,6 +47,8 @@ protected: boost::shared_ptr GetController(); + + private: boost::weak_ptr controllerW_; }; diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/MeasureTool.cpp --- a/Framework/Scene2DViewport/MeasureTool.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/MeasureTool.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -28,14 +28,6 @@ namespace OrthancStone { - MeasureTool::~MeasureTool() - { - // if the controller is dead, let's not bother. - boost::shared_ptr controller = controllerW_.lock(); - if (controller) - controller->Unregister(this); - } - void MeasureTool::Enable() { enabled_ = true; @@ -79,15 +71,13 @@ #endif } - MeasureTool::MeasureTool(MessageBroker& broker, + MeasureTool::MeasureTool( boost::weak_ptr controllerW) - : IObserver(broker) - , controllerW_(controllerW) + : controllerW_(controllerW) , enabled_(true) { - GetController()->RegisterObserverCallback( - new Callable - (*this, &MeasureTool::OnSceneTransformChanged)); + // TODO => Move this out of constructor + Register(*GetController(), &MeasureTool::OnSceneTransformChanged); } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/MeasureTool.h --- a/Framework/Scene2DViewport/MeasureTool.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/MeasureTool.h Tue Feb 04 15:24:03 2020 +0100 @@ -20,6 +20,7 @@ #pragma once +#include "../Messages/ObserverBase.h" #include "../Scene2D/PolylineSceneLayer.h" #include "../Scene2D/Scene2D.h" #include "../Scene2D/ScenePoint2D.h" @@ -27,7 +28,6 @@ #include "../Scene2DViewport/PredeclaredTypes.h" #include "../Scene2DViewport/ViewportController.h" -#include #include #include @@ -38,10 +38,12 @@ class IFlexiblePointerTracker; class MeasureToolMemento; - class MeasureTool : public IObserver + class MeasureTool : public ObserverBase { public: - virtual ~MeasureTool(); + virtual ~MeasureTool() + { + } /** Enabled tools are rendered in the scene. @@ -111,7 +113,7 @@ virtual std::string GetDescription() = 0; protected: - MeasureTool(MessageBroker& broker, boost::weak_ptr controllerW); + MeasureTool(boost::weak_ptr controllerW); /** The measuring tool may exist in a standalone fashion, without any available diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/MeasureToolsToolbox.cpp --- a/Framework/Scene2DViewport/MeasureToolsToolbox.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/MeasureToolsToolbox.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -311,28 +311,30 @@ for (int i = startingLayerIndex; i < startingLayerIndex + 5; ++i) { TextSceneLayer* textLayer = layerHolder->GetTextLayer(i); - ORTHANC_ASSERT(textLayer != NULL); - textLayer->SetText(text); - - if (i == startingLayerIndex + 4) - { - textLayer->SetColor(TEXT_COLOR_RED, - TEXT_COLOR_GREEN, - TEXT_COLOR_BLUE); - } - else + if (textLayer != NULL) { - textLayer->SetColor(TEXT_OUTLINE_COLOR_RED, - TEXT_OUTLINE_COLOR_GREEN, - TEXT_OUTLINE_COLOR_BLUE); - } + textLayer->SetText(text); - ScenePoint2D textAnchor; - int offIndex = i - startingLayerIndex; - ORTHANC_ASSERT(offIndex >= 0 && offIndex < 5); - textLayer->SetPosition( - p.GetX() + xoffsets[offIndex] * pixelToScene, - p.GetY() + yoffsets[offIndex] * pixelToScene); + if (i == startingLayerIndex + 4) + { + textLayer->SetColor(TEXT_COLOR_RED, + TEXT_COLOR_GREEN, + TEXT_COLOR_BLUE); + } + else + { + textLayer->SetColor(TEXT_OUTLINE_COLOR_RED, + TEXT_OUTLINE_COLOR_GREEN, + TEXT_OUTLINE_COLOR_BLUE); + } + + ScenePoint2D textAnchor; + int offIndex = i - startingLayerIndex; + ORTHANC_ASSERT(offIndex >= 0 && offIndex < 5); + textLayer->SetPosition( + p.GetX() + xoffsets[offIndex] * pixelToScene, + p.GetY() + yoffsets[offIndex] * pixelToScene); + } } } #else @@ -344,20 +346,21 @@ , int layerIndex) { TextSceneLayer* textLayer = layerHolder->GetTextLayer(layerIndex); - ORTHANC_ASSERT(textLayer != NULL); - textLayer->SetText(text); + if (textLayer != NULL) + { + textLayer->SetText(text); + textLayer->SetColor(TEXT_COLOR_RED, TEXT_COLOR_GREEN, TEXT_COLOR_BLUE); - textLayer->SetColor(TEXT_COLOR_RED, TEXT_COLOR_GREEN, TEXT_COLOR_BLUE); - - ScenePoint2D textAnchor; - textLayer->SetPosition(p.GetX(), p.GetY()); + ScenePoint2D textAnchor; + textLayer->SetPosition(p.GetX(), p.GetY()); + } } #endif - std::ostream& operator<<(std::ostream& os, const ScenePoint2D& p) - { - os << "x = " << p.GetX() << " , y = " << p.GetY(); - return os; - } + std::ostream& operator<<(std::ostream& os, const ScenePoint2D& p) + { + os << "x = " << p.GetX() << " , y = " << p.GetY(); + return os; + } } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/MeasureTrackers.cpp --- a/Framework/Scene2DViewport/MeasureTrackers.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/MeasureTrackers.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -53,23 +53,17 @@ command_->Undo(); } - Scene2D& CreateMeasureTracker::GetScene() - { - return controllerW_.lock()->GetScene(); - } - EditMeasureTracker::EditMeasureTracker(boost::weak_ptr controllerW, const PointerEvent& e) : controllerW_(controllerW) , alive_(true) , commitResult_(true) { boost::shared_ptr controller = controllerW.lock(); - originalClickPosition_ = e.GetMainPosition().Apply(controller->GetScene().GetCanvasToSceneTransform()); - } - Scene2D& EditMeasureTracker::GetScene() - { - return controllerW_.lock()->GetScene(); + if (controller) + { + originalClickPosition_ = e.GetMainPosition().Apply(controller->GetScene().GetCanvasToSceneTransform()); + } } void EditMeasureTracker::Cancel() diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/MeasureTrackers.h --- a/Framework/Scene2DViewport/MeasureTrackers.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/MeasureTrackers.h Tue Feb 04 15:24:03 2020 +0100 @@ -48,7 +48,6 @@ boost::shared_ptr command_; boost::weak_ptr controllerW_; bool alive_; - Scene2D& GetScene(); private: bool commitResult_; @@ -68,7 +67,6 @@ boost::shared_ptr command_; boost::weak_ptr controllerW_; bool alive_; - Scene2D& GetScene(); ScenePoint2D GetOriginalClickPosition() const { diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/OneGesturePointerTracker.cpp --- a/Framework/Scene2DViewport/OneGesturePointerTracker.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/OneGesturePointerTracker.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -55,6 +55,15 @@ // the number of active touches currentTouchCount_++; LOG(INFO) << "currentTouchCount_ becomes: " << currentTouchCount_; + + /** + * 2019-12-06 (SJO): Patch to have consistent behavior when mouse + * leaves the canvas while the tracker is still active, then + * button is released while out-of-canvas. Such an event is not + * catched (at least in WebAssembly), so we delete the tracker on + * the next click inside the canvas. + **/ + alive_ = false; } bool OneGesturePointerTracker::IsAlive() const diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/ViewportController.cpp --- a/Framework/Scene2DViewport/ViewportController.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/ViewportController.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -24,41 +24,78 @@ #include "MeasureCommands.h" #include "../StoneException.h" +#include "../Scene2D/PanSceneTracker.h" +#include "../Scene2D/RotateSceneTracker.h" +#include "../Scene2D/ZoomSceneTracker.h" #include namespace OrthancStone { - ViewportController::ViewportController(boost::weak_ptr undoStackW, - MessageBroker& broker, - IViewport& viewport) - : IObservable(broker) - , undoStackW_(undoStackW) - , canvasToSceneFactor_(0.0) - , viewport_(viewport) + IFlexiblePointerTracker* DefaultViewportInteractor::CreateTracker( + boost::shared_ptr controller, + const PointerEvent& event, + unsigned int viewportWidth, + unsigned int viewportHeight) + { + switch (event.GetMouseButton()) + { + case MouseButton_Left: + return new RotateSceneTracker(controller, event); + + case MouseButton_Middle: + return new PanSceneTracker(controller, event); + + case MouseButton_Right: + { + if (viewportWidth != 0) + { + return new ZoomSceneTracker(controller, event, viewportWidth); + } + else + { + return NULL; + } + } + + default: + return NULL; + } + } + + + ViewportController::ViewportController() : + undoStackW_(boost::make_shared()), + scene_(new Scene2D), + canvasToSceneFactor_(1) + { + } + + ViewportController::ViewportController(const Scene2D& scene) : + undoStackW_(boost::make_shared()), + scene_(scene.Clone()), + canvasToSceneFactor_(1) + { + } + + ViewportController::ViewportController(boost::weak_ptr undoStackW) : + undoStackW_(undoStackW), + scene_(new Scene2D), + canvasToSceneFactor_(1) { } ViewportController::~ViewportController() { - - } - - boost::shared_ptr ViewportController::GetUndoStack() - { - return undoStackW_.lock(); - } - - boost::shared_ptr ViewportController::GetUndoStack() const - { - return undoStackW_.lock(); } void ViewportController::PushCommand(boost::shared_ptr command) { boost::shared_ptr undoStack = undoStackW_.lock(); - if(undoStack.get() != NULL) + if (undoStack.get() != NULL) + { undoStack->PushCommand(command); + } else { LOG(ERROR) << "Internal error: no undo stack in the viewport controller!"; @@ -69,7 +106,9 @@ { boost::shared_ptr undoStack = undoStackW_.lock(); if (undoStack.get() != NULL) + { undoStack->Undo(); + } else { LOG(ERROR) << "Internal error: no undo stack in the viewport controller!"; @@ -80,7 +119,9 @@ { boost::shared_ptr undoStack = undoStackW_.lock(); if (undoStack.get() != NULL) + { undoStack->Redo(); + } else { LOG(ERROR) << "Internal error: no undo stack in the viewport controller!"; @@ -91,7 +132,9 @@ { boost::shared_ptr undoStack = undoStackW_.lock(); if (undoStack.get() != NULL) + { return undoStack->CanUndo(); + } else { LOG(ERROR) << "Internal error: no undo stack in the viewport controller!"; @@ -103,7 +146,9 @@ { boost::shared_ptr undoStack = undoStackW_.lock(); if (undoStack.get() != NULL) + { return undoStack->CanRedo(); + } else { LOG(ERROR) << "Internal error: no undo stack in the viewport controller!"; @@ -111,11 +156,6 @@ } } - bool ViewportController::HandlePointerEvent(PointerEvent e) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - std::vector > ViewportController::HitTestMeasureTools( ScenePoint2D p) { @@ -138,42 +178,30 @@ } } - const OrthancStone::AffineTransform2D& ViewportController::GetCanvasToSceneTransform() const + OrthancStone::AffineTransform2D ViewportController::GetCanvasToSceneTransform() const { - return GetScene().GetCanvasToSceneTransform(); - } - - const OrthancStone::AffineTransform2D& ViewportController::GetSceneToCanvasTransform() const - { - return GetScene().GetSceneToCanvasTransform(); + return scene_->GetCanvasToSceneTransform(); } - void ViewportController::SetSceneToCanvasTransform( - const AffineTransform2D& transform) + OrthancStone::AffineTransform2D ViewportController::GetSceneToCanvasTransform() const { - viewport_.GetScene().SetSceneToCanvasTransform(transform); - BroadcastMessage(SceneTransformChanged(*this)); - - // update the canvas to scene factor - canvasToSceneFactor_ = 0.0; - canvasToSceneFactor_ = GetCanvasToSceneFactor(); + return scene_->GetSceneToCanvasTransform(); } - void ViewportController::FitContent( - unsigned int canvasWidth, unsigned int canvasHeight) + void ViewportController::SetSceneToCanvasTransform(const AffineTransform2D& transform) { - viewport_.GetScene().FitContent(canvasWidth, canvasHeight); + scene_->SetSceneToCanvasTransform(transform); + + canvasToSceneFactor_ = scene_->GetCanvasToSceneTransform().ComputeZoom(); BroadcastMessage(SceneTransformChanged(*this)); } - void ViewportController::FitContent() + void ViewportController::FitContent(unsigned int viewportWidth, + unsigned int viewportHeight) { - if (viewport_.HasCompositor()) - { - const ICompositor& compositor = viewport_.GetCompositor(); - viewport_.GetScene().FitContent(compositor.GetCanvasWidth(), compositor.GetCanvasHeight()); - BroadcastMessage(SceneTransformChanged(*this)); - } + scene_->FitContent(viewportWidth, viewportHeight); + canvasToSceneFactor_ = scene_->GetCanvasToSceneTransform().ComputeZoom(); + BroadcastMessage(SceneTransformChanged(*this)); } void ViewportController::AddMeasureTool(boost::shared_ptr measureTool) @@ -192,14 +220,8 @@ measureTools_.end()); } - double ViewportController::GetCanvasToSceneFactor() const { - if (canvasToSceneFactor_ == 0) - { - canvasToSceneFactor_ = - GetScene().GetCanvasToSceneTransform().ComputeZoom(); - } return canvasToSceneFactor_; } @@ -222,4 +244,63 @@ { return TEXT_CENTER_DISTANCE_CANVAS_COORD * GetCanvasToSceneFactor(); } + + + void ViewportController::HandleMousePress(OrthancStone::IViewportInteractor& interactor, + const PointerEvent& event, + unsigned int viewportWidth, + unsigned int viewportHeight) + { + if (activeTracker_) + { + // We are dealing with a multi-stage tracker (that is made of several interactions) + activeTracker_->PointerDown(event); + + if (!activeTracker_->IsAlive()) + { + activeTracker_.reset(); + } + } + else + { + // Check whether there is already a measure tool at that position + for (size_t i = 0; i < measureTools_.size(); ++i) + { + if (measureTools_[i]->HitTest(event.GetMainPosition())) + { + activeTracker_ = measureTools_[i]->CreateEditionTracker(event); + return; + } + } + + // No measure tool, create new tracker from the interactor + activeTracker_.reset(interactor.CreateTracker(shared_from_this(), event, viewportWidth, viewportHeight)); + } + } + + bool ViewportController::HandleMouseMove(const PointerEvent& event) + { + if (activeTracker_) + { + activeTracker_->PointerMove(event); + return true; + } + else + { + return false; + } + } + + void ViewportController::HandleMouseRelease(const PointerEvent& event) + { + if (activeTracker_) + { + activeTracker_->PointerUp(event); + + if (!activeTracker_->IsAlive()) + { + activeTracker_.reset(); + } + } + } } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Scene2DViewport/ViewportController.h --- a/Framework/Scene2DViewport/ViewportController.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Scene2DViewport/ViewportController.h Tue Feb 04 15:24:03 2020 +0100 @@ -22,14 +22,41 @@ #include "PredeclaredTypes.h" -#include "../Viewport/IViewport.h" -#include "../Scene2D/PointerEvent.h" +#include "../Messages/IObservable.h" +#include "../Scene2D/Scene2D.h" #include "../Scene2DViewport/IFlexiblePointerTracker.h" +#include #include namespace OrthancStone { + // TODO - Move this to another file + class IViewportInteractor : public boost::noncopyable + { + public: + virtual ~IViewportInteractor() + { + } + + virtual IFlexiblePointerTracker* CreateTracker(boost::shared_ptr controller, + const PointerEvent& event, + unsigned int viewportWidth, + unsigned int viewportHeight) = 0; + }; + + + // TODO - Move this to another file + class DefaultViewportInteractor : public IViewportInteractor + { + public: + virtual IFlexiblePointerTracker* CreateTracker(boost::shared_ptr controller, + const PointerEvent& event, + unsigned int viewportWidth, + unsigned int viewportHeight) ORTHANC_OVERRIDE; + }; + + class UndoStack; const double ARC_RADIUS_CANVAS_COORD = 30.0; @@ -74,25 +101,22 @@ Each canvas or other GUI area where we want to display a 2D image, either directly or through slicing must be assigned a ViewportController. */ - class ViewportController : public IObservable + class ViewportController : + public IObservable, + public boost::enable_shared_from_this { public: ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, \ - SceneTransformChanged, ViewportController); + SceneTransformChanged, ViewportController); + + ViewportController(); - ViewportController(boost::weak_ptr undoStackW, - MessageBroker& broker, - IViewport& viewport); + ViewportController(const Scene2D& scene /* will be cloned */); + ViewportController(boost::weak_ptr undoStackW); ~ViewportController(); - /** - This method is called by the GUI system and should update/delete the - current tracker - */ - bool HandlePointerEvent(PointerEvent e); - /** This method returns the list of measure tools containing the supplied point (in scene coords). A tracker can then be requested from the chosen @@ -110,20 +134,20 @@ With this method, the object takes ownership of the supplied tracker and updates it according to user interaction */ - void SetActiveTracker(boost::shared_ptr tracker); + void AcquireActiveTracker(IFlexiblePointerTracker* tracker); /** Forwarded to the underlying scene */ - const AffineTransform2D& GetCanvasToSceneTransform() const; + AffineTransform2D GetCanvasToSceneTransform() const; /** Forwarded to the underlying scene */ - const AffineTransform2D& GetSceneToCanvasTransform() const; + AffineTransform2D GetSceneToCanvasTransform() const; /** Forwarded to the underlying scene, and broadcasted to the observers */ void SetSceneToCanvasTransform(const AffineTransform2D& transform); /** Forwarded to the underlying scene, and broadcasted to the observers */ - void FitContent(unsigned int canvasWidth, unsigned int canvasHeight); - void FitContent(); + void FitContent(unsigned int viewportWidth, + unsigned int viewportHeight); /** Adds a new measure tool */ void AddMeasureTool(boost::shared_ptr measureTool); @@ -173,31 +197,45 @@ /** forwarded to the UndoStack */ bool CanRedo() const; - Scene2D& GetScene() - { - return viewport_.GetScene(); - } + + // Must be expressed in canvas coordinates + void HandleMousePress(IViewportInteractor& interactor, + const PointerEvent& event, + unsigned int viewportWidth, + unsigned int viewportHeight); + + // Must be expressed in canvas coordinates. Returns "true" if the + // state has changed, so that "Invalidate()" can be called. + bool HandleMouseMove(const PointerEvent& event); + + // Must be expressed in canvas coordinates + void HandleMouseRelease(const PointerEvent& event); const Scene2D& GetScene() const { - return const_cast(viewport_).GetScene(); + return *scene_; + } + + Scene2D& GetScene() + { + return *scene_; + } + + bool HasActiveTracker() const + { + return activeTracker_.get() != NULL; } private: double GetCanvasToSceneFactor() const; - boost::weak_ptr undoStackW_; - - boost::shared_ptr GetUndoStack(); - boost::shared_ptr GetUndoStack() const; + boost::weak_ptr undoStackW_; // Global stack, possibly shared by all viewports + std::vector > measureTools_; + boost::shared_ptr activeTracker_; // TODO - Couldn't this be a "std::auto_ptr"? - std::vector > measureTools_; - boost::shared_ptr tracker_; - + std::auto_ptr scene_; + // this is cached - mutable double canvasToSceneFactor_; - - // Refactoring on 2019-07-10: Removing shared_ptr from scene - IViewport& viewport_; + double canvasToSceneFactor_; }; } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/StoneEnumerations.h --- a/Framework/StoneEnumerations.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/StoneEnumerations.h Tue Feb 04 15:24:03 2020 +0100 @@ -24,21 +24,6 @@ #include -namespace Deprecated -{ - enum SliceImageQuality - { - SliceImageQuality_FullPng, // smaller to transmit but longer to generate on Orthanc side (better choice when on low bandwidth) - SliceImageQuality_FullPam, // bigger to transmit but faster to generate on Orthanc side (better choice when on localhost or LAN) - SliceImageQuality_Jpeg50, - SliceImageQuality_Jpeg90, - SliceImageQuality_Jpeg95, - - SliceImageQuality_InternalRaw // downloads the raw pixels data as they are stored in the DICOM file (internal use only) - }; -} - - namespace OrthancStone { enum SliceOffsetMode @@ -59,7 +44,8 @@ { MouseButton_Left, MouseButton_Right, - MouseButton_Middle + MouseButton_Middle, + MouseButton_None // For instance, because of touch event }; enum MouseWheelDirection @@ -135,6 +121,15 @@ BitmapAnchor_TopRight }; + enum SliceAction + { + SliceAction_FastPlus, + SliceAction_Plus, + SliceAction_None, + SliceAction_Minus, + SliceAction_FastMinus + }; + SopClassUid StringToSopClassUid(const std::string& source); void ComputeWindowing(float& targetCenter, diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/StoneInitialization.cpp --- a/Framework/StoneInitialization.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/StoneInitialization.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -21,20 +21,58 @@ #include "StoneInitialization.h" -#include - #if !defined(ORTHANC_ENABLE_SDL) # error Macro ORTHANC_ENABLE_SDL must be defined #endif +#if !defined(ORTHANC_ENABLE_QT) +# error Macro ORTHANC_ENABLE_QT must be defined +#endif + +#if !defined(ORTHANC_ENABLE_SSL) +# error Macro ORTHANC_ENABLE_SSL must be defined +#endif + +#if !defined(ORTHANC_ENABLE_CURL) +# error Macro ORTHANC_ENABLE_CURL must be defined +#endif + +#if !defined(ORTHANC_ENABLE_DCMTK) +# error Macro ORTHANC_ENABLE_DCMTK must be defined +# if !defined(DCMTK_VERSION_NUMBER) +# error Macro DCMTK_VERSION_NUMBER must be defined +# endif +#endif + #if ORTHANC_ENABLE_SDL == 1 # include "Viewport/SdlWindow.h" #endif +#if ORTHANC_ENABLE_QT == 1 +# include +#endif + #if ORTHANC_ENABLE_CURL == 1 -#include +# include +#endif + +#if ORTHANC_ENABLE_DCMTK == 1 +# include #endif +#if ORTHANC_ENABLE_WASM == 1 +static double viewportsTimeout_ = 1000; +static std::auto_ptr viewportsRegistry_; +#endif + +#include "Toolbox/LinearAlgebra.h" + +#include +#include + +#include + + namespace OrthancStone { #if ORTHANC_ENABLE_LOGGING_PLUGIN == 1 @@ -49,25 +87,142 @@ Orthanc::Logging::Initialize(); #endif -#if ORTHANC_ENABLE_SDL == 1 - OrthancStone::SdlWindow::GlobalInitialize(); +#if ORTHANC_ENABLE_SSL == 1 + // Must be before curl + Orthanc::Toolbox::InitializeOpenSsl(); #endif #if ORTHANC_ENABLE_CURL == 1 Orthanc::HttpClient::GlobalInitialize(); +# if ORTHANC_ENABLE_SSL == 1 + Orthanc::HttpClient::ConfigureSsl(false, ""); +# endif +#endif + +#if ORTHANC_ENABLE_DCMTK == 1 + Orthanc::FromDcmtkBridge::InitializeDictionary(true); + Orthanc::FromDcmtkBridge::InitializeCodecs(); +# if DCMTK_VERSION_NUMBER <= 360 + OFLog::configure(OFLogger::FATAL_LOG_LEVEL); +# else + OFLog::configure(OFLogger::OFF_LOG_LEVEL); +# endif +#endif + + /** + * This call is necessary to make "boost::lexical_cast<>" work in + * a consistent way in the presence of "double" or "float", and of + * a numeric locale that replaces dot (".") by comma (",") as the + * decimal separator. + * https://stackoverflow.com/a/18981514/881731 + **/ + std::locale::global(std::locale::classic()); + + { + // Run-time checks of locale settings, to be run after Qt has + // been initialized, as Qt changes locale settings + +#if ORTHANC_ENABLE_QT == 1 + if (QCoreApplication::instance() == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, + "Qt must be initialized before Stone"); + } +#endif + + { + OrthancStone::Vector v; + if (!OrthancStone::LinearAlgebra::ParseVector(v, "1.3671875\\-1.3671875") || + v.size() != 2 || + !OrthancStone::LinearAlgebra::IsNear(1.3671875f, v[0]) || + !OrthancStone::LinearAlgebra::IsNear(-1.3671875f, v[1])) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, + "Error in the locale settings, giving up"); + } + } + + { + Json::Value dicomweb = Json::objectValue; + dicomweb["00280030"] = Json::objectValue; + dicomweb["00280030"]["vr"] = "DS"; + dicomweb["00280030"]["Value"] = Json::arrayValue; + dicomweb["00280030"]["Value"].append(1.2f); + dicomweb["00280030"]["Value"].append(-1.5f); + + Orthanc::DicomMap source; + source.FromDicomWeb(dicomweb); + + std::string s; + OrthancStone::Vector v; + if (!source.LookupStringValue(s, Orthanc::DICOM_TAG_PIXEL_SPACING, false) || + !OrthancStone::LinearAlgebra::ParseVector(v, s) || + v.size() != 2 || + !OrthancStone::LinearAlgebra::IsNear(1.2f, v[0]) || + !OrthancStone::LinearAlgebra::IsNear(-1.5f, v[1])) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, + "Error in the locale settings, giving up"); + } + } + } + +#if ORTHANC_ENABLE_SDL == 1 + OrthancStone::SdlWindow::GlobalInitialize(); #endif } + void StoneFinalize() { +#if ORTHANC_ENABLE_WASM == 1 + viewportsRegistry_.reset(); +#endif + #if ORTHANC_ENABLE_SDL == 1 OrthancStone::SdlWindow::GlobalFinalize(); #endif +#if ORTHANC_ENABLE_DCMTK == 1 + Orthanc::FromDcmtkBridge::FinalizeCodecs(); +#endif + #if ORTHANC_ENABLE_CURL == 1 Orthanc::HttpClient::GlobalFinalize(); #endif +#if ORTHANC_ENABLE_SSL == 1 + Orthanc::Toolbox::FinalizeOpenSsl(); +#endif + Orthanc::Logging::Finalize(); } + + +#if ORTHANC_ENABLE_WASM == 1 + void SetWebGLViewportsRegistryTimeout(double timeout) + { + if (viewportsRegistry_.get()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + viewportsTimeout_ = timeout; + } + } +#endif + + +#if ORTHANC_ENABLE_WASM == 1 + WebGLViewportsRegistry& GetWebGLViewportsRegistry() + { + if (viewportsRegistry_.get() == NULL) + { + viewportsRegistry_.reset(new WebGLViewportsRegistry(viewportsTimeout_)); + } + + return *viewportsRegistry_; + } +#endif } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/StoneInitialization.h --- a/Framework/StoneInitialization.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/StoneInitialization.h Tue Feb 04 15:24:03 2020 +0100 @@ -21,6 +21,18 @@ #pragma once +#if !defined(ORTHANC_ENABLE_WASM) +# error Macro ORTHANC_ENABLE_WASM must be defined +#endif + +#if !defined(ORTHANC_ENABLE_LOGGING_PLUGIN) +# error Macro ORTHANC_ENABLE_LOGGING_PLUGIN must be defined +#endif + +#if ORTHANC_ENABLE_WASM == 1 +# include "Viewport/WebGLViewportsRegistry.h" +#endif + #include namespace OrthancStone @@ -32,4 +44,12 @@ #endif void StoneFinalize(); + +#if ORTHANC_ENABLE_WASM == 1 + void SetWebGLViewportsRegistryTimeout(double timeout); +#endif + +#if ORTHANC_ENABLE_WASM == 1 + WebGLViewportsRegistry& GetWebGLViewportsRegistry(); +#endif } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Toolbox/AffineTransform2D.cpp diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Toolbox/AffineTransform2D.h diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Toolbox/CoordinateSystem3D.cpp --- a/Framework/Toolbox/CoordinateSystem3D.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Toolbox/CoordinateSystem3D.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -241,4 +241,14 @@ return s; } + + CoordinateSystem3D CoordinateSystem3D::NormalizeCuttingPlane(const CoordinateSystem3D& plane) + { + double ox, oy; + plane.ProjectPoint(ox, oy, LinearAlgebra::CreateVector(0, 0, 0)); + + CoordinateSystem3D normalized(plane); + normalized.SetOrigin(plane.MapSliceToWorldCoordinates(ox, oy)); + return normalized; + } } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Toolbox/CoordinateSystem3D.h --- a/Framework/Toolbox/CoordinateSystem3D.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Toolbox/CoordinateSystem3D.h Tue Feb 04 15:24:03 2020 +0100 @@ -22,6 +22,7 @@ #pragma once #include "LinearAlgebra.h" +#include "../Scene2D/ScenePoint2D.h" #include @@ -95,12 +96,24 @@ Vector MapSliceToWorldCoordinates(double x, double y) const; + Vector MapSliceToWorldCoordinates(const ScenePoint2D& p) const + { + return MapSliceToWorldCoordinates(p.GetX(), p.GetY()); + } + double ProjectAlongNormal(const Vector& point) const; void ProjectPoint(double& offsetX, double& offsetY, const Vector& point) const; + ScenePoint2D ProjectPoint(const Vector& point) const + { + double x, y; + ProjectPoint(x, y, point); + return ScenePoint2D(x, y); + } + /* Alternated faster implementation (untested yet) */ @@ -120,5 +133,9 @@ static bool ComputeDistance(double& distance, const CoordinateSystem3D& a, const CoordinateSystem3D& b); + + // Normalize a cutting plane so that the origin (0,0,0) of the 3D + // world is mapped to the origin of its (x,y) coordinate system + static CoordinateSystem3D NormalizeCuttingPlane(const CoordinateSystem3D& plane); }; } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Toolbox/DicomInstanceParameters.cpp --- a/Framework/Toolbox/DicomInstanceParameters.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Toolbox/DicomInstanceParameters.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -265,7 +265,7 @@ void DicomInstanceParameters::Data::ApplyRescaleAndDoseScaling(Orthanc::ImageAccessor& image, - bool useDouble) const + bool useDouble) const { if (image.GetFormat() != Orthanc::PixelFormat_Float32) { @@ -467,4 +467,48 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } } + + + double DicomInstanceParameters::Data::ApplyRescale(double value) const + { + double factor = doseGridScaling_; + double offset = 0.0; + + if (hasRescale_) + { + factor *= rescaleSlope_; + offset = rescaleIntercept_; + } + + return (value * factor + offset); + } + + + bool DicomInstanceParameters::Data::ComputeRegularSpacing(double& spacing) const + { + if (frameOffsets_.size() == 0) // Not a RT-DOSE + { + return false; + } + else if (frameOffsets_.size() == 1) + { + spacing = 1; // Edge case: RT-DOSE with one single frame + return true; + } + else + { + spacing = std::abs(frameOffsets_[1] - frameOffsets_[0]); + + for (size_t i = 1; i + 1 < frameOffsets_.size(); i++) + { + double s = frameOffsets_[i + 1] - frameOffsets_[i]; + if (!LinearAlgebra::IsNear(spacing, s, 0.001)) + { + return false; + } + } + + return true; + } + } } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Toolbox/DicomInstanceParameters.h --- a/Framework/Toolbox/DicomInstanceParameters.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Toolbox/DicomInstanceParameters.h Tue Feb 04 15:24:03 2020 +0100 @@ -71,8 +71,12 @@ bool IsPlaneWithinSlice(unsigned int frame, const CoordinateSystem3D& plane) const; - void ApplyRescaleAndDoseScaling( - Orthanc::ImageAccessor& image, bool useDouble) const; + void ApplyRescaleAndDoseScaling(Orthanc::ImageAccessor& image, + bool useDouble) const; + + double ApplyRescale(double value) const; + + bool ComputeRegularSpacing(double& target) const; }; @@ -207,9 +211,25 @@ return data_.doseUnits_; } + void SetDoseGridScaling(double value) + { + data_.doseGridScaling_ = value; + } + double GetDoseGridScaling() const { return data_.doseGridScaling_; } + + double ApplyRescale(double value) const + { + return data_.ApplyRescale(value); + } + + // Required for RT-DOSE + bool ComputeRegularSpacing(double& target) const + { + return data_.ComputeRegularSpacing(target); + } }; } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Toolbox/DicomStructureSet.cpp --- a/Framework/Toolbox/DicomStructureSet.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Toolbox/DicomStructureSet.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -27,12 +27,11 @@ #include #include #include -#include #include #if defined(_MSC_VER) -#pragma warning(push) -#pragma warning(disable:4244) +# pragma warning(push) +# pragma warning(disable:4244) #endif #include @@ -43,9 +42,14 @@ #include #if defined(_MSC_VER) -#pragma warning(pop) +# pragma warning(pop) #endif +#if ORTHANC_ENABLE_DCMTK == 1 +# include "ParsedDicomDataset.h" +#endif + + typedef boost::geometry::model::d2::point_xy BoostPoint; typedef boost::geometry::model::polygon BoostPolygon; typedef boost::geometry::model::multi_polygon BoostMultiPolygon; @@ -81,7 +85,7 @@ } } -#ifdef USE_BOOST_UNION_FOR_POLYGONS +#if USE_BOOST_UNION_FOR_POLYGONS == 1 static BoostPolygon CreateRectangle(float x1, float y1, float x2, float y2) @@ -99,7 +103,7 @@ namespace OrthancStone { static RtStructRectangleInSlab CreateRectangle(float x1, float y1, - float x2, float y2) + float x2, float y2) { RtStructRectangleInSlab rect; rect.xmin = std::min(x1, x2); @@ -174,15 +178,15 @@ double magnitude = GeometryToolbox::ProjectAlongNormal(v, geometry_.GetNormal()); if(!LinearAlgebra::IsNear( - magnitude, - projectionAlongNormal_, - sliceThickness_ / 2.0 /* in mm */ )) + magnitude, + projectionAlongNormal_, + sliceThickness_ / 2.0 /* in mm */ )) { LOG(ERROR) << "This RT-STRUCT contains a point that is off the " - << "slice of its instance | " - << "magnitude = " << magnitude << " | " - << "projectionAlongNormal_ = " << projectionAlongNormal_ << " | " - << "tolerance (sliceThickness_ / 2.0) = " << (sliceThickness_ / 2.0); + << "slice of its instance | " + << "magnitude = " << magnitude << " | " + << "projectionAlongNormal_ = " << projectionAlongNormal_ << " | " + << "tolerance (sliceThickness_ / 2.0) = " << (sliceThickness_ / 2.0); throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); } @@ -202,10 +206,10 @@ if (!onSlice) { LOG(WARNING) << "This RT-STRUCT contains a point that is off the " - << "slice of its instance | " - << "magnitude = " << magnitude << " | " - << "projectionAlongNormal_ = " << projectionAlongNormal_ << " | " - << "tolerance (sliceThickness_ / 2.0) = " << (sliceThickness_ / 2.0); + << "slice of its instance | " + << "magnitude = " << magnitude << " | " + << "projectionAlongNormal_ = " << projectionAlongNormal_ << " | " + << "tolerance (sliceThickness_ / 2.0) = " << (sliceThickness_ / 2.0); } return onSlice; } @@ -373,12 +377,14 @@ else if (GeometryToolbox::IsParallelOrOpposite (isOpposite, slice.GetNormal(), geometry_.GetAxisX())) { - // plane is constant X + // plane is constant X => Sagittal view (remember that in the + // sagittal projection, the normal must be swapped) + /* - Please read the comments in the section above, by taking into account - the fact that, in this case, the plane has a constant X, not Y (in - polygon geometry_ coordinates) + Please read the comments in the section above, by taking into account + the fact that, in this case, the plane has a constant X, not Y (in + polygon geometry_ coordinates) */ if (x < extent_.GetX1() || @@ -427,10 +433,6 @@ slice.ProjectPoint2(x1, y1, p1); slice.ProjectPoint2(x2, y2, p2); - // TODO WHY THIS??? - y1 = -y1; - y2 = -y2; - return true; } } @@ -463,7 +465,7 @@ return structures_[index]; } - DicomStructureSet::DicomStructureSet(const OrthancPlugins::FullOrthancDataset& tags) + void DicomStructureSet::Setup(const OrthancPlugins::IDicomDataset& tags) { OrthancPlugins::DicomDatasetReader reader(tags); @@ -514,11 +516,11 @@ } LOG(INFO) << "New RT structure: \"" << structures_[i].name_ - << "\" with interpretation \"" << structures_[i].interpretation_ - << "\" containing " << countSlices << " slices (color: " - << static_cast(structures_[i].red_) << "," - << static_cast(structures_[i].green_) << "," - << static_cast(structures_[i].blue_) << ")"; + << "\" with interpretation \"" << structures_[i].interpretation_ + << "\" containing " << countSlices << " slices (color: " + << static_cast(structures_[i].red_) << "," + << static_cast(structures_[i].green_) << "," + << static_cast(structures_[i].blue_) << ")"; // These temporary variables avoid allocating many vectors in the loop below OrthancPlugins::DicomPath countPointsPath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, i, @@ -609,6 +611,15 @@ } +#if ORTHANC_ENABLE_DCMTK == 1 + DicomStructureSet::DicomStructureSet(Orthanc::ParsedDicomFile& instance) + { + ParsedDicomDataset dataset(instance); + Setup(dataset); + } +#endif + + Vector DicomStructureSet::GetStructureCenter(size_t index) const { const Structure& structure = GetStructure(index); @@ -773,14 +784,14 @@ if (Orthanc::Toolbox::StripSpaces(sopInstanceUid) == "") { LOG(ERROR) << "DicomStructureSet::CheckReferencedSlices(): " - << " missing information about referenced instance " - << "(sopInstanceUid is empty!)"; + << " missing information about referenced instance " + << "(sopInstanceUid is empty!)"; } else { LOG(ERROR) << "DicomStructureSet::CheckReferencedSlices(): " - << " missing information about referenced instance " - << "(sopInstanceUid = " << sopInstanceUid << ")"; + << " missing information about referenced instance " + << "(sopInstanceUid = " << sopInstanceUid << ")"; } //throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } @@ -803,17 +814,18 @@ } } -#ifdef USE_BOOST_UNION_FOR_POLYGONS - bool DicomStructureSet::ProjectStructure(std::vector< std::vector >& polygons, - const Structure& structure, - const CoordinateSystem3D& slice) const + bool DicomStructureSet::ProjectStructure( +#if USE_BOOST_UNION_FOR_POLYGONS == 1 + std::vector< std::vector >& polygons, #else - bool DicomStructureSet::ProjectStructure(std::vector< std::pair >& segments, + std::vector< std::pair >& segments, +#endif const Structure& structure, - const CoordinateSystem3D& slice) const -#endif + const CoordinateSystem3D& sourceSlice) const { -#ifdef USE_BOOST_UNION_FOR_POLYGONS + const CoordinateSystem3D slice = CoordinateSystem3D::NormalizeCuttingPlane(sourceSlice); + +#if USE_BOOST_UNION_FOR_POLYGONS == 1 polygons.clear(); #else segments.clear(); @@ -831,7 +843,7 @@ { if (polygon->IsOnSlice(slice)) { -#ifdef USE_BOOST_UNION_FOR_POLYGONS +#if USE_BOOST_UNION_FOR_POLYGONS == 1 polygons.push_back(std::vector()); for (Points::const_iterator p = polygon->GetPoints().begin(); @@ -882,15 +894,26 @@ #if 1 // Sagittal or coronal projection -#ifdef USE_BOOST_UNION_FOR_POLYGONS +#if USE_BOOST_UNION_FOR_POLYGONS == 1 std::vector projected; + + for (Polygons::const_iterator polygon = structure.polygons_.begin(); + polygon != structure.polygons_.end(); ++polygon) + { + double x1, y1, x2, y2; + + if (polygon->Project(x1, y1, x2, y2, slice)) + { + projected.push_back(CreateRectangle(x1, y1, x2, y2)); + } + } #else // this will contain the intersection of the polygon slab with // the cutting plane, projected on the cutting plane coord system // (that yields a rectangle) + the Z coordinate of the polygon // (this is required to group polygons with the same Z later) std::vector > projected; -#endif + for (Polygons::const_iterator polygon = structure.polygons_.begin(); polygon != structure.polygons_.end(); ++polygon) { @@ -903,13 +926,15 @@ // x1,y1 and x2,y2 are in "slice" coordinates (the cutting plane // geometry) projected.push_back(std::make_pair(CreateRectangle( - static_cast(x1), - static_cast(y1), - static_cast(x2), - static_cast(y2)),curZ)); + static_cast(x1), + static_cast(y1), + static_cast(x2), + static_cast(y2)),curZ)); } } -#ifndef USE_BOOST_UNION_FOR_POLYGONS +#endif + +#if USE_BOOST_UNION_FOR_POLYGONS != 1 // projected contains a set of rectangles specified by two opposite // corners (x1,y1,x2,y2) // we need to merge them @@ -999,4 +1024,44 @@ return false; } } + + + void DicomStructureSet::ProjectOntoLayer(PolylineSceneLayer& layer, + const CoordinateSystem3D& plane, + size_t structureIndex, + const Color& color) const + { +#if USE_BOOST_UNION_FOR_POLYGONS == 1 + std::vector< std::vector > polygons; + if (ProjectStructure(polygons, structureIndex, plane)) + { + for (size_t j = 0; j < polygons.size(); j++) + { + std::vector chain; + chain.reserve(polygons[j].size()); + + for (size_t k = 0; k < polygons[j].size(); k++) + { + chain.push_back(ScenePoint2D(polygons[j][k].x, polygons[j][k].y)); + } + + layer.AddChain(chain, true, color.GetRed(), color.GetGreen(), color.GetBlue()); + } + } + +#else + std::vector< std::pair > segments; + + if (ProjectStructure(segments, structureIndex, plane)) + { + for (size_t j = 0; j < segments.size(); j++) + { + std::vector chain(2); + chain[0] = ScenePoint2D(segments[j].first.x, segments[j].first.y); + chain[1] = ScenePoint2D(segments[j].second.x, segments[j].second.y); + layer.AddChain(chain, false, color.GetRed(), color.GetGreen(), color.GetBlue()); + } + } +#endif + } } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Toolbox/DicomStructureSet.h --- a/Framework/Toolbox/DicomStructureSet.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Toolbox/DicomStructureSet.h Tue Feb 04 15:24:03 2020 +0100 @@ -21,10 +21,19 @@ #pragma once +#if !defined(ORTHANC_ENABLE_DCMTK) +# error The macro ORTHANC_ENABLE_DCMTK must be defined +#endif + #include "DicomStructureSetUtils.h" #include "CoordinateSystem3D.h" #include "Extent2D.h" #include "../Scene2D/Color.h" +#include "../Scene2D/PolylineSceneLayer.h" + +#if ORTHANC_ENABLE_DCMTK == 1 +# include +#endif //#define USE_BOOST_UNION_FOR_POLYGONS 1 @@ -137,21 +146,30 @@ Structures structures_; ReferencedSlices referencedSlices_; + void Setup(const OrthancPlugins::IDicomDataset& dataset); + const Structure& GetStructure(size_t index) const; Structure& GetStructure(size_t index); -#ifdef USE_BOOST_UNION_FOR_POLYGONS - bool ProjectStructure(std::vector< std::vector >& polygons, - const Structure& structure, - const CoordinateSystem3D& slice) const; + bool ProjectStructure( +#if USE_BOOST_UNION_FOR_POLYGONS == 1 + std::vector< std::vector >& polygons, #else - bool ProjectStructure(std::vector< std::pair >& segments, + std::vector< std::pair >& segments, +#endif const Structure& structure, const CoordinateSystem3D& slice) const; -#endif + public: - DicomStructureSet(const OrthancPlugins::FullOrthancDataset& instance); + DicomStructureSet(const OrthancPlugins::FullOrthancDataset& instance) + { + Setup(instance); + } + +#if ORTHANC_ENABLE_DCMTK == 1 + DicomStructureSet(Orthanc::ParsedDicomFile& instance); +#endif size_t GetStructuresCount() const { @@ -185,20 +203,32 @@ Vector GetNormal() const; -#ifdef USE_BOOST_UNION_FOR_POLYGONS +#if USE_BOOST_UNION_FOR_POLYGONS == 1 bool ProjectStructure(std::vector< std::vector >& polygons, - size_t index, - const CoordinateSystem3D& slice) const + size_t index, + const CoordinateSystem3D& slice) const { return ProjectStructure(polygons, GetStructure(index), slice); } #else bool ProjectStructure(std::vector< std::pair >& segments, - size_t index, - const CoordinateSystem3D& slice) const + size_t index, + const CoordinateSystem3D& slice) const { return ProjectStructure(segments, GetStructure(index), slice); } #endif + + void ProjectOntoLayer(PolylineSceneLayer& layer, + const CoordinateSystem3D& plane, + size_t structureIndex, + const Color& color) const; + + void ProjectOntoLayer(PolylineSceneLayer& layer, + const CoordinateSystem3D& plane, + size_t structureIndex) const + { + ProjectOntoLayer(layer, plane, structureIndex, GetStructureColor(structureIndex)); + } }; } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Toolbox/GenericToolbox.h diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Toolbox/LinearAlgebra.cpp --- a/Framework/Toolbox/LinearAlgebra.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Toolbox/LinearAlgebra.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -22,6 +22,7 @@ #include "LinearAlgebra.h" #include "../StoneException.h" +#include "GenericToolbox.h" #include #include @@ -32,6 +33,7 @@ #include #include +#include namespace OrthancStone { @@ -74,13 +76,30 @@ for (size_t i = 0; i < items.size(); i++) { /** + * SJO - 2019-11-19 - WARNING: I reverted from "std::stod()" + * to "boost::lexical_cast", as both "std::stod()" and + * "std::strtod()" are sensitive to locale settings, making + * this code non portable and very dangerous as it fails + * silently. A string such as "1.3671875\1.3671875" is + * interpreted as "1\1", because "std::stod()" expects a comma + * (",") instead of a point ("."). This problem is notably + * seen in Qt-based applications, that somehow set locales + * aggressively. + * + * "boost::lexical_cast<>" is also dependent on the locale + * settings, but apparently not in a way that makes this + * function fail with Qt. The Orthanc core defines macro + * "-DBOOST_LEXICAL_CAST_ASSUME_C_LOCALE" in static builds to + * this end. + **/ + +#if 0 // __cplusplus >= 201103L // Is C++11 enabled? + /** * We try and avoid the use of "boost::lexical_cast<>" here, - * as it is very slow. As we are parsing many doubles, we - * prefer to use the standard "std::stod" function if - * available: http://www.cplusplus.com/reference/string/stod/ + * as it is very slow, and as Stone has to parse many doubles. + * https://tinodidriksen.com/2011/05/cpp-convert-string-to-double-speed/ **/ -#if __cplusplus >= 201103L // Is C++11 enabled? try { target[i] = std::stod(items[i]); @@ -90,7 +109,37 @@ target.clear(); return false; } -#else // Fallback implementation using Boost + +#elif 0 + /** + * "std::strtod()" is the recommended alternative to + * "std::stod()". It is apparently as fast as plain-C + * "atof()", with more security. + **/ + char* end = NULL; + target[i] = std::strtod(items[i].c_str(), &end); + if (end == NULL || + end != items[i].c_str() + items[i].size()) + { + return false; + } + +#elif 1 + /** + * Use of our homemade implementation of + * "boost::lexical_cast()". It is much faster than boost. + **/ + if (!GenericToolbox::StringToDouble(target[i], items[i].c_str())) + { + return false; + } + +#else + /** + * Fallback implementation using Boost (slower, but somehow + * independent to locale contrarily to "std::stod()", and + * generic as it does not use our custom implementation). + **/ try { target[i] = boost::lexical_cast(items[i]); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Toolbox/ParsedDicomCache.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Toolbox/ParsedDicomCache.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,143 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "ParsedDicomCache.h" + +namespace OrthancStone +{ + class ParsedDicomCache::Item : public Orthanc::ICacheable + { + private: + std::auto_ptr dicom_; + size_t fileSize_; + bool hasPixelData_; + + public: + Item(Orthanc::ParsedDicomFile* dicom, + size_t fileSize, + bool hasPixelData) : + dicom_(dicom), + fileSize_(fileSize), + hasPixelData_(hasPixelData) + { + if (dicom == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + } + + virtual size_t GetMemoryUsage() const + { + return fileSize_; + } + + Orthanc::ParsedDicomFile& GetDicom() const + { + assert(dicom_.get() != NULL); + return *dicom_; + } + + bool HasPixelData() const + { + return hasPixelData_; + } + }; + + + std::string ParsedDicomCache::GetIndex(unsigned int bucket, + const std::string& bucketKey) + { + return boost::lexical_cast(bucket) + "|" + bucketKey; + } + + + void ParsedDicomCache::Acquire(unsigned int bucket, + const std::string& bucketKey, + Orthanc::ParsedDicomFile* dicom, + size_t fileSize, + bool hasPixelData) + { + LOG(TRACE) << "new item stored in cache: bucket " << bucket << ", key " << bucketKey; + cache_.Acquire(GetIndex(bucket, bucketKey), new Item(dicom, fileSize, hasPixelData)); + } + + + ParsedDicomCache::Reader::Reader(ParsedDicomCache& cache, + unsigned int bucket, + const std::string& bucketKey) : + /** + * The "DcmFileFormat" object cannot be accessed from multiple + * threads, even if using only getters. An unique lock (mutex) is + * mandatory. + **/ + accessor_(cache.cache_, GetIndex(bucket, bucketKey), true /* unique */) + { + if (accessor_.IsValid()) + { + LOG(TRACE) << "accessing item within cache: bucket " << bucket << ", key " << bucketKey; + item_ = &dynamic_cast(accessor_.GetValue()); + } + else + { + LOG(TRACE) << "missing item within cache: bucket " << bucket << ", key " << bucketKey; + item_ = NULL; + } + } + + + bool ParsedDicomCache::Reader::HasPixelData() const + { + if (item_ == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + return item_->HasPixelData(); + } + } + + + Orthanc::ParsedDicomFile& ParsedDicomCache::Reader::GetDicom() const + { + if (item_ == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + return item_->GetDicom(); + } + } + + + size_t ParsedDicomCache::Reader::GetFileSize() const + { + if (item_ == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + return item_->GetMemoryUsage(); + } + } +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Toolbox/ParsedDicomCache.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Toolbox/ParsedDicomCache.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,80 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include +#include + +namespace OrthancStone +{ + class ParsedDicomCache : public boost::noncopyable + { + private: + class Item; + + static std::string GetIndex(unsigned int bucket, + const std::string& bucketKey); + + Orthanc::MemoryObjectCache cache_; + + public: + ParsedDicomCache(size_t size) + { + cache_.SetMaximumSize(size); + } + + void Invalidate(unsigned int bucket, + const std::string& bucketKey) + { + cache_.Invalidate(GetIndex(bucket, bucketKey)); + } + + void Acquire(unsigned int bucket, + const std::string& bucketKey, + Orthanc::ParsedDicomFile* dicom, + size_t fileSize, + bool hasPixelData); + + class Reader : public boost::noncopyable + { + private: + Orthanc::MemoryObjectCache::Accessor accessor_; + Item* item_; + + public: + Reader(ParsedDicomCache& cache, + unsigned int bucket, + const std::string& bucketKey); + + bool IsValid() const + { + return item_ != NULL; + } + + bool HasPixelData() const; + + Orthanc::ParsedDicomFile& GetDicom() const; + + size_t GetFileSize() const; + }; + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Toolbox/ParsedDicomDataset.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Toolbox/ParsedDicomDataset.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,104 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "ParsedDicomDataset.h" + +#include + +namespace OrthancStone +{ + static DcmItem* LookupPath(Orthanc::ParsedDicomFile& dicom, + const OrthancPlugins::DicomPath& path) + { + DcmItem* node = dicom.GetDcmtkObject().getDataset(); + + for (size_t i = 0; i < path.GetPrefixLength(); i++) + { + const OrthancPlugins::DicomTag& tmp = path.GetPrefixTag(i); + DcmTagKey tag(tmp.GetGroup(), tmp.GetElement()); + + DcmSequenceOfItems* sequence = NULL; + if (!node->findAndGetSequence(tag, sequence).good() || + sequence == NULL) + { + return NULL; + } + + unsigned long pos = path.GetPrefixIndex(i); + if (pos >= sequence->card()) + { + return NULL; + } + + node = sequence->getItem(pos); + if (node == NULL) + { + return NULL; + } + } + + return node; + } + + + bool ParsedDicomDataset::GetStringValue(std::string& result, + const OrthancPlugins::DicomPath& path) const + { + DcmItem* node = LookupPath(dicom_, path); + + if (node != NULL) + { + DcmTagKey tag(path.GetFinalTag().GetGroup(), path.GetFinalTag().GetElement()); + + const char* s = NULL; + if (node->findAndGetString(tag, s).good() && + s != NULL) + { + result.assign(s); + return true; + } + } + + return false; + } + + + bool ParsedDicomDataset::GetSequenceSize(size_t& size, + const OrthancPlugins::DicomPath& path) const + { + DcmItem* node = LookupPath(dicom_, path); + + if (node != NULL) + { + DcmTagKey tag(path.GetFinalTag().GetGroup(), path.GetFinalTag().GetElement()); + + DcmSequenceOfItems* s = NULL; + if (node->findAndGetSequence(tag, s).good() && + s != NULL) + { + size = s->card(); + return true; + } + } + + return false; + } +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Toolbox/ParsedDicomDataset.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Toolbox/ParsedDicomDataset.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,46 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include +#include + +namespace OrthancStone +{ + class ParsedDicomDataset : public OrthancPlugins::IDicomDataset + { + private: + Orthanc::ParsedDicomFile& dicom_; + + public: + ParsedDicomDataset(Orthanc::ParsedDicomFile& dicom) : + dicom_(dicom) + { + } + + virtual bool GetStringValue(std::string& result, + const OrthancPlugins::DicomPath& path) const ORTHANC_OVERRIDE; + + virtual bool GetSequenceSize(size_t& size, + const OrthancPlugins::DicomPath& path) const ORTHANC_OVERRIDE; + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Toolbox/SlicesSorter.cpp --- a/Framework/Toolbox/SlicesSorter.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Toolbox/SlicesSorter.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -289,13 +289,14 @@ } - double SlicesSorter::ComputeSpacingBetweenSlices() const + bool SlicesSorter::ComputeSpacingBetweenSlices(double& spacing /* out */) const { if (GetSlicesCount() <= 1) { // This is a volume that is empty or that contains one single // slice: Choose a dummy z-dimension for voxels - return 1.0; + spacing = 1.0; + return true; } const OrthancStone::CoordinateSystem3D& reference = GetSliceGeometry(0); @@ -303,28 +304,27 @@ double referencePosition = reference.ProjectAlongNormal(reference.GetOrigin()); double p = reference.ProjectAlongNormal(GetSliceGeometry(1).GetOrigin()); - double spacingZ = p - referencePosition; + spacing = p - referencePosition; - if (spacingZ <= 0) + if (spacing <= 0) { - LOG(ERROR) << "SlicesSorter::ComputeSpacingBetweenSlices(): (spacingZ <= 0)"; + LOG(ERROR) << "SlicesSorter::ComputeSpacingBetweenSlices(): (spacing <= 0)"; throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, "Please call the Sort() method before"); } for (size_t i = 1; i < GetSlicesCount(); i++) { - OrthancStone::Vector p = reference.GetOrigin() + spacingZ * static_cast(i) * reference.GetNormal(); + OrthancStone::Vector p = reference.GetOrigin() + spacing * static_cast(i) * reference.GetNormal(); double d = boost::numeric::ublas::norm_2(p - GetSliceGeometry(i).GetOrigin()); if (!OrthancStone::LinearAlgebra::IsNear(d, 0, 0.001 /* tolerance expressed in mm */)) { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, - "The origins of the slices of a volume image are not regularly spaced"); + return false; } } - return spacingZ; + return true; } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Toolbox/SlicesSorter.h --- a/Framework/Toolbox/SlicesSorter.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Toolbox/SlicesSorter.h Tue Feb 04 15:24:03 2020 +0100 @@ -91,7 +91,7 @@ const CoordinateSystem3D& slice) const; // WARNING - The slices must have been sorted before calling this method - double ComputeSpacingBetweenSlices() const; + bool ComputeSpacingBetweenSlices(double& spacing /* out */) const; // WARNING - The slices must have been sorted before calling this method bool AreAllSlicesDistinct() const; diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Viewport/IViewport.h --- a/Framework/Viewport/IViewport.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Viewport/IViewport.h Tue Feb 04 15:24:03 2020 +0100 @@ -21,34 +21,44 @@ #pragma once #include "../Scene2D/ICompositor.h" -#include "../Scene2D/Scene2D.h" -#include "../Scene2D/ScenePoint2D.h" +#include "../Scene2DViewport/ViewportController.h" namespace OrthancStone { /** * Class that combines a Scene2D with a canvas where to draw the * scene. A call to "Refresh()" will update the content of the - * canvas. + * canvas. A "IViewport" can possibly be accessed from several + * threads depending on the rendering back-end (e.g. in SDL or Qt): + * The "ILock" subclass implements the locking mechanism to modify + * the content of the scene. + * + * NB: The lock must be a "recursive_mutex", as the viewport + * controller can lock it a second time (TODO - Why so?). **/ class IViewport : public boost::noncopyable { public: + class ILock : public boost::noncopyable + { + public: + virtual ~ILock() + { + } + + virtual bool HasCompositor() const = 0; + + virtual ICompositor& GetCompositor() = 0; + + virtual ViewportController& GetController() = 0; + + virtual void Invalidate() = 0; + }; + virtual ~IViewport() { } - virtual Scene2D& GetScene() = 0; - - virtual void Refresh() = 0; - - virtual ScenePoint2D GetPixelCenterCoordinates(int x, int y) const = 0; - - virtual bool HasCompositor() const = 0; - - virtual ICompositor& GetCompositor() = 0; - - virtual const ICompositor& GetCompositor() const = 0; + virtual ILock* Lock() = 0; }; } - diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Viewport/SdlViewport.cpp --- a/Framework/Viewport/SdlViewport.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Viewport/SdlViewport.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -22,44 +22,106 @@ #include -#include - namespace OrthancStone { + ICompositor& SdlViewport::SdlLock::GetCompositor() + { + if (that_.compositor_.get() == NULL) + { + // The derived class should have called "AcquireCompositor()" + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + else + { + return *that_.compositor_; + } + } + + + void SdlViewport::AcquireCompositor(ICompositor* compositor /* takes ownership */) + { + if (compositor == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + compositor_.reset(compositor); + } + + + SdlViewport::SdlViewport() : + controller_(new ViewportController) + { + refreshEvent_ = SDL_RegisterEvents(1); + + if (refreshEvent_ == static_cast(-1)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + + + void SdlViewport::SendRefreshEvent() + { + SDL_Event event; + SDL_memset(&event, 0, sizeof(event)); + event.type = refreshEvent_; + SDL_PushEvent(&event); // This function is thread-safe, and can be called from other threads safely. + } + + SdlOpenGLViewport::SdlOpenGLViewport(const char* title, unsigned int width, unsigned int height, bool allowDpiScaling) : context_(title, width, height, allowDpiScaling) { - compositor_.reset(new OpenGLCompositor(context_, GetScene())); + AcquireCompositor(new OpenGLCompositor(context_)); // (*) + } + + + SdlOpenGLViewport::~SdlOpenGLViewport() + { + // Make sure that the "OpenGLCompositor" is destroyed BEFORE the + // "OpenGLContext" it references (*) + ClearCompositor(); + } + + + void SdlOpenGLViewport::Paint() + { + SdlLock lock(*this); + lock.GetCompositor().Refresh(lock.GetController().GetScene()); } - SdlOpenGLViewport::SdlOpenGLViewport(const char* title, - unsigned int width, - unsigned int height, - boost::shared_ptr& scene, - bool allowDpiScaling) : - SdlViewport(scene), - context_(title, width, height, allowDpiScaling) + + void SdlOpenGLViewport::UpdateSize(unsigned int width, + unsigned int height) { - compositor_.reset(new OpenGLCompositor(context_, GetScene())); + // nothing to do in OpenGL, the OpenGLCompositor::UpdateSize will be called automatically + SdlLock lock(*this); + lock.Invalidate(); } - void SdlOpenGLViewport::Refresh() + + void SdlOpenGLViewport::ToggleMaximize() { - compositor_->Refresh(); + // No need to call "Invalidate()" here, as "UpdateSize()" will + // be invoked after event "SDL_WINDOWEVENT_SIZE_CHANGED" + SdlLock lock(*this); + context_.ToggleMaximize(); } + SdlCairoViewport::SdlCairoViewport(const char* title, unsigned int width, unsigned int height, bool allowDpiScaling) : window_(title, width, height, false /* enable OpenGL */, allowDpiScaling), - compositor_(GetScene(), width, height) + sdlSurface_(NULL) { - UpdateSdlSurfaceSize(width, height); + AcquireCompositor(new CairoCompositor(width, height)); } SdlCairoViewport::~SdlCairoViewport() @@ -70,29 +132,66 @@ } } - void SdlCairoViewport::Refresh() + void SdlCairoViewport::Paint() { - compositor_.Refresh(); - window_.Render(sdlSurface_); + SdlLock lock(*this); + + lock.GetCompositor().Refresh(lock.GetController().GetScene()); + CreateSdlSurfaceFromCompositor(dynamic_cast(lock.GetCompositor())); + + if (sdlSurface_ != NULL) + { + window_.Render(sdlSurface_); + } } + void SdlCairoViewport::UpdateSize(unsigned int width, unsigned int height) { - compositor_.UpdateSize(width, height); - UpdateSdlSurfaceSize(width, height); - Refresh(); + SdlLock lock(*this); + dynamic_cast(lock.GetCompositor()).UpdateSize(width, height); + lock.Invalidate(); } - void SdlCairoViewport::UpdateSdlSurfaceSize(unsigned int width, - unsigned int height) + + void SdlCairoViewport::ToggleMaximize() + { + // No need to call "Invalidate()" here, as "UpdateSize()" will + // be invoked after event "SDL_WINDOWEVENT_SIZE_CHANGED" + SdlLock lock(*this); + window_.ToggleMaximize(); + } + + + // Assumes that the mutex is locked + void SdlCairoViewport::CreateSdlSurfaceFromCompositor(CairoCompositor& compositor) { static const uint32_t rmask = 0x00ff0000; static const uint32_t gmask = 0x0000ff00; static const uint32_t bmask = 0x000000ff; - sdlSurface_ = SDL_CreateRGBSurfaceFrom((void*)(compositor_.GetCanvas().GetBuffer()), width, height, 32, - compositor_.GetCanvas().GetPitch(), rmask, gmask, bmask, 0); + const unsigned int width = compositor.GetCanvas().GetWidth(); + const unsigned int height = compositor.GetCanvas().GetHeight(); + + if (sdlSurface_ != NULL) + { + if (sdlSurface_->pixels == compositor.GetCanvas().GetBuffer() && + sdlSurface_->w == static_cast(width) && + sdlSurface_->h == static_cast(height) && + sdlSurface_->pitch == static_cast(compositor.GetCanvas().GetPitch())) + { + // The image from the compositor has not changed, no need to update the surface + return; + } + else + { + SDL_FreeSurface(sdlSurface_); + } + } + + sdlSurface_ = SDL_CreateRGBSurfaceFrom((void*)(compositor.GetCanvas().GetBuffer()), width, height, 32, + compositor.GetCanvas().GetPitch(), rmask, gmask, bmask, 0); if (!sdlSurface_) { LOG(ERROR) << "Cannot create a SDL surface from a Cairo surface"; diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Viewport/SdlViewport.h --- a/Framework/Viewport/SdlViewport.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Viewport/SdlViewport.h Tue Feb 04 15:24:03 2020 +0100 @@ -39,26 +39,81 @@ #include "../OpenGL/SdlOpenGLContext.h" #include "../Scene2D/OpenGLCompositor.h" #include "../Scene2D/CairoCompositor.h" -#include "ViewportBase.h" +#include "IViewport.h" + +#include namespace OrthancStone { - class SdlViewport : public ViewportBase + class SdlViewport : public IViewport { - public: - SdlViewport() + private: + boost::mutex mutex_; + uint32_t refreshEvent_; + boost::shared_ptr controller_; + std::auto_ptr compositor_; + + void SendRefreshEvent(); + + protected: + class SdlLock : public ILock { + private: + SdlViewport& that_; + boost::mutex::scoped_lock lock_; + + public: + SdlLock(SdlViewport& that) : + that_(that), + lock_(that.mutex_) + { + } + + virtual bool HasCompositor() const ORTHANC_OVERRIDE + { + return true; + } + + virtual ICompositor& GetCompositor() ORTHANC_OVERRIDE; + + virtual ViewportController& GetController() ORTHANC_OVERRIDE + { + return *that_.controller_; + } + + virtual void Invalidate() ORTHANC_OVERRIDE + { + that_.SendRefreshEvent(); + } + }; + + void ClearCompositor() + { + compositor_.reset(); } - SdlViewport(boost::shared_ptr& scene) : - ViewportBase(scene) + void AcquireCompositor(ICompositor* compositor /* takes ownership */); + + public: + SdlViewport(); + + bool IsRefreshEvent(const SDL_Event& event) const { + return (event.type == refreshEvent_); } - virtual SdlWindow& GetWindow() = 0; - + virtual ILock* Lock() ORTHANC_OVERRIDE + { + return new SdlLock(*this); + } + virtual void UpdateSize(unsigned int width, unsigned int height) = 0; + + virtual void ToggleMaximize() = 0; + + // Must be invoked from the main SDL thread + virtual void Paint() = 0; }; @@ -66,7 +121,6 @@ { private: SdlOpenGLContext context_; - std::auto_ptr compositor_; public: SdlOpenGLViewport(const char* title, @@ -74,46 +128,24 @@ unsigned int height, bool allowDpiScaling = true); - SdlOpenGLViewport(const char* title, - unsigned int width, - unsigned int height, - boost::shared_ptr& scene, - bool allowDpiScaling = true); + virtual ~SdlOpenGLViewport(); - virtual SdlWindow& GetWindow() ORTHANC_OVERRIDE - { - return context_.GetWindow(); - } - - virtual void Refresh() ORTHANC_OVERRIDE; + virtual void Paint() ORTHANC_OVERRIDE; - virtual void UpdateSize(unsigned int width, unsigned int height) ORTHANC_OVERRIDE - { - // nothing to do in OpenGL, the OpenGLCompositor::UpdateSize will be called automatically - } + virtual void UpdateSize(unsigned int width, + unsigned int height) ORTHANC_OVERRIDE; - virtual bool HasCompositor() const ORTHANC_OVERRIDE - { - return true; - } - - virtual ICompositor& GetCompositor() ORTHANC_OVERRIDE - { - return *compositor_.get(); - } + virtual void ToggleMaximize() ORTHANC_OVERRIDE; }; class SdlCairoViewport : public SdlViewport { private: - SdlWindow window_; - CairoCompositor compositor_; - SDL_Surface* sdlSurface_; + SdlWindow window_; + SDL_Surface* sdlSurface_; - private: - void UpdateSdlSurfaceSize(unsigned int width, - unsigned int height); + void CreateSdlSurfaceFromCompositor(CairoCompositor& compositor); public: SdlCairoViewport(const char* title, @@ -121,32 +153,13 @@ unsigned int height, bool allowDpiScaling = true); - SdlCairoViewport(const char* title, - unsigned int width, - unsigned int height, - boost::shared_ptr& scene, - bool allowDpiScaling = true); + virtual ~SdlCairoViewport(); - ~SdlCairoViewport(); - - virtual SdlWindow& GetWindow() ORTHANC_OVERRIDE - { - return window_; - } - - virtual void Refresh() ORTHANC_OVERRIDE; + virtual void Paint() ORTHANC_OVERRIDE; virtual void UpdateSize(unsigned int width, unsigned int height) ORTHANC_OVERRIDE; - virtual bool HasCompositor() const ORTHANC_OVERRIDE - { - return true; - } - - virtual ICompositor& GetCompositor() ORTHANC_OVERRIDE - { - return compositor_; - } + virtual void ToggleMaximize() ORTHANC_OVERRIDE; }; } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Viewport/SdlWindow.cpp --- a/Framework/Viewport/SdlWindow.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Viewport/SdlWindow.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -31,6 +31,8 @@ #endif // WIN32 +#include +#include #include namespace OrthancStone @@ -154,7 +156,14 @@ void SdlWindow::Render(SDL_Surface* surface) { - //SDL_RenderClear(renderer_); + /** + * "You are strongly encouraged to call SDL_RenderClear() to + * initialize the backbuffer before starting each new frame's + * drawing, even if you plan to overwrite every pixel." + * https://wiki.libsdl.org/SDL_RenderPresent + **/ + SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255); + SDL_RenderClear(renderer_); // Clear the entire screen to our selected color SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer_, surface); if (texture != NULL) diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Viewport/SdlWindow.h --- a/Framework/Viewport/SdlWindow.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Viewport/SdlWindow.h Tue Feb 04 15:24:03 2020 +0100 @@ -23,18 +23,22 @@ #if ORTHANC_ENABLE_SDL == 1 -#include -#include #include +// Forward declaration of SDL type to avoid clashes with DCMTK headers +// on "typedef Sint8", in "StoneInitialization.cpp" +struct SDL_Window; +struct SDL_Renderer; +struct SDL_Surface; + namespace OrthancStone { class SdlWindow : public boost::noncopyable { private: - SDL_Window *window_; - SDL_Renderer *renderer_; - bool maximized_; + struct SDL_Window *window_; + struct SDL_Renderer *renderer_; + bool maximized_; public: SdlWindow(const char* title, @@ -54,7 +58,12 @@ unsigned int GetHeight() const; - void Render(SDL_Surface* surface); + /** + * WARNING: "Refresh()" cannot only be called from the main SDL + * thread, in which the window was created. Otherwise, the + * renderer displays nothing! + **/ + void Render(struct SDL_Surface* surface); void ToggleMaximize(); diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Viewport/ViewportBase.cpp --- a/Framework/Viewport/ViewportBase.cpp Tue Feb 04 15:23:33 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,59 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - -#include "ViewportBase.h" - -#include - -#include - -namespace OrthancStone -{ - ViewportBase::ViewportBase() : - scene_(boost::make_shared()) - { - } - - - ViewportBase::ViewportBase(boost::shared_ptr& scene) : - scene_(scene) - { - if (scene.get() == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - } - - - ScenePoint2D ViewportBase::GetPixelCenterCoordinates(int x, int y) const - { - if (HasCompositor()) - { - const ICompositor& compositor = GetCompositor(); - return ScenePoint2D( - static_cast(x) + 0.5 - static_cast(compositor.GetCanvasWidth()) / 2.0, - static_cast(y) + 0.5 - static_cast(compositor.GetCanvasHeight()) / 2.0); - } - else - { - return ScenePoint2D(0, 0); - } - } -} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Viewport/ViewportBase.h --- a/Framework/Viewport/ViewportBase.h Tue Feb 04 15:23:33 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,54 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - -#pragma once - -#include "IViewport.h" - -#include - -namespace OrthancStone -{ - class ViewportBase : public IViewport - { - private: - boost::shared_ptr scene_; - - public: - ViewportBase(); - - ViewportBase(boost::shared_ptr& scene); - - virtual Scene2D& GetScene() ORTHANC_OVERRIDE - { - return *scene_; - } - - virtual ScenePoint2D GetPixelCenterCoordinates(int x, int y) const ORTHANC_OVERRIDE; - - virtual const ICompositor& GetCompositor() const ORTHANC_OVERRIDE - { - IViewport* mutableThis = - const_cast(static_cast(this)); - return mutableThis->GetCompositor(); - } - - }; -} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Viewport/WebAssemblyCairoViewport.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Viewport/WebAssemblyCairoViewport.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,133 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "WebAssemblyCairoViewport.h" + +#include "../Scene2D/CairoCompositor.h" + +#include + +namespace OrthancStone +{ + void WebAssemblyCairoViewport::GetCanvasSize(unsigned int& width, + unsigned int& height) + { + double w, h; + emscripten_get_element_css_size(GetFullCanvasId().c_str(), &w, &h); + + /** + * Emscripten has the function emscripten_get_element_css_size() + * to query the width and height of a named HTML element. I'm + * calling this first to get the initial size of the canvas DOM + * element, and then call emscripten_set_canvas_size() to + * initialize the framebuffer size of the canvas to the same + * size as its DOM element. + * https://floooh.github.io/2017/02/22/emsc-html.html + **/ + if (w > 0 && + h > 0) + { + width = static_cast(boost::math::iround(w)); + height = static_cast(boost::math::iround(h)); + } + else + { + width = 0; + height = 0; + } + } + + + void WebAssemblyCairoViewport::Paint(ICompositor& compositor, + ViewportController& controller) + { + compositor.Refresh(controller.GetScene()); + + // Create a temporary memory buffer for the canvas in JavaScript + Orthanc::ImageAccessor cairo; + dynamic_cast(compositor).GetCanvas().GetReadOnlyAccessor(cairo); + + const unsigned int width = cairo.GetWidth(); + const unsigned int height = cairo.GetHeight(); + + if (javascript_.get() == NULL || + javascript_->GetWidth() != width || + javascript_->GetHeight() != height) + { + javascript_.reset(new Orthanc::Image(Orthanc::PixelFormat_RGBA32, width, height, + true /* force minimal pitch */)); + } + + // Convert from BGRA32 memory layout (only color mode supported + // by Cairo, which corresponds to CAIRO_FORMAT_ARGB32) to RGBA32 + // (as expected by HTML5 canvas). This simply amounts to + // swapping the B and R channels. Alpha channel is also set to + // full opacity (255). + uint8_t* q = reinterpret_cast(javascript_->GetBuffer()); + for (unsigned int y = 0; y < height; y++) + { + const uint8_t* p = reinterpret_cast(cairo.GetConstRow(y)); + for (unsigned int x = 0; x < width; x++) + { + q[0] = p[2]; // R + q[1] = p[1]; // G + q[2] = p[0]; // B + q[3] = 255; // A + + p += 4; + q += 4; + } + } + + // Execute JavaScript commands to blit the image buffer onto the + // 2D drawing context of the HTML5 canvas + EM_ASM({ + const data = new Uint8ClampedArray(Module.HEAP8.buffer, $1, 4 * $2 * $3); + const img = new ImageData(data, $2, $3); + const ctx = document.getElementById(UTF8ToString($0)).getContext('2d'); + ctx.putImageData(img, 0, 0); + }, + GetShortCanvasId().c_str(), // $0 + javascript_->GetBuffer(), // $1 + javascript_->GetWidth(), // $2 + javascript_->GetHeight()); // $3 + } + + + void WebAssemblyCairoViewport::UpdateSize(ICompositor& compositor) + { + unsigned int width, height; + GetCanvasSize(width, height); + emscripten_set_canvas_element_size(GetFullCanvasId().c_str(), width, height); + + dynamic_cast(compositor).UpdateSize(width, height); + } + + + WebAssemblyCairoViewport::WebAssemblyCairoViewport(const std::string& canvasId) : + WebAssemblyViewport(canvasId, NULL) + { + unsigned int width, height; + GetCanvasSize(width, height); + emscripten_set_canvas_element_size(GetFullCanvasId().c_str(), width, height); + AcquireCompositor(new CairoCompositor(width, height)); + } +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Viewport/WebAssemblyCairoViewport.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Viewport/WebAssemblyCairoViewport.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,50 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "WebAssemblyViewport.h" + +namespace OrthancStone +{ + class WebAssemblyCairoViewport : public WebAssemblyViewport + { + private: + std::auto_ptr javascript_; + + void GetCanvasSize(unsigned int& width, + unsigned int& height); + + protected: + virtual void Paint(ICompositor& compositor, + ViewportController& controller) ORTHANC_OVERRIDE; + + virtual void UpdateSize(ICompositor& compositor) ORTHANC_OVERRIDE; + + public: + WebAssemblyCairoViewport(const std::string& canvasId); + + virtual ~WebAssemblyCairoViewport() + { + ClearCompositor(); + } + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Viewport/WebAssemblyViewport.cpp --- a/Framework/Viewport/WebAssemblyViewport.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Viewport/WebAssemblyViewport.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -21,311 +21,248 @@ #include "WebAssemblyViewport.h" -#include "../StoneException.h" +#include -#include +#include namespace OrthancStone { - WebAssemblyOpenGLViewport::WebAssemblyOpenGLViewport(const std::string& canvas) - : WebAssemblyViewport(canvas) - , context_(canvas) - , cssWidth_(0) // will be set in Refresh() - , cssHeight_(0) // ditto - , pixelWidth_(0) // ditto - , pixelHeight_(0) // ditto + static void ConvertMouseEvent(PointerEvent& target, + const EmscriptenMouseEvent& source, + const ICompositor& compositor) { - compositor_.reset(new OpenGLCompositor(context_, GetScene())); - RegisterContextCallbacks(); - } + int x = static_cast(source.targetX); + int y = static_cast(source.targetY); - WebAssemblyOpenGLViewport::WebAssemblyOpenGLViewport(const std::string& canvas, - boost::shared_ptr& scene) - : WebAssemblyViewport(canvas, scene) - , context_(canvas) - , cssWidth_(0) // will be set in Refresh() - , cssHeight_(0) // ditto - , pixelWidth_(0) // ditto - , pixelHeight_(0) // ditto - { - compositor_.reset(new OpenGLCompositor(context_, GetScene())); - RegisterContextCallbacks(); - } + switch (source.button) + { + case 0: + target.SetMouseButton(MouseButton_Left); + break; - void WebAssemblyOpenGLViewport::UpdateSize() - { - context_.UpdateSize(); // First read the size of the canvas + case 1: + target.SetMouseButton(MouseButton_Middle); + break; - if (compositor_.get() != NULL) - { - compositor_->Refresh(); // Then refresh the content of the canvas + case 2: + target.SetMouseButton(MouseButton_Right); + break; + + default: + target.SetMouseButton(MouseButton_None); + break; } - } - - /* - typedef EM_BOOL (*em_webgl_context_callback)(int eventType, const void *reserved, void *userData); - - EMSCRIPTEN_EVENT_WEBGLCONTEXTLOST EMSCRIPTEN_EVENT_WEBGLCONTEXTRESTORED - - EMSCRIPTEN_RESULT emscripten_set_webglcontextlost_callback( - const char *target, void *userData, EM_BOOL useCapture, em_webgl_context_callback callback) - - EMSCRIPTEN_RESULT emscripten_set_webglcontextrestored_callback( - const char *target, void *userData, EM_BOOL useCapture, em_webgl_context_callback callback) - - */ - - EM_BOOL WebAssemblyOpenGLViewport_OpenGLContextLost_callback( - int eventType, const void* reserved, void* userData) - { - ORTHANC_ASSERT(eventType == EMSCRIPTEN_EVENT_WEBGLCONTEXTLOST); - WebAssemblyOpenGLViewport* viewport = reinterpret_cast(userData); - return viewport->OpenGLContextLost(); - } - - EM_BOOL WebAssemblyOpenGLViewport_OpenGLContextRestored_callback( - int eventType, const void* reserved, void* userData) - { - ORTHANC_ASSERT(eventType == EMSCRIPTEN_EVENT_WEBGLCONTEXTRESTORED); - WebAssemblyOpenGLViewport* viewport = reinterpret_cast(userData); - return viewport->OpenGLContextRestored(); - } - - void WebAssemblyOpenGLViewport::DisableCompositor() - { - compositor_.reset(); + + target.AddPosition(compositor.GetPixelCenterCoordinates(x, y)); + target.SetAltModifier(source.altKey); + target.SetControlModifier(source.ctrlKey); + target.SetShiftModifier(source.shiftKey); } - ICompositor& WebAssemblyOpenGLViewport::GetCompositor() - { - if (compositor_.get() == NULL) - { - // "HasCompositor()" should have been called - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - else - { - return *compositor_; - } - } - void WebAssemblyOpenGLViewport::UpdateSizeIfNeeded() + class WebAssemblyViewport::WasmLock : public ILock { - bool needsRefresh = false; - std::string canvasId = GetCanvasIdentifier(); - - { - double cssWidth = 0; - double cssHeight = 0; - EMSCRIPTEN_RESULT res = EMSCRIPTEN_RESULT_SUCCESS; - res = - emscripten_get_element_css_size(canvasId.c_str(), &cssWidth, &cssHeight); + private: + WebAssemblyViewport& that_; - if (res == EMSCRIPTEN_RESULT_SUCCESS) - { - if ((cssWidth != cssWidth_) || (cssHeight != cssHeight_)) - { - cssWidth_ = cssWidth; - cssHeight_ = cssHeight; - needsRefresh = true; - } - } + public: + WasmLock(WebAssemblyViewport& that) : + that_(that) + { } + virtual bool HasCompositor() const ORTHANC_OVERRIDE { - int pixelWidth = 0; - int pixelHeight = 0; - EMSCRIPTEN_RESULT res = EMSCRIPTEN_RESULT_SUCCESS; - res = - emscripten_get_canvas_element_size(canvasId.c_str(), &pixelWidth, &pixelHeight); - - if (res == EMSCRIPTEN_RESULT_SUCCESS) - { - if ((pixelWidth != pixelWidth_) || (pixelHeight != pixelHeight_)) - { - pixelWidth_ = pixelWidth; - pixelHeight_ = pixelHeight; - needsRefresh = true; - } - } + return that_.compositor_.get() != NULL; } - if (needsRefresh) - UpdateSize(); - } - - void WebAssemblyOpenGLViewport::Refresh() - { - try + virtual ICompositor& GetCompositor() ORTHANC_OVERRIDE { - // first, we check if the canvas size (both css size in css pixels and - // backing store) have changed. if so, we call UpdateSize to deal with - // it - - LOG(INFO) << "updating cairo viewport size"; - - // maybe the canvas size has changed and we need to update the - // canvas backing store size - UpdateSizeIfNeeded(); - - if (HasCompositor()) + if (that_.compositor_.get() == NULL) { - GetCompositor().Refresh(); + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } else { - // this block was added because of (perceived?) bugs in the - // browser where the WebGL contexts are NOT automatically restored - // after being lost. - // the WebGL context has been lost. Sce - - //LOG(ERROR) << "About to call WebAssemblyOpenGLContext::TryRecreate()."; - //LOG(ERROR) << "Before calling it, isContextLost == " << context_.IsContextLost(); - - if (!context_.IsContextLost()) - { - LOG(TRACE) << "Context restored!"; - //LOG(ERROR) << "After calling it, isContextLost == " << context_.IsContextLost(); - RestoreCompositor(); - UpdateSize(); - } + return *that_.compositor_; } } - catch (const StoneException& e) + + virtual ViewportController& GetController() ORTHANC_OVERRIDE + { + assert(that_.controller_); + return *that_.controller_; + } + + virtual void Invalidate() ORTHANC_OVERRIDE + { + that_.Invalidate(); + } + }; + + + EM_BOOL WebAssemblyViewport::OnRequestAnimationFrame(double time, void *userData) + { + WebAssemblyViewport& that = *reinterpret_cast(userData); + + if (that.compositor_.get() != NULL && + that.controller_ /* should always be true */) + { + that.Paint(*that.compositor_, *that.controller_); + } + + return true; + } + + + EM_BOOL WebAssemblyViewport::OnResize(int eventType, const EmscriptenUiEvent *uiEvent, void *userData) + { + WebAssemblyViewport& that = *reinterpret_cast(userData); + + if (that.compositor_.get() != NULL) { - if (e.GetErrorCode() == ErrorCode_WebGLContextLost) + that.UpdateSize(*that.compositor_); + that.Invalidate(); + } + + return true; + } + + + EM_BOOL WebAssemblyViewport::OnMouseDown(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData) + { + WebAssemblyViewport& that = *reinterpret_cast(userData); + + LOG(INFO) << "mouse down: " << that.GetFullCanvasId(); + + if (that.compositor_.get() != NULL && + that.interactor_.get() != NULL) + { + PointerEvent pointer; + ConvertMouseEvent(pointer, *mouseEvent, *that.compositor_); + + that.controller_->HandleMousePress(*that.interactor_, pointer, + that.compositor_->GetCanvasWidth(), + that.compositor_->GetCanvasHeight()); + that.Invalidate(); + } + + return true; + } + + + EM_BOOL WebAssemblyViewport::OnMouseMove(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData) + { + WebAssemblyViewport& that = *reinterpret_cast(userData); + + if (that.compositor_.get() != NULL && + that.controller_->HasActiveTracker()) + { + PointerEvent pointer; + ConvertMouseEvent(pointer, *mouseEvent, *that.compositor_); + if (that.controller_->HandleMouseMove(pointer)) { - LOG(WARNING) << "Context is lost! Compositor will be disabled."; - DisableCompositor(); - // we now need to wait for the "context restored" callback - } - else - { - throw; + that.Invalidate(); } } - catch (...) - { - // something else nasty happened - throw; - } - } - void WebAssemblyOpenGLViewport::RestoreCompositor() - { - // the context must have been restored! - ORTHANC_ASSERT(!context_.IsContextLost()); - if (compositor_.get() == NULL) - { - compositor_.reset(new OpenGLCompositor(context_, GetScene())); - } - else - { - LOG(WARNING) << "RestoreCompositor() called for \"" << GetCanvasIdentifier() << "\" while it was NOT lost! Nothing done."; - } - } - - bool WebAssemblyOpenGLViewport::OpenGLContextLost() - { - LOG(ERROR) << "WebAssemblyOpenGLViewport::OpenGLContextLost() for canvas: " << GetCanvasIdentifier(); - DisableCompositor(); return true; } - bool WebAssemblyOpenGLViewport::OpenGLContextRestored() + + EM_BOOL WebAssemblyViewport::OnMouseUp(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData) { - LOG(ERROR) << "WebAssemblyOpenGLViewport::OpenGLContextRestored() for canvas: " << GetCanvasIdentifier(); - - // maybe the context has already been restored by other means (the - // Refresh() function) - if (!HasCompositor()) + WebAssemblyViewport& that = *reinterpret_cast(userData); + + if (that.compositor_.get() != NULL) { - RestoreCompositor(); - UpdateSize(); + PointerEvent pointer; + ConvertMouseEvent(pointer, *mouseEvent, *that.compositor_); + that.controller_->HandleMouseRelease(pointer); + that.Invalidate(); } - return false; + + return true; } - void WebAssemblyOpenGLViewport::RegisterContextCallbacks() + + void WebAssemblyViewport::Invalidate() { -#if 0 - // DISABLED ON 2019-08-20 and replaced by external JS calls because I could - // not get emscripten API to work - // TODO: what's the impact of userCapture=true ? - const char* canvasId = GetCanvasIdentifier().c_str(); - void* that = reinterpret_cast(this); - EMSCRIPTEN_RESULT status = EMSCRIPTEN_RESULT_SUCCESS; + emscripten_request_animation_frame(OnRequestAnimationFrame, this); + } + - //status = emscripten_set_webglcontextlost_callback(canvasId, that, true, WebAssemblyOpenGLViewport_OpenGLContextLost_callback); - //if (status != EMSCRIPTEN_RESULT_SUCCESS) - //{ - // std::stringstream ss; - // ss << "Error while calling emscripten_set_webglcontextlost_callback for: \"" << GetCanvasIdentifier() << "\""; - // std::string msg = ss.str(); - // LOG(ERROR) << msg; - // ORTHANC_ASSERT(false, msg.c_str()); - //} - - status = emscripten_set_webglcontextrestored_callback(canvasId, that, true, WebAssemblyOpenGLViewport_OpenGLContextRestored_callback); - if (status != EMSCRIPTEN_RESULT_SUCCESS) + void WebAssemblyViewport::AcquireCompositor(ICompositor* compositor /* takes ownership */) + { + if (compositor == NULL) { - std::stringstream ss; - ss << "Error while calling emscripten_set_webglcontextrestored_callback for: \"" << GetCanvasIdentifier() << "\""; - std::string msg = ss.str(); - LOG(ERROR) << msg; - ORTHANC_ASSERT(false, msg.c_str()); + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); } - LOG(TRACE) << "WebAssemblyOpenGLViewport::RegisterContextCallbacks() SUCCESS!!!"; -#endif + else + { + compositor_.reset(compositor); + } } - WebAssemblyCairoViewport::WebAssemblyCairoViewport(const std::string& canvas) : - WebAssemblyViewport(canvas), - canvas_(canvas), - compositor_(GetScene(), 1024, 768) + + WebAssemblyViewport::WebAssemblyViewport(const std::string& canvasId, + const Scene2D* scene) : + shortCanvasId_(canvasId), + fullCanvasId_("#" + canvasId), + interactor_(new DefaultViewportInteractor) { - } + if (scene == NULL) + { + controller_ = boost::make_shared(); + } + else + { + controller_ = boost::make_shared(*scene); + } + + LOG(INFO) << "Initializing Stone viewport on HTML canvas: " << canvasId; - WebAssemblyCairoViewport::WebAssemblyCairoViewport(const std::string& canvas, - boost::shared_ptr& scene) : - WebAssemblyViewport(canvas, scene), - canvas_(canvas), - compositor_(GetScene(), 1024, 768) - { + if (canvasId.empty() || + canvasId[0] == '#') + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, + "The canvas identifier must not start with '#'"); + } + + // Disable right-click on the canvas (i.e. context menu) + EM_ASM({ + document.getElementById(UTF8ToString($0)).oncontextmenu = function(event) { + event.preventDefault(); + } + }, + canvasId.c_str() // $0 + ); + + // It is not possible to monitor the resizing of individual + // canvas, so we track the full window of the browser + emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, this, false, OnResize); + + emscripten_set_mousedown_callback(fullCanvasId_.c_str(), this, false, OnMouseDown); + emscripten_set_mousemove_callback(fullCanvasId_.c_str(), this, false, OnMouseMove); + emscripten_set_mouseup_callback(fullCanvasId_.c_str(), this, false, OnMouseUp); } - void WebAssemblyCairoViewport::UpdateSize() + + IViewport::ILock* WebAssemblyViewport::Lock() { - LOG(INFO) << "updating cairo viewport size"; - double w, h; - emscripten_get_element_css_size(canvas_.c_str(), &w, &h); - - /** - * Emscripten has the function emscripten_get_element_css_size() - * to query the width and height of a named HTML element. I'm - * calling this first to get the initial size of the canvas DOM - * element, and then call emscripten_set_canvas_size() to - * initialize the framebuffer size of the canvas to the same - * size as its DOM element. - * https://floooh.github.io/2017/02/22/emsc-html.html - **/ - unsigned int canvasWidth = 0; - unsigned int canvasHeight = 0; - - if (w > 0 || - h > 0) - { - canvasWidth = static_cast(boost::math::iround(w)); - canvasHeight = static_cast(boost::math::iround(h)); - } - - emscripten_set_canvas_element_size(canvas_.c_str(), canvasWidth, canvasHeight); - compositor_.UpdateSize(canvasWidth, canvasHeight); + return new WasmLock(*this); } - void WebAssemblyCairoViewport::Refresh() + + void WebAssemblyViewport::AcquireInteractor(IViewportInteractor* interactor) { - LOG(INFO) << "refreshing cairo viewport, TODO: blit to the canvans.getContext('2d')"; - GetCompositor().Refresh(); + if (interactor == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + else + { + interactor_.reset(interactor); + } } } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Viewport/WebAssemblyViewport.h --- a/Framework/Viewport/WebAssemblyViewport.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Viewport/WebAssemblyViewport.h Tue Feb 04 15:24:03 2020 +0100 @@ -13,7 +13,7 @@ * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . **/ @@ -21,113 +21,78 @@ #pragma once -#include "../OpenGL/WebAssemblyOpenGLContext.h" -#include "../Scene2D/OpenGLCompositor.h" -#include "../Scene2D/CairoCompositor.h" -#include "ViewportBase.h" +#if !defined(ORTHANC_ENABLE_WASM) +# error Macro ORTHANC_ENABLE_WASM must be defined +#endif + +#if ORTHANC_ENABLE_WASM != 1 +# error This file can only be used if targeting WebAssembly +#endif + +#include "IViewport.h" + +#include +#include namespace OrthancStone { - class WebAssemblyViewport : public ViewportBase + class WebAssemblyViewport : public IViewport { private: - std::string canvasIdentifier_; + class WasmLock; + + std::string shortCanvasId_; + std::string fullCanvasId_; + std::auto_ptr compositor_; + boost::shared_ptr controller_; + std::auto_ptr interactor_; - public: - WebAssemblyViewport(const std::string& canvasIdentifier) - : canvasIdentifier_(canvasIdentifier) + static EM_BOOL OnRequestAnimationFrame(double time, void *userData); + + static EM_BOOL OnResize(int eventType, const EmscriptenUiEvent *uiEvent, void *userData); + + static EM_BOOL OnMouseDown(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData); + + static EM_BOOL OnMouseMove(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData); + + static EM_BOOL OnMouseUp(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData); + + protected: + void Invalidate(); + + void ClearCompositor() { - } - - WebAssemblyViewport(const std::string& canvasIdentifier, - boost::shared_ptr& scene) - : ViewportBase(scene) - , canvasIdentifier_(canvasIdentifier) - { + compositor_.reset(); } - const std::string& GetCanvasIdentifier() const + bool HasCompositor() const { - return canvasIdentifier_; + return compositor_.get() != NULL; } - }; - - class WebAssemblyOpenGLViewport : public WebAssemblyViewport - { - private: - OpenGL::WebAssemblyOpenGLContext context_; - std::auto_ptr compositor_; - double cssWidth_; - double cssHeight_; - int pixelWidth_; - int pixelHeight_; + void AcquireCompositor(ICompositor* compositor /* takes ownership */); - private: - void UpdateSizeIfNeeded(); + virtual void Paint(ICompositor& compositor, + ViewportController& controller) = 0; + + virtual void UpdateSize(ICompositor& compositor) = 0; public: - WebAssemblyOpenGLViewport(const std::string& canvas); - - WebAssemblyOpenGLViewport(const std::string& canvas, - boost::shared_ptr& scene); - - // This function must be called each time the browser window is resized - void UpdateSize(); + WebAssemblyViewport(const std::string& canvasId, + const Scene2D* scene); + + virtual ILock* Lock() ORTHANC_OVERRIDE; - virtual bool HasCompositor() const ORTHANC_OVERRIDE + void AcquireInteractor(IViewportInteractor* interactor); + + const std::string& GetShortCanvasId() const { - return (compositor_.get() != NULL); - } - - bool IsContextLost() - { - return context_.IsContextLost(); + return shortCanvasId_; } - virtual ICompositor& GetCompositor() ORTHANC_OVERRIDE; - - virtual void Refresh() ORTHANC_OVERRIDE; - - // this does NOT return whether the context is lost! This is called to - // tell Stone that the context has been lost - bool OpenGLContextLost(); - - // This should be called to indicate that the context has been lost - bool OpenGLContextRestored(); - - private: - void DisableCompositor(); - void RestoreCompositor(); - - void RegisterContextCallbacks(); - }; - - - class WebAssemblyCairoViewport : public WebAssemblyViewport - { - private: - CairoCompositor compositor_; - std::string canvas_; - - public: - WebAssemblyCairoViewport(const std::string& canvas); - - WebAssemblyCairoViewport(const std::string& canvas, - boost::shared_ptr& scene); - - void UpdateSize(); - - virtual void Refresh() ORTHANC_OVERRIDE; - - virtual bool HasCompositor() const ORTHANC_OVERRIDE + const std::string& GetFullCanvasId() const { - return true; - } - - virtual ICompositor& GetCompositor() ORTHANC_OVERRIDE - { - return compositor_; + return fullCanvasId_; } }; } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Viewport/WebGLViewport.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Viewport/WebGLViewport.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,105 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "WebGLViewport.h" + +#include "../StoneException.h" +#include "../Scene2D/OpenGLCompositor.h" + +namespace OrthancStone +{ + void WebGLViewport::Paint(ICompositor& compositor, + ViewportController& controller) + { + try + { + compositor.Refresh(controller.GetScene()); + + /** + * No need to manually swap the buffer: "Rendered WebGL content + * is implicitly presented (displayed to the user) on the canvas + * when the event handler that renders with WebGL returns back + * to the browser event loop." + * https://emscripten.org/docs/api_reference/html5.h.html#webgl-context + * + * Could call "emscripten_webgl_commit_frame()" if + * "explicitSwapControl" option were set to "true". + **/ + } + catch (const StoneException& e) + { + // Ignore problems about the loss of the WebGL context (edge case) + if (e.GetErrorCode() == ErrorCode_WebGLContextLost) + { + return; + } + else + { + throw; + } + } + } + + + void WebGLViewport::UpdateSize(ICompositor& compositor) + { + try + { + context_.UpdateSize(); + } + catch (const StoneException& e) + { + // Ignore problems about the loss of the WebGL context (edge case) + if (e.GetErrorCode() == ErrorCode_WebGLContextLost) + { + return; + } + else + { + throw; + } + } + } + + + WebGLViewport::WebGLViewport(const std::string& canvasId) : + WebAssemblyViewport(canvasId, NULL), + context_(GetFullCanvasId()) + { + AcquireCompositor(new OpenGLCompositor(context_)); + } + + + WebGLViewport::WebGLViewport(const std::string& canvasId, + const Scene2D& scene) : + WebAssemblyViewport(canvasId, &scene), + context_(GetFullCanvasId()) + { + AcquireCompositor(new OpenGLCompositor(context_)); + } + + + WebGLViewport::~WebGLViewport() + { + // Make sure to delete the compositor before its parent "context_" gets deleted + ClearCompositor(); + } +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Viewport/WebGLViewport.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Viewport/WebGLViewport.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,53 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "WebAssemblyViewport.h" +#include "../OpenGL/WebAssemblyOpenGLContext.h" + +namespace OrthancStone +{ + class WebGLViewport : public WebAssemblyViewport + { + private: + OpenGL::WebAssemblyOpenGLContext context_; + + protected: + virtual void Paint(ICompositor& compositor, + ViewportController& controller) ORTHANC_OVERRIDE; + + virtual void UpdateSize(ICompositor& compositor) ORTHANC_OVERRIDE; + + public: + WebGLViewport(const std::string& canvasId); + + WebGLViewport(const std::string& canvasId, + const Scene2D& scene); + + virtual ~WebGLViewport(); + + bool IsContextLost() + { + return context_.IsContextLost(); + } + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Viewport/WebGLViewportsRegistry.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Viewport/WebGLViewportsRegistry.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,165 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "WebGLViewportsRegistry.h" + +#include + +#include + +namespace OrthancStone +{ + void WebGLViewportsRegistry::LaunchTimer() + { + emscripten_set_timeout(OnTimeoutCallback, timeoutMS_, this); + } + + + void WebGLViewportsRegistry::OnTimeout() + { + for (Viewports::iterator it = viewports_.begin(); it != viewports_.end(); ++it) + { + if (it->second == NULL || + it->second->IsContextLost()) + { + LOG(INFO) << "WebGL context lost for canvas: " << it->first; + + // Try and duplicate the HTML5 canvas in the DOM + EM_ASM({ + var canvas = document.getElementById(UTF8ToString($0)); + if (canvas) { + var parent = canvas.parentElement; + if (parent) { + var cloned = canvas.cloneNode(true /* deep copy */); + parent.insertBefore(cloned, canvas); + parent.removeChild(canvas); + } + } + }, + it->first.c_str() // $0 = ID of the canvas + ); + + // At this point, the old canvas is removed from the DOM and + // replaced by a fresh one with the same ID: Recreate the + // WebGL context on the new canvas + boost::shared_ptr viewport; + + { + std::auto_ptr lock(it->second->Lock()); + viewport = boost::make_shared(it->first, lock->GetController().GetScene()); + } + + // Replace the old WebGL viewport by the new one + it->second = viewport; + + // Tag the fresh canvas as needing a repaint + { + std::auto_ptr lock(it->second->Lock()); + lock->Invalidate(); + } + } + } + + LaunchTimer(); + } + + + void WebGLViewportsRegistry::OnTimeoutCallback(void *userData) + { + WebGLViewportsRegistry& that = *reinterpret_cast(userData); + that.OnTimeout(); + } + + + WebGLViewportsRegistry::WebGLViewportsRegistry(double timeoutMS) : + timeoutMS_(timeoutMS) + { + if (timeoutMS <= 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + LaunchTimer(); + } + + + boost::shared_ptr WebGLViewportsRegistry::Add(const std::string& canvasId) + { + if (viewports_.find(canvasId) != viewports_.end()) + { + LOG(ERROR) << "Canvas was already registered: " << canvasId; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + boost::shared_ptr viewport(new WebGLViewport(canvasId)); + viewports_[canvasId] = viewport; + return viewport; + } + } + + + void WebGLViewportsRegistry::Remove(const std::string& canvasId) + { + Viewports::iterator found = viewports_.find(canvasId); + + if (found == viewports_.end()) + { + LOG(ERROR) << "Cannot remove unregistered canvas: " << canvasId; + } + else + { + viewports_.erase(found); + } + } + + + void WebGLViewportsRegistry::Clear() + { + viewports_.clear(); + } + + + WebGLViewportsRegistry::Accessor::Accessor(WebGLViewportsRegistry& that, + const std::string& canvasId) : + that_(that) + { + Viewports::iterator viewport = that.viewports_.find(canvasId); + if (viewport != that.viewports_.end() && + viewport->second != NULL) + { + lock_.reset(viewport->second->Lock()); + } + } + + + IViewport::ILock& WebGLViewportsRegistry::Accessor::GetViewport() const + { + if (IsValid()) + { + return *lock_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Viewport/WebGLViewportsRegistry.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Viewport/WebGLViewportsRegistry.h Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,82 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "WebGLViewport.h" + +namespace OrthancStone +{ + /** + * This singleton class must be used if many WebGL viewports are + * created by the higher-level application, implying possible loss + * of WebGL contexts. The object will run an infinite update loop + * that checks whether all the WebGL context are still valid (not + * lost). If some WebGL context is lost, it is automatically + * reinitialized by created a fresh HTML5 canvas. + **/ + class WebGLViewportsRegistry : public boost::noncopyable + { + private: + typedef std::map > Viewports; + + double timeoutMS_; + Viewports viewports_; + + void LaunchTimer(); + + void OnTimeout(); + + static void OnTimeoutCallback(void *userData); + + public: + WebGLViewportsRegistry(double timeoutMS /* in milliseconds */); + + ~WebGLViewportsRegistry() + { + Clear(); + } + + boost::shared_ptr Add(const std::string& canvasId); + + void Remove(const std::string& canvasId); + + void Clear(); + + class Accessor : public boost::noncopyable + { + private: + WebGLViewportsRegistry& that_; + std::auto_ptr lock_; + + public: + Accessor(WebGLViewportsRegistry& that, + const std::string& canvasId); + + bool IsValid() const + { + return lock_.get() != NULL; + } + + IViewport::ILock& GetViewport() const; + }; + }; +} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Volumes/DicomVolumeImage.h --- a/Framework/Volumes/DicomVolumeImage.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Volumes/DicomVolumeImage.h Tue Feb 04 15:24:03 2020 +0100 @@ -28,14 +28,6 @@ namespace OrthancStone { - class IGeometryProvider - { - public: - virtual ~IGeometryProvider() {} - virtual bool HasGeometry() const = 0; - virtual const VolumeImageGeometry& GetImageGeometry() const = 0; - }; - /** This class combines a 3D image buffer, a 3D volume geometry and information about the DICOM parameters of the series. @@ -44,6 +36,7 @@ class DicomVolumeImage : public boost::noncopyable { public: + // TODO - Are these messages still useful? ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, GeometryReadyMessage, DicomVolumeImage); ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, ContentUpdatedMessage, DicomVolumeImage); @@ -67,8 +60,10 @@ } void Initialize(const VolumeImageGeometry& geometry, - Orthanc::PixelFormat format, bool computeRange = false); + Orthanc::PixelFormat format, + bool computeRange = false); + // Used by volume slicers void SetDicomParameters(const DicomInstanceParameters& parameters); uint64_t GetRevision() const diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Volumes/DicomVolumeImageMPRSlicer.cpp --- a/Framework/Volumes/DicomVolumeImageMPRSlicer.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Volumes/DicomVolumeImageMPRSlicer.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -45,8 +45,7 @@ revision_(volume_.GetRevision()) { valid_ = (volume_.HasDicomParameters() && - volume_.GetGeometry().DetectSlice(projection_, sliceIndex_, - cuttingPlane)); + volume_.GetGeometry().DetectSlice(projection_, sliceIndex_, cuttingPlane)); } @@ -82,21 +81,6 @@ ImageBuffer3D::SliceReader reader(volume_.GetPixelData(), projection_, sliceIndex_); texture.reset(dynamic_cast (configurator->CreateTextureFromDicom(reader.GetAccessor(), parameters))); - - // -#if 0 - Orthanc::JpegWriter writer; - writer.SetQuality(60); - static int index = 0; - std::string filePath = "C:\\temp\\sliceReader_P"; - filePath += boost::lexical_cast(projection_); - filePath += "_I"; - filePath += boost::lexical_cast(index); - filePath += ".jpg"; - index++; - writer.WriteToFile(filePath, reader.GetAccessor()); -#endif - // } const CoordinateSystem3D& system = volume_.GetGeometry().GetProjectionGeometry(projection_); @@ -105,53 +89,11 @@ cuttingPlane.ProjectPoint(x0, y0, system.GetOrigin()); cuttingPlane.ProjectPoint(x1, y1, system.GetOrigin() + system.GetAxisX()); - // -#if 0 { - LOG(ERROR) << "+----------------------------------------------------+"; - LOG(ERROR) << "| DicomVolumeImageMPRSlicer::Slice::CreateSceneLayer |"; - LOG(ERROR) << "+----------------------------------------------------+"; - std::string projectionString; - switch (projection_) - { - case VolumeProjection_Coronal: - projectionString = "CORONAL"; - break; - case VolumeProjection_Axial: - projectionString = "CORONAL"; - break; - case VolumeProjection_Sagittal: - projectionString = "SAGITTAL"; - break; - default: - ORTHANC_ASSERT(false); - } - if(volume_.GetGeometry().GetDepth() == 200) - LOG(ERROR) << "| CT IMAGE 512x512 with projection " << projectionString; - else - LOG(ERROR) << "| RTDOSE IMAGE NNNxNNN with projection " << projectionString; - LOG(ERROR) << "+----------------------------------------------------+"; - LOG(ERROR) << "| cuttingPlane = " << cuttingPlane; - LOG(ERROR) << "| point to project = " << system.GetOrigin(); - LOG(ERROR) << "| result = x0: " << x0 << " y0: " << y0; - LOG(ERROR) << "+----------------------- END ------------------------+"; + double xz, yz; + cuttingPlane.ProjectPoint(xz, yz, LinearAlgebra::CreateVector(0, 0, 0)); + texture->SetOrigin(x0 - xz, y0 - yz); } -#endif - // - -#if 1 // BGO 2019-08-13 - // The sagittal coordinate system has a Y vector going down. The displayed - // image (scene coords) has a Y vector pointing upwards (towards the patient - // coord Z index) - // we need to flip the Y local coordinates to get the scene-coord offset. - // TODO: this is quite ugly. Isn't there a better way? - if(projection_ == VolumeProjection_Sagittal) - texture->SetOrigin(x0, -y0); - else - texture->SetOrigin(x0, y0); -#else - texture->SetOrigin(x0, y0); -#endif double dx = x1 - x0; double dy = y1 - y0; @@ -164,19 +106,6 @@ Vector tmp = volume_.GetGeometry().GetVoxelDimensions(projection_); texture->SetPixelSpacing(tmp[0], tmp[1]); - // - { - //using std::endl; - //std::stringstream ss; - //ss << "DicomVolumeImageMPRSlicer::Slice::CreateSceneLayer | cuttingPlane = " << cuttingPlane << " | projection_ = " << projection_ << endl; - //ss << "volume_.GetGeometry().GetProjectionGeometry(projection_) = " << system << endl; - //ss << "cuttingPlane.ProjectPoint(x0, y0, system.GetOrigin()); --> | x0 = " << x0 << " | y0 = " << y0 << "| x1 = " << x1 << " | y1 = " << y1 << endl; - //ss << "volume_.GetGeometry() = " << volume_.GetGeometry() << endl; - //ss << "volume_.GetGeometry() = " << volume_.GetGeometry() << endl; - //LOG(ERROR) << ss.str(); - } - // - return texture.release(); } diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Volumes/IVolumeSlicer.cpp~ --- a/Framework/Volumes/IVolumeSlicer.cpp~ Tue Feb 04 15:23:33 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,113 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -namespace OrthancStone -{ - /** - This interface is implemented by objects representing 3D volume data and - that are able to return an object that: - - represent a slice of their data - - are able to create the corresponding slice visual representation. - */ - class IVolumeSlicer : public boost::noncopyable - { - public: - /** - This interface is implemented by objects representing a slice of - volume data and that are able to create a 2D layer to display a this - slice. - - The CreateSceneLayer factory method is called with an optional - configurator that possibly impacts the ISceneLayer subclass that is - created (for instance, if a LUT must be applied on the texture when - displaying it) - */ - class IExtractedSlice : public boost::noncopyable - { - public: - virtual ~IExtractedSlice() - { - } - - /** - Invalid slices are created when the data is not ready yet or if the - cut is outside of the available geometry. - */ - virtual bool IsValid() = 0; - - /** - This retrieves the *revision* that gets incremented every time the - underlying object undergoes a mutable operation (that it, changes its - state). - This **must** be a cheap call. - */ - virtual uint64_t GetRevision() = 0; - - /** Creates the slice visual representation */ - virtual ISceneLayer* CreateSceneLayer( - const ILayerStyleConfigurator* configurator, // possibly absent - const CoordinateSystem3D& cuttingPlane) = 0; - }; - - /** - See IExtractedSlice.IsValid() - */ - class InvalidSlice : public IExtractedSlice - { - public: - virtual bool IsValid() - { - return false; - } - - virtual uint64_t GetRevision() - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - - virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator, - const CoordinateSystem3D& cuttingPlane) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - }; - - - virtual ~IVolumeSlicer() - { - } - - /** - This method is implemented by the objects representing volumetric data - and must returns an IExtractedSlice subclass that contains all the data - needed to, later on, create its visual representation through - CreateSceneLayer. - Subclasses a.o.: - - InvalidSlice, - - DicomVolumeImageMPRSlicer::Slice, - - DicomVolumeImageReslicer::Slice - - DicomStructureSetLoader::Slice - */ - virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) = 0; - }; -} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Volumes/IVolumeSlicer.h~ --- a/Framework/Volumes/IVolumeSlicer.h~ Tue Feb 04 15:23:33 2020 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,113 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -namespace OrthancStone -{ - /** - This interface is implemented by objects representing 3D volume data and - that are able to return an object that: - - represent a slice of their data - - are able to create the corresponding slice visual representation. - */ - class IVolumeSlicer : public boost::noncopyable - { - public: - /** - This interface is implemented by objects representing a slice of - volume data and that are able to create a 2D layer to display a this - slice. - - The CreateSceneLayer factory method is called with an optional - configurator that possibly impacts the ISceneLayer subclass that is - created (for instance, if a LUT must be applied on the texture when - displaying it) - */ - class IExtractedSlice : public boost::noncopyable - { - public: - virtual ~IExtractedSlice() - { - } - - /** - Invalid slices are created when the data is not ready yet or if the - cut is outside of the available geometry. - */ - virtual bool IsValid() = 0; - - /** - This retrieves the *revision* that gets incremented every time the - underlying object undergoes a mutable operation (that it, changes its - state). - This **must** be a cheap call. - */ - virtual uint64_t GetRevision() = 0; - - /** Creates the slice visual representation */ - virtual ISceneLayer* CreateSceneLayer( - const ILayerStyleConfigurator* configurator, // possibly absent - const CoordinateSystem3D& cuttingPlane) = 0; - }; - - /** - See IExtractedSlice.IsValid() - */ - class InvalidSlice : public IExtractedSlice - { - public: - virtual bool IsValid() - { - return false; - } - - virtual uint64_t GetRevision() - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - - virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator, - const CoordinateSystem3D& cuttingPlane) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - }; - - - virtual ~IVolumeSlicer() - { - } - - /** - This method is implemented by the objects representing volumetric data - and must returns an IExtractedSlice subclass that contains all the data - needed to, later on, create its visual representation through - CreateSceneLayer. - Subclasses a.o.: - - InvalidSlice, - - DicomVolumeImageMPRSlicer::Slice, - - DicomVolumeImageReslicer::Slice - - DicomStructureSetLoader::Slice - */ - virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) = 0; - }; -} diff -r 1c7ae79c426d -r 68579a31eeb4 Framework/Volumes/VolumeImageGeometry.cpp --- a/Framework/Volumes/VolumeImageGeometry.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Framework/Volumes/VolumeImageGeometry.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -39,7 +39,7 @@ sagittalGeometry_ = CoordinateSystem3D(p, axialGeometry_.GetAxisY(), - axialGeometry_.GetNormal()); + -axialGeometry_.GetNormal()); Vector origin = ( axialGeometry_.MapSliceToWorldCoordinates(-0.5 * voxelDimensions_[0], @@ -296,16 +296,18 @@ { return false; } - - unsigned int d = static_cast(std::floor(z)); - if (d >= projectionDepth) - { - return false; - } else { - slice = d; - return true; + unsigned int d = static_cast(std::floor(z)); + if (d >= projectionDepth) + { + return false; + } + else + { + slice = d; + return true; + } } } @@ -321,7 +323,18 @@ Vector dim = GetVoxelDimensions(projection); CoordinateSystem3D plane = GetProjectionGeometry(projection); - plane.SetOrigin(plane.GetOrigin() + static_cast(z) * plane.GetNormal() * dim[2]); + Vector normal = plane.GetNormal(); + if (projection == VolumeProjection_Sagittal) + { + /** + * WARNING: In sagittal geometry, the normal points to REDUCING + * X-axis in the 3D world. This is necessary to keep the + * right-hand coordinate system. Hence the negation. + **/ + normal = -normal; + } + + plane.SetOrigin(plane.GetOrigin() + static_cast(z) * dim[2] * normal); return plane; } diff -r 1c7ae79c426d -r 68579a31eeb4 Platforms/Generic/DelayedCallCommand.cpp --- a/Platforms/Generic/DelayedCallCommand.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Platforms/Generic/DelayedCallCommand.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -26,13 +26,11 @@ namespace Deprecated { - DelayedCallCommand::DelayedCallCommand(OrthancStone::MessageBroker& broker, - OrthancStone::MessageHandler* callback, // takes ownership + DelayedCallCommand::DelayedCallCommand(MessageHandler* callback, // takes ownership unsigned int timeoutInMs, Orthanc::IDynamicObject* payload /* takes ownership */, OrthancStone::NativeStoneApplicationContext& context ) : - IObservable(broker), callback_(callback), payload_(payload), context_(context), diff -r 1c7ae79c426d -r 68579a31eeb4 Platforms/Generic/DelayedCallCommand.h --- a/Platforms/Generic/DelayedCallCommand.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Platforms/Generic/DelayedCallCommand.h Tue Feb 04 15:24:03 2020 +0100 @@ -35,15 +35,14 @@ class DelayedCallCommand : public IOracleCommand, OrthancStone::IObservable { protected: - std::auto_ptr > callback_; + std::auto_ptr > callback_; std::auto_ptr payload_; OrthancStone::NativeStoneApplicationContext& context_; boost::posix_time::ptime expirationTimePoint_; unsigned int timeoutInMs_; public: - DelayedCallCommand(OrthancStone::MessageBroker& broker, - OrthancStone::MessageHandler* callback, // takes ownership + DelayedCallCommand(MessageHandler* callback, // takes ownership unsigned int timeoutInMs, Orthanc::IDynamicObject* payload /* takes ownership */, OrthancStone::NativeStoneApplicationContext& context diff -r 1c7ae79c426d -r 68579a31eeb4 Platforms/Generic/OracleDelayedCallExecutor.h --- a/Platforms/Generic/OracleDelayedCallExecutor.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Platforms/Generic/OracleDelayedCallExecutor.h Tue Feb 04 15:24:03 2020 +0100 @@ -36,19 +36,17 @@ OrthancStone::NativeStoneApplicationContext& context_; public: - OracleDelayedCallExecutor(OrthancStone::MessageBroker& broker, - Oracle& oracle, + OracleDelayedCallExecutor(Oracle& oracle, OrthancStone::NativeStoneApplicationContext& context) : - IDelayedCallExecutor(broker), oracle_(oracle), context_(context) { } - virtual void Schedule(OrthancStone::MessageHandler* callback, + virtual void Schedule(MessageHandler* callback, unsigned int timeoutInMs = 1000) { - oracle_.Submit(new DelayedCallCommand(broker_, callback, timeoutInMs, NULL, context_)); + oracle_.Submit(new DelayedCallCommand(callback, timeoutInMs, NULL, context_)); } }; } diff -r 1c7ae79c426d -r 68579a31eeb4 Platforms/Generic/OracleWebService.cpp --- a/Platforms/Generic/OracleWebService.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Platforms/Generic/OracleWebService.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -29,19 +29,17 @@ class OracleWebService::WebServiceCachedGetCommand : public IOracleCommand, OrthancStone::IObservable { protected: - std::auto_ptr > successCallback_; + std::auto_ptr > successCallback_; std::auto_ptr payload_; boost::shared_ptr cachedMessage_; OrthancStone::NativeStoneApplicationContext& context_; public: - WebServiceCachedGetCommand(OrthancStone::MessageBroker& broker, - OrthancStone::MessageHandler* successCallback, // takes ownership + WebServiceCachedGetCommand(MessageHandler* successCallback, // takes ownership boost::shared_ptr cachedMessage, Orthanc::IDynamicObject* payload /* takes ownership */, OrthancStone::NativeStoneApplicationContext& context ) : - IObservable(broker), successCallback_(successCallback), payload_(payload), cachedMessage_(cachedMessage), @@ -73,9 +71,9 @@ void OracleWebService::NotifyHttpSuccessLater(boost::shared_ptr cachedMessage, Orthanc::IDynamicObject* payload, // takes ownership - OrthancStone::MessageHandler* successCallback) + MessageHandler* successCallback) { - oracle_.Submit(new WebServiceCachedGetCommand(GetBroker(), successCallback, cachedMessage, payload, context_)); + oracle_.Submit(new WebServiceCachedGetCommand(successCallback, cachedMessage, payload, context_)); } diff -r 1c7ae79c426d -r 68579a31eeb4 Platforms/Generic/OracleWebService.h --- a/Platforms/Generic/OracleWebService.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Platforms/Generic/OracleWebService.h Tue Feb 04 15:24:03 2020 +0100 @@ -43,11 +43,9 @@ class WebServiceCachedGetCommand; public: - OracleWebService(OrthancStone::MessageBroker& broker, - Oracle& oracle, + OracleWebService(Oracle& oracle, const Orthanc::WebServiceParameters& parameters, OrthancStone::NativeStoneApplicationContext& context) : - BaseWebService(broker), oracle_(oracle), context_(context), parameters_(parameters) @@ -58,37 +56,37 @@ const HttpHeaders& headers, const std::string& body, Orthanc::IDynamicObject* payload, // takes ownership - OrthancStone::MessageHandler* successCallback, // takes ownership - OrthancStone::MessageHandler* failureCallback = NULL, // takes ownership + MessageHandler* successCallback, // takes ownership + MessageHandler* failureCallback = NULL, // takes ownership unsigned int timeoutInSeconds = 60) { - oracle_.Submit(new WebServicePostCommand(GetBroker(), successCallback, failureCallback, parameters_, uri, headers, timeoutInSeconds, body, payload, context_)); + oracle_.Submit(new WebServicePostCommand(successCallback, failureCallback, parameters_, uri, headers, timeoutInSeconds, body, payload, context_)); } virtual void DeleteAsync(const std::string& uri, const HttpHeaders& headers, Orthanc::IDynamicObject* payload, - OrthancStone::MessageHandler* successCallback, - OrthancStone::MessageHandler* failureCallback = NULL, + MessageHandler* successCallback, + MessageHandler* failureCallback = NULL, unsigned int timeoutInSeconds = 60) { - oracle_.Submit(new WebServiceDeleteCommand(GetBroker(), successCallback, failureCallback, parameters_, uri, headers, timeoutInSeconds, payload, context_)); + oracle_.Submit(new WebServiceDeleteCommand(successCallback, failureCallback, parameters_, uri, headers, timeoutInSeconds, payload, context_)); } protected: virtual void GetAsyncInternal(const std::string& uri, const HttpHeaders& headers, Orthanc::IDynamicObject* payload, // takes ownership - OrthancStone::MessageHandler* successCallback, // takes ownership - OrthancStone::MessageHandler* failureCallback = NULL,// takes ownership + MessageHandler* successCallback, // takes ownership + MessageHandler* failureCallback = NULL,// takes ownership unsigned int timeoutInSeconds = 60) { - oracle_.Submit(new WebServiceGetCommand(GetBroker(), successCallback, failureCallback, parameters_, uri, headers, timeoutInSeconds, payload, context_)); + oracle_.Submit(new WebServiceGetCommand(successCallback, failureCallback, parameters_, uri, headers, timeoutInSeconds, payload, context_)); } virtual void NotifyHttpSuccessLater(boost::shared_ptr cachedHttpMessage, Orthanc::IDynamicObject* payload, // takes ownership - OrthancStone::MessageHandler* successCallback); + MessageHandler* successCallback); }; } diff -r 1c7ae79c426d -r 68579a31eeb4 Platforms/Generic/WebServiceCommandBase.cpp --- a/Platforms/Generic/WebServiceCommandBase.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Platforms/Generic/WebServiceCommandBase.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -25,16 +25,14 @@ namespace Deprecated { - WebServiceCommandBase::WebServiceCommandBase(OrthancStone::MessageBroker& broker, - OrthancStone::MessageHandler* successCallback, - OrthancStone::MessageHandler* failureCallback, + WebServiceCommandBase::WebServiceCommandBase(MessageHandler* successCallback, + MessageHandler* failureCallback, const Orthanc::WebServiceParameters& parameters, const std::string& url, const IWebService::HttpHeaders& headers, unsigned int timeoutInSeconds, Orthanc::IDynamicObject* payload /* takes ownership */, OrthancStone::NativeStoneApplicationContext& context) : - IObservable(broker), successCallback_(successCallback), failureCallback_(failureCallback), parameters_(parameters), diff -r 1c7ae79c426d -r 68579a31eeb4 Platforms/Generic/WebServiceCommandBase.h --- a/Platforms/Generic/WebServiceCommandBase.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Platforms/Generic/WebServiceCommandBase.h Tue Feb 04 15:24:03 2020 +0100 @@ -37,8 +37,8 @@ class WebServiceCommandBase : public IOracleCommand, OrthancStone::IObservable { protected: - std::auto_ptr > successCallback_; - std::auto_ptr > failureCallback_; + std::auto_ptr > successCallback_; + std::auto_ptr > failureCallback_; Orthanc::WebServiceParameters parameters_; std::string url_; IWebService::HttpHeaders headers_; @@ -51,9 +51,8 @@ unsigned int timeoutInSeconds_; public: - WebServiceCommandBase(OrthancStone::MessageBroker& broker, - OrthancStone::MessageHandler* successCallback, // takes ownership - OrthancStone::MessageHandler* failureCallback, // takes ownership + WebServiceCommandBase(MessageHandler* successCallback, // takes ownership + MessageHandler* failureCallback, // takes ownership const Orthanc::WebServiceParameters& parameters, const std::string& url, const IWebService::HttpHeaders& headers, diff -r 1c7ae79c426d -r 68579a31eeb4 Platforms/Generic/WebServiceDeleteCommand.cpp --- a/Platforms/Generic/WebServiceDeleteCommand.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Platforms/Generic/WebServiceDeleteCommand.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -25,16 +25,15 @@ namespace Deprecated { - WebServiceDeleteCommand::WebServiceDeleteCommand(OrthancStone::MessageBroker& broker, - OrthancStone::MessageHandler* successCallback, // takes ownership - OrthancStone::MessageHandler* failureCallback, // takes ownership + WebServiceDeleteCommand::WebServiceDeleteCommand(MessageHandler* successCallback, // takes ownership + MessageHandler* failureCallback, // takes ownership const Orthanc::WebServiceParameters& parameters, const std::string& url, const Deprecated::IWebService::HttpHeaders& headers, unsigned int timeoutInSeconds, Orthanc::IDynamicObject* payload /* takes ownership */, OrthancStone::NativeStoneApplicationContext& context) : - WebServiceCommandBase(broker, successCallback, failureCallback, parameters, url, headers, timeoutInSeconds, payload, context) + WebServiceCommandBase(successCallback, failureCallback, parameters, url, headers, timeoutInSeconds, payload, context) { } diff -r 1c7ae79c426d -r 68579a31eeb4 Platforms/Generic/WebServiceDeleteCommand.h --- a/Platforms/Generic/WebServiceDeleteCommand.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Platforms/Generic/WebServiceDeleteCommand.h Tue Feb 04 15:24:03 2020 +0100 @@ -28,9 +28,8 @@ class WebServiceDeleteCommand : public WebServiceCommandBase { public: - WebServiceDeleteCommand(OrthancStone::MessageBroker& broker, - OrthancStone::MessageHandler* successCallback, // takes ownership - OrthancStone::MessageHandler* failureCallback, // takes ownership + WebServiceDeleteCommand(MessageHandler* successCallback, // takes ownership + MessageHandler* failureCallback, // takes ownership const Orthanc::WebServiceParameters& parameters, const std::string& url, const IWebService::HttpHeaders& headers, diff -r 1c7ae79c426d -r 68579a31eeb4 Platforms/Generic/WebServiceGetCommand.cpp --- a/Platforms/Generic/WebServiceGetCommand.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Platforms/Generic/WebServiceGetCommand.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -25,17 +25,15 @@ namespace Deprecated { - - WebServiceGetCommand::WebServiceGetCommand(OrthancStone::MessageBroker& broker, - OrthancStone::MessageHandler* successCallback, // takes ownership - OrthancStone::MessageHandler* failureCallback, // takes ownership + WebServiceGetCommand::WebServiceGetCommand(MessageHandler* successCallback, // takes ownership + MessageHandler* failureCallback, // takes ownership const Orthanc::WebServiceParameters& parameters, const std::string& url, const IWebService::HttpHeaders& headers, unsigned int timeoutInSeconds, Orthanc::IDynamicObject* payload /* takes ownership */, OrthancStone::NativeStoneApplicationContext& context) : - WebServiceCommandBase(broker, successCallback, failureCallback, parameters, url, headers, timeoutInSeconds, payload, context) + WebServiceCommandBase(successCallback, failureCallback, parameters, url, headers, timeoutInSeconds, payload, context) { } diff -r 1c7ae79c426d -r 68579a31eeb4 Platforms/Generic/WebServiceGetCommand.h --- a/Platforms/Generic/WebServiceGetCommand.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Platforms/Generic/WebServiceGetCommand.h Tue Feb 04 15:24:03 2020 +0100 @@ -28,9 +28,8 @@ class WebServiceGetCommand : public WebServiceCommandBase { public: - WebServiceGetCommand(OrthancStone::MessageBroker& broker, - OrthancStone::MessageHandler* successCallback, // takes ownership - OrthancStone::MessageHandler* failureCallback, // takes ownership + WebServiceGetCommand(MessageHandler* successCallback, // takes ownership + MessageHandler* failureCallback, // takes ownership const Orthanc::WebServiceParameters& parameters, const std::string& url, const IWebService::HttpHeaders& headers, diff -r 1c7ae79c426d -r 68579a31eeb4 Platforms/Generic/WebServicePostCommand.cpp --- a/Platforms/Generic/WebServicePostCommand.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/Platforms/Generic/WebServicePostCommand.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -25,9 +25,8 @@ namespace Deprecated { - WebServicePostCommand::WebServicePostCommand(OrthancStone::MessageBroker& broker, - OrthancStone::MessageHandler* successCallback, // takes ownership - OrthancStone::MessageHandler* failureCallback, // takes ownership + WebServicePostCommand::WebServicePostCommand(MessageHandler* successCallback, // takes ownership + MessageHandler* failureCallback, // takes ownership const Orthanc::WebServiceParameters& parameters, const std::string& url, const Deprecated::IWebService::HttpHeaders& headers, @@ -35,7 +34,7 @@ const std::string& body, Orthanc::IDynamicObject* payload /* takes ownership */, OrthancStone::NativeStoneApplicationContext& context) : - WebServiceCommandBase(broker, successCallback, failureCallback, parameters, url, headers, timeoutInSeconds, payload, context), + WebServiceCommandBase(successCallback, failureCallback, parameters, url, headers, timeoutInSeconds, payload, context), body_(body) { } diff -r 1c7ae79c426d -r 68579a31eeb4 Platforms/Generic/WebServicePostCommand.h --- a/Platforms/Generic/WebServicePostCommand.h Tue Feb 04 15:23:33 2020 +0100 +++ b/Platforms/Generic/WebServicePostCommand.h Tue Feb 04 15:24:03 2020 +0100 @@ -31,9 +31,8 @@ std::string body_; public: - WebServicePostCommand(OrthancStone::MessageBroker& broker, - OrthancStone::MessageHandler* successCallback, // takes ownership - OrthancStone::MessageHandler* failureCallback, // takes ownership + WebServicePostCommand(MessageHandler* successCallback, // takes ownership + MessageHandler* failureCallback, // takes ownership const Orthanc::WebServiceParameters& parameters, const std::string& url, const IWebService::HttpHeaders& headers, diff -r 1c7ae79c426d -r 68579a31eeb4 Resources/CMake/OrthancStoneConfiguration.cmake --- a/Resources/CMake/OrthancStoneConfiguration.cmake Tue Feb 04 15:23:33 2020 +0100 +++ b/Resources/CMake/OrthancStoneConfiguration.cmake Tue Feb 04 15:24:03 2020 +0100 @@ -250,61 +250,33 @@ ## All the source files required to build Stone of Orthanc ##################################################################### -set(APPLICATIONS_SOURCES - ${ORTHANC_STONE_ROOT}/Applications/IStoneApplication.h - ) - if (NOT ORTHANC_SANDBOXED) set(PLATFORM_SOURCES - ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceCommandBase.cpp - ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceGetCommand.cpp - ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServicePostCommand.cpp - ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceDeleteCommand.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/GenericLoadersContext.cpp ${ORTHANC_STONE_ROOT}/Platforms/Generic/DelayedCallCommand.cpp ${ORTHANC_STONE_ROOT}/Platforms/Generic/Oracle.cpp ${ORTHANC_STONE_ROOT}/Platforms/Generic/OracleDelayedCallExecutor.h + ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceCommandBase.cpp + ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceDeleteCommand.cpp + ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceGetCommand.cpp + ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServicePostCommand.cpp ) - if (ENABLE_STONE_DEPRECATED) - list(APPEND PLATFORM_SOURCES - ${ORTHANC_STONE_ROOT}/Platforms/Generic/OracleWebService.cpp - ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Viewport/CairoFont.cpp + if (ENABLE_SDL) + list(APPEND ORTHANC_STONE_SOURCES + ${ORTHANC_STONE_ROOT}/Framework/Viewport/SdlWindow.cpp ) endif() if (ENABLE_SDL OR ENABLE_QT) - if (ENABLE_STONE_DEPRECATED) - list(APPEND APPLICATIONS_SOURCES - ${ORTHANC_STONE_ROOT}/Applications/Generic/NativeStoneApplicationRunner.cpp - ${ORTHANC_STONE_ROOT}/Applications/Generic/NativeStoneApplicationContext.cpp - ) - endif() - - if (ENABLE_SDL) + if (ENABLE_OPENGL) list(APPEND ORTHANC_STONE_SOURCES - ${ORTHANC_STONE_ROOT}/Framework/Viewport/SdlWindow.cpp + ${ORTHANC_STONE_ROOT}/Framework/OpenGL/SdlOpenGLContext.cpp + ${ORTHANC_STONE_ROOT}/Framework/Viewport/SdlViewport.cpp ) - - list(APPEND APPLICATIONS_SOURCES - ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlCairoSurface.cpp - ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlEngine.cpp - ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlOrthancSurface.cpp - ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlStoneApplicationRunner.cpp - ) - - if (ENABLE_OPENGL) - list(APPEND ORTHANC_STONE_SOURCES - ${ORTHANC_STONE_ROOT}/Framework/OpenGL/SdlOpenGLContext.cpp - ${ORTHANC_STONE_ROOT}/Framework/Viewport/SdlViewport.cpp - ) - endif() endif() endif() elseif (ENABLE_WASM) - list(APPEND APPLICATIONS_SOURCES - ${ORTHANC_STONE_ROOT}/Applications/Wasm/StartupParametersBuilder.cpp - ) - set(STONE_WASM_SOURCES ${ORTHANC_STONE_ROOT}/Platforms/Wasm/Defaults.cpp ${ORTHANC_STONE_ROOT}/Platforms/Wasm/WasmDelayedCallExecutor.cpp @@ -331,15 +303,52 @@ DEPENDS "${ORTHANC_STONE_ROOT}/Platforms/Wasm/default-library.js") endif() -if (ENABLE_SDL OR ENABLE_WASM) - list(APPEND APPLICATIONS_SOURCES - ${ORTHANC_STONE_ROOT}/Applications/Generic/GuiAdapter.cpp - ${ORTHANC_STONE_ROOT}/Applications/Generic/GuiAdapter.h - ) -endif() +if (ENABLE_STONE_DEPRECATED) + if (NOT ORTHANC_SANDBOXED) + list(APPEND PLATFORM_SOURCES + ${ORTHANC_STONE_ROOT}/Platforms/Generic/OracleWebService.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Viewport/CairoFont.cpp + ) + endif() + + if (ENABLE_SDL OR ENABLE_WASM) + list(APPEND APPLICATIONS_SOURCES + ${ORTHANC_STONE_ROOT}/Applications/Generic/GuiAdapter.cpp + ${ORTHANC_STONE_ROOT}/Applications/Generic/GuiAdapter.h + ) + endif() + + if (ENABLE_SDL OR ENABLE_QT) + list(APPEND APPLICATIONS_SOURCES + ${ORTHANC_STONE_ROOT}/Applications/Generic/NativeStoneApplicationRunner.cpp + ${ORTHANC_STONE_ROOT}/Applications/Generic/NativeStoneApplicationContext.cpp + ) + endif() -if (ENABLE_STONE_DEPRECATED) + if (ENABLE_SDL) + list(APPEND APPLICATIONS_SOURCES + ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlCairoSurface.cpp + ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlEngine.cpp + ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlOrthancSurface.cpp + ${ORTHANC_STONE_ROOT}/Applications/Sdl/SdlStoneApplicationRunner.cpp + ) + endif() + + if (ENABLE_WASM) + list(APPEND APPLICATIONS_SOURCES + ${ORTHANC_STONE_ROOT}/Applications/Wasm/StartupParametersBuilder.cpp + ) + endif() + + if (ENABLE_THREADS) + list(APPEND ORTHANC_STONE_SOURCES + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Messages/LockingEmitter.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Messages/LockingEmitter.h + ) + endif() + list(APPEND ORTHANC_STONE_SOURCES + ${ORTHANC_STONE_ROOT}/Applications/IStoneApplication.h ${ORTHANC_STONE_ROOT}/Applications/StoneApplicationContext.cpp ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/CircleMeasureTracker.cpp ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/ColorFrameRenderer.cpp @@ -352,6 +361,18 @@ ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/LineMeasureTracker.cpp ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/RenderStyle.cpp ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/SliceOutlineRenderer.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/DicomStructureSetLoader.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/DicomStructureSetLoader.h + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/DicomStructureSetLoader2.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/DicomStructureSetLoader2.h + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/LoaderCache.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/LoaderCache.h + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/LoaderStateMachine.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/LoaderStateMachine.h + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.h + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp + ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/OrthancSeriesVolumeProgressiveLoader.h ${ORTHANC_STONE_ROOT}/Framework/Deprecated/SmartLoader.cpp ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/BaseWebService.cpp ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/DicomFrameConverter.cpp @@ -405,17 +426,29 @@ endif() +if (ENABLE_DCMTK) + list(APPEND ORTHANC_STONE_SOURCES + ${ORTHANC_STONE_ROOT}/Framework/Oracle/ParseDicomSuccessMessage.cpp + ${ORTHANC_STONE_ROOT}/Framework/Toolbox/ParsedDicomCache.cpp + ${ORTHANC_STONE_ROOT}/Framework/Toolbox/ParsedDicomDataset.cpp + ) +endif() + if (ENABLE_THREADS) list(APPEND ORTHANC_STONE_SOURCES - ${ORTHANC_STONE_ROOT}/Framework/Messages/LockingEmitter.h ${ORTHANC_STONE_ROOT}/Framework/Oracle/ThreadedOracle.cpp + ${ORTHANC_STONE_ROOT}/Framework/Oracle/GenericOracleRunner.cpp ) endif() if (ENABLE_WASM) list(APPEND ORTHANC_STONE_SOURCES + ${ORTHANC_STONE_ROOT}/Framework/Loaders/WebAssemblyLoadersContext.cpp ${ORTHANC_STONE_ROOT}/Framework/Oracle/WebAssemblyOracle.cpp + ${ORTHANC_STONE_ROOT}/Framework/Viewport/WebAssemblyCairoViewport.cpp + ${ORTHANC_STONE_ROOT}/Framework/Viewport/WebAssemblyViewport.cpp + ${ORTHANC_STONE_ROOT}/Framework/Viewport/WebAssemblyViewport.h ) endif() @@ -441,33 +474,29 @@ ${ORTHANC_STONE_ROOT}/Framework/Loaders/BasicFetchingItemsSorter.h ${ORTHANC_STONE_ROOT}/Framework/Loaders/BasicFetchingStrategy.cpp ${ORTHANC_STONE_ROOT}/Framework/Loaders/BasicFetchingStrategy.h - ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomStructureSetLoader.cpp - ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomStructureSetLoader.h - ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomStructureSetLoader2.cpp - ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomStructureSetLoader2.h + ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomResourcesLoader.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomSource.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomVolumeLoader.cpp ${ORTHANC_STONE_ROOT}/Framework/Loaders/IFetchingItemsSorter.h ${ORTHANC_STONE_ROOT}/Framework/Loaders/IFetchingStrategy.h - ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoaderCache.cpp - ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoaderCache.h - ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoaderStateMachine.cpp - ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoaderStateMachine.h - ${ORTHANC_STONE_ROOT}/Framework/Loaders/OrthancMultiframeVolumeLoader.cpp - ${ORTHANC_STONE_ROOT}/Framework/Loaders/OrthancMultiframeVolumeLoader.h - ${ORTHANC_STONE_ROOT}/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp - ${ORTHANC_STONE_ROOT}/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h + ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoadedDicomResources.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/OracleScheduler.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/SeriesFramesLoader.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/SeriesMetadataLoader.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/SeriesOrderedFrames.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/SeriesThumbnailsLoader.cpp ${ORTHANC_STONE_ROOT}/Framework/Messages/ICallable.h ${ORTHANC_STONE_ROOT}/Framework/Messages/IMessage.h ${ORTHANC_STONE_ROOT}/Framework/Messages/IObservable.cpp - ${ORTHANC_STONE_ROOT}/Framework/Messages/IObserver.cpp ${ORTHANC_STONE_ROOT}/Framework/Messages/IObserver.h - ${ORTHANC_STONE_ROOT}/Framework/Messages/MessageBroker.h - ${ORTHANC_STONE_ROOT}/Framework/Messages/MessageForwarder.cpp ${ORTHANC_STONE_ROOT}/Framework/Oracle/GetOrthancImageCommand.cpp ${ORTHANC_STONE_ROOT}/Framework/Oracle/GetOrthancWebViewerJpegCommand.cpp - ${ORTHANC_STONE_ROOT}/Framework/Oracle/OracleCommandWithPayload.cpp + ${ORTHANC_STONE_ROOT}/Framework/Oracle/HttpCommand.cpp + ${ORTHANC_STONE_ROOT}/Framework/Oracle/OracleCommandBase.cpp ${ORTHANC_STONE_ROOT}/Framework/Oracle/OrthancRestApiCommand.cpp - ${ORTHANC_STONE_ROOT}/Framework/Oracle/HttpCommand.cpp + ${ORTHANC_STONE_ROOT}/Framework/Oracle/ParseDicomFromFileCommand.cpp + ${ORTHANC_STONE_ROOT}/Framework/Oracle/ParseDicomFromWadoCommand.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/CairoCompositor.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/ColorTextureSceneLayer.cpp ${ORTHANC_STONE_ROOT}/Framework/Scene2D/FloatTextureSceneLayer.cpp @@ -564,6 +593,8 @@ ${ORTHANC_STONE_ROOT}/Framework/Toolbox/Extent2D.h ${ORTHANC_STONE_ROOT}/Framework/Toolbox/FiniteProjectiveCamera.cpp ${ORTHANC_STONE_ROOT}/Framework/Toolbox/FiniteProjectiveCamera.h + ${ORTHANC_STONE_ROOT}/Framework/Toolbox/GenericToolbox.cpp + ${ORTHANC_STONE_ROOT}/Framework/Toolbox/GenericToolbox.h ${ORTHANC_STONE_ROOT}/Framework/Toolbox/GeometryToolbox.cpp ${ORTHANC_STONE_ROOT}/Framework/Toolbox/GeometryToolbox.h ${ORTHANC_STONE_ROOT}/Framework/Toolbox/ImageGeometry.cpp @@ -580,12 +611,8 @@ ${ORTHANC_STONE_ROOT}/Framework/Toolbox/TextRenderer.h ${ORTHANC_STONE_ROOT}/Framework/Toolbox/UndoRedoStack.cpp ${ORTHANC_STONE_ROOT}/Framework/Toolbox/UndoRedoStack.h - ${ORTHANC_STONE_ROOT}/Framework/Toolbox/GenericToolbox.cpp - ${ORTHANC_STONE_ROOT}/Framework/Toolbox/GenericToolbox.h ${ORTHANC_STONE_ROOT}/Framework/Viewport/IViewport.h - ${ORTHANC_STONE_ROOT}/Framework/Viewport/ViewportBase.h - ${ORTHANC_STONE_ROOT}/Framework/Viewport/ViewportBase.cpp ${ORTHANC_STONE_ROOT}/Framework/Volumes/IVolumeSlicer.cpp ${ORTHANC_STONE_ROOT}/Framework/Volumes/IVolumeSlicer.h @@ -671,10 +698,10 @@ if (ENABLE_WASM) list(APPEND ORTHANC_STONE_SOURCES + ${ORTHANC_STONE_ROOT}/Framework/OpenGL/WebAssemblyOpenGLContext.cpp ${ORTHANC_STONE_ROOT}/Framework/OpenGL/WebAssemblyOpenGLContext.h - ${ORTHANC_STONE_ROOT}/Framework/OpenGL/WebAssemblyOpenGLContext.cpp - ${ORTHANC_STONE_ROOT}/Framework/Viewport/WebAssemblyViewport.h - ${ORTHANC_STONE_ROOT}/Framework/Viewport/WebAssemblyViewport.cpp + ${ORTHANC_STONE_ROOT}/Framework/Viewport/WebGLViewport.cpp + ${ORTHANC_STONE_ROOT}/Framework/Viewport/WebGLViewportsRegistry.cpp ) endif() endif() diff -r 1c7ae79c426d -r 68579a31eeb4 Resources/CMake/QtConfiguration.cmake --- a/Resources/CMake/QtConfiguration.cmake Tue Feb 04 15:23:33 2020 +0100 +++ b/Resources/CMake/QtConfiguration.cmake Tue Feb 04 15:24:03 2020 +0100 @@ -20,26 +20,12 @@ set(CMAKE_AUTOMOC OFF) set(CMAKE_AUTOUIC OFF) -if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") - # Linux Standard Base version 5 ships Qt 4.2.3 + +## Note that these set of macros MUST be defined as a "function()", +## otherwise it fails +function(DEFINE_QT_MACROS) include(Qt4Macros) - # The script "LinuxStandardBaseUic.py" is just a wrapper around the - # "uic" compiler from LSB that does not support the "" - # header that is automatically added by Qt Creator - set(QT_UIC_EXECUTABLE ${CMAKE_CURRENT_LIST_DIR}/LinuxStandardBaseUic.py) - - set(QT_MOC_EXECUTABLE ${LSB_PATH}/bin/moc) - - include_directories( - ${LSB_PATH}/include/QtCore - ${LSB_PATH}/include/QtGui - ${LSB_PATH}/include/QtOpenGL - ) - - link_libraries(QtCore QtGui QtOpenGL) - - ## ## This part is adapted from file "Qt4Macros.cmake" shipped with ## CMake 3.5.1, released under the following license: @@ -86,9 +72,91 @@ ## ## End of "Qt4Macros.cmake" adaptation. ## +endfunction() + +if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") + # Linux Standard Base version 5 ships Qt 4.2.3 + DEFINE_QT_MACROS() + + # The script "LinuxStandardBaseUic.py" is just a wrapper around the + # "uic" compiler from LSB that does not support the "" + # header that is automatically added by Qt Creator + set(QT_UIC_EXECUTABLE ${CMAKE_CURRENT_LIST_DIR}/LinuxStandardBaseUic.py) + + set(QT_MOC_EXECUTABLE ${LSB_PATH}/bin/moc) + + include_directories( + ${LSB_PATH}/include/QtCore + ${LSB_PATH}/include/QtGui + ${LSB_PATH}/include/QtOpenGL + ) + + link_libraries(QtCore QtGui QtOpenGL) + +elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + DEFINE_QT_MACROS() + + include_directories(${QT5_INSTALL_ROOT}/include) + link_directories(${QT5_INSTALL_ROOT}/lib) + + if (OFF) #CMAKE_CROSSCOMPILING) + set(QT_UIC_EXECUTABLE wine ${QT5_INSTALL_ROOT}/bin/uic.exe) + set(QT_MOC_EXECUTABLE wine ${QT5_INSTALL_ROOT}/bin/moc.exe) + else() + set(QT_UIC_EXECUTABLE ${QT5_INSTALL_ROOT}/bin/uic) + set(QT_MOC_EXECUTABLE ${QT5_INSTALL_ROOT}/bin/moc) + endif() + + include_directories( + ${QT5_INSTALL_ROOT}/include/QtCore + ${QT5_INSTALL_ROOT}/include/QtGui + ${QT5_INSTALL_ROOT}/include/QtOpenGL + ${QT5_INSTALL_ROOT}/include/QtWidgets + ) + + if (OFF) + # Dynamic Qt + link_libraries(Qt5Core Qt5Gui Qt5OpenGL Qt5Widgets) + + file(COPY + ${QT5_INSTALL_ROOT}/bin/Qt5Core.dll + ${QT5_INSTALL_ROOT}/bin/Qt5Gui.dll + ${QT5_INSTALL_ROOT}/bin/Qt5OpenGL.dll + ${QT5_INSTALL_ROOT}/bin/Qt5Widgets.dll + ${QT5_INSTALL_ROOT}/bin/libstdc++-6.dll + ${QT5_INSTALL_ROOT}/bin/libgcc_s_dw2-1.dll + ${QT5_INSTALL_ROOT}/bin/libwinpthread-1.dll + DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) + + file(COPY + ${QT5_INSTALL_ROOT}/plugins/platforms/qwindows.dll + DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/platforms) + + else() + # Static Qt + link_libraries( + ${QT5_INSTALL_ROOT}/lib/libQt5Widgets.a + ${QT5_INSTALL_ROOT}/lib/libQt5Gui.a + ${QT5_INSTALL_ROOT}/lib/libQt5OpenGL.a + ${QT5_INSTALL_ROOT}/lib/libQt5Core.a + ${QT5_INSTALL_ROOT}/lib/libqtharfbuzz.a + ${QT5_INSTALL_ROOT}/lib/libqtpcre2.a + ${QT5_INSTALL_ROOT}/lib/libQt5FontDatabaseSupport.a + ${QT5_INSTALL_ROOT}/lib/libQt5EventDispatcherSupport.a + ${QT5_INSTALL_ROOT}/lib/libQt5ThemeSupport.a + ${QT5_INSTALL_ROOT}/plugins/platforms/libqwindows.a + winmm + version + ws2_32 + uxtheme + imm32 + dwmapi + ) + endif() + else() - # Not using Linux Standard Base + # Not using Windows, not using Linux Standard Base, # Find the QtWidgets library find_package(Qt5Widgets QUIET) diff -r 1c7ae79c426d -r 68579a31eeb4 Resources/Conventions.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Conventions.txt Tue Feb 04 15:24:03 2020 +0100 @@ -0,0 +1,87 @@ + +Some notes about the lifetime of objects +======================================== + +Stone applications +------------------ + +A typical Stone application can be split in 3 parts: + +1- The "loaders part" and the associated "IOracle", that communicate + through "IMessage" objects. The lifetime of these objects is + governed by the "IStoneContext". + +2- The "data part" holds the data loaded by the "loaders part". The + related objects must not be aware of the oracle, neither of the + messages. It is up to the user application to store these objects. + +3- The "viewport part" is based upon the "Scene2D" class. + + +Multithreading +-------------- + +* Stone makes the hypothesis that its objects live in a single thread. + All the content of the "Framework" folder (with the exception of + the "Oracle" stuff) must not use "boost::thread". + +* The "IOracleCommand" classes represent commands that must be + executed asynchronously from the Stone thread. Their actual + execution is done by the "IOracle". + +* In WebAssembly, the "IOracle" corresponds to the "html5.h" + facilities (notably for the Fetch API). There is no mutex here, as + JavaScript is inherently single-threaded. + +* In plain C++ applications, the "IOracle" corresponds to a FIFO queue + of commands that are executed by a pool of threads. The Stone + context holds a global mutex, that must be properly locked by the + user application, and by the "IOracle" when it sends back messages + to the Stone loaders (cf. class "IMessageEmitter"). + +* Multithreading is thus achieved by defining new oracle commands by + subclassing "IOracleCommand", then by defining a way to execute them + (cf. class "GenericCommandRunner"). + + +References between objects +-------------------------- + +* An object allocated on the heap must never store a reference/pointer + to another object. + +* A class designed to be allocated only on the stack can store a + reference/pointer to another object. Here is the list of + such classes: + + - IMessage and its derived classes: All the messages are allocated + on the stack. + + +Pointers +-------- + +* As we are targeting C++03 (for VS2008 and LSB compatibility), use + "std::auto_ptr<>" and "boost::shared_ptr<>" (*not* + "std::shared_ptr<>"). + +* The fact of transfering the ownership of one object to another must + be tagged by naming the method "Acquire...()", and by providing a + raw pointer. + +* Use "std::auto_ptr<>" if the goal is to internally store a pointer + whose lifetime corresponds to the host object. + +* The use of "boost::weak_ptr<>" should be restricted to + oracle/message handling. + +* The use of "boost::shared_ptr<>" should be minimized to avoid + clutter. The "loaders" and "data parts" objects must however + be created as "boost::shared_ptr<>". + + +Global context +-------------- + +* As the global Stone context can be created/destroyed by other + languages than C++, we don't use a "boost:shared_ptr<>". diff -r 1c7ae79c426d -r 68579a31eeb4 UnitTestsSources/GenericToolboxTests.cpp --- a/UnitTestsSources/GenericToolboxTests.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/UnitTestsSources/GenericToolboxTests.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -20,7 +20,7 @@ #include -#include +#include #include #include "gtest/gtest.h" @@ -3878,19 +3878,17 @@ bool ok = true; { - boost::chrono::system_clock::time_point start = boost::chrono::system_clock::now(); + boost::posix_time::ptime start = boost::posix_time::microsec_clock::local_time(); for (size_t i = 0; i < NUM_TIMINGS_CONVS; ++i) { ok = StringToDouble(r, txt); } - boost::chrono::system_clock::time_point end = boost::chrono::system_clock::now(); - boost::chrono::microseconds elapsed = - boost::chrono::duration_cast(end - start); - total_us_StringToDouble += elapsed.count(); + boost::posix_time::ptime end = boost::posix_time::microsec_clock::local_time(); + total_us_StringToDouble += (end - start).total_microseconds(); } { - boost::chrono::system_clock::time_point start = boost::chrono::system_clock::now(); + boost::posix_time::ptime start = boost::posix_time::microsec_clock::local_time(); for (size_t i = 0; i < NUM_TIMINGS_CONVS; ++i) { try @@ -3903,10 +3901,8 @@ ok = false; } } - boost::chrono::system_clock::time_point end = boost::chrono::system_clock::now(); - boost::chrono::microseconds elapsed = - boost::chrono::duration_cast(end - start); - total_us_lexical_cast += elapsed.count(); + boost::posix_time::ptime end = boost::posix_time::microsec_clock::local_time(); + total_us_lexical_cast += (end - start).total_microseconds(); } numConversions += NUM_TIMINGS_CONVS; @@ -4095,19 +4091,17 @@ bool ok = true; { - boost::chrono::system_clock::time_point start = boost::chrono::system_clock::now(); + boost::posix_time::ptime start = boost::posix_time::microsec_clock::local_time(); for (size_t i = 0; i < NUM_TIMINGS_CONVS; ++i) { ok = StringToDouble(r, txt); } - boost::chrono::system_clock::time_point end = boost::chrono::system_clock::now(); - boost::chrono::microseconds elapsed = - boost::chrono::duration_cast(end - start); - total_us_StringToDouble += elapsed.count(); + boost::posix_time::ptime end = boost::posix_time::microsec_clock::local_time(); + total_us_StringToDouble += (end - start).total_microseconds(); } { - boost::chrono::system_clock::time_point start = boost::chrono::system_clock::now(); + boost::posix_time::ptime start = boost::posix_time::microsec_clock::local_time(); for (size_t i = 0; i < NUM_TIMINGS_CONVS; ++i) { try @@ -4120,10 +4114,8 @@ ok = false; } } - boost::chrono::system_clock::time_point end = boost::chrono::system_clock::now(); - boost::chrono::microseconds elapsed = - boost::chrono::duration_cast(end - start); - total_us_lexical_cast += elapsed.count(); + boost::posix_time::ptime end = boost::posix_time::microsec_clock::local_time(); + total_us_lexical_cast += (end - start).total_microseconds(); } numConversions += NUM_TIMINGS_CONVS; diff -r 1c7ae79c426d -r 68579a31eeb4 UnitTestsSources/TestMessageBroker.cpp --- a/UnitTestsSources/TestMessageBroker.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/UnitTestsSources/TestMessageBroker.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -21,10 +21,8 @@ #include "gtest/gtest.h" -#include "../Framework/Messages/MessageBroker.h" -#include "../Framework/Messages/IObservable.h" -#include "../Framework/Messages/IObserver.h" -#include "../Framework/Messages/MessageForwarder.h" +#include "Framework/Messages/IObservable.h" +#include "Framework/Messages/ObserverBase.h" int testCounter = 0; @@ -47,51 +45,26 @@ { } }; - - MyObservable(MessageBroker& broker) : - IObservable(broker) - { - } }; - class MyObserver : public IObserver + class MyObserver : public ObserverBase { public: - MyObserver(MessageBroker& broker) - : IObserver(broker) - {} - void HandleCompletedMessage(const MyObservable::MyCustomMessage& message) { testCounter += message.payload_; } - - }; - - - class MyIntermediate : public IObserver, public IObservable - { - IObservable& observedObject_; - public: - MyIntermediate(MessageBroker& broker, IObservable& observedObject) - : IObserver(broker), - IObservable(broker), - observedObject_(observedObject) - { - observedObject_.RegisterObserverCallback(new MessageForwarder(broker, *this)); - } }; } TEST(MessageBroker, TestPermanentConnectionSimpleUseCase) { - MessageBroker broker; - MyObservable observable(broker); - MyObserver observer(broker); + MyObservable observable; + boost::shared_ptr observer(new MyObserver); // create a permanent connection between an observable and an observer - observable.RegisterObserverCallback(new Callable(observer, &MyObserver::HandleCompletedMessage)); + observer->Register(observable, &MyObserver::HandleCompletedMessage); testCounter = 0; observable.BroadcastMessage(MyObservable::MyCustomMessage(12)); @@ -103,155 +76,29 @@ ASSERT_EQ(20, testCounter); // Unregister the observer; make sure it's not called anymore - observable.Unregister(&observer); + observer.reset(); testCounter = 0; observable.BroadcastMessage(MyObservable::MyCustomMessage(20)); ASSERT_EQ(0, testCounter); } -TEST(MessageBroker, TestMessageForwarderSimpleUseCase) -{ - MessageBroker broker; - MyObservable observable(broker); - MyIntermediate intermediate(broker, observable); - MyObserver observer(broker); - - // let the observer observers the intermediate that is actually forwarding the messages from the observable - intermediate.RegisterObserverCallback(new Callable(observer, &MyObserver::HandleCompletedMessage)); - - testCounter = 0; - observable.BroadcastMessage(MyObservable::MyCustomMessage(12)); - ASSERT_EQ(12, testCounter); - - // the connection is permanent; if we emit the same message again, the observer will be notified again - testCounter = 0; - observable.BroadcastMessage(MyObservable::MyCustomMessage(20)); - ASSERT_EQ(20, testCounter); -} - TEST(MessageBroker, TestPermanentConnectionDeleteObserver) { - MessageBroker broker; - MyObservable observable(broker); - MyObserver* observer = new MyObserver(broker); + MyObservable observable; + boost::shared_ptr observer(new MyObserver); // create a permanent connection between an observable and an observer - observable.RegisterObserverCallback(new Callable(*observer, &MyObserver::HandleCompletedMessage)); + observer->Register(observable, &MyObserver::HandleCompletedMessage); testCounter = 0; observable.BroadcastMessage(MyObservable::MyCustomMessage(12)); ASSERT_EQ(12, testCounter); // delete the observer and check that the callback is not called anymore - delete observer; + observer.reset(); // the connection is permanent; if we emit the same message again, the observer will be notified again testCounter = 0; observable.BroadcastMessage(MyObservable::MyCustomMessage(20)); ASSERT_EQ(0, testCounter); } - -TEST(MessageBroker, TestMessageForwarderDeleteIntermediate) -{ - MessageBroker broker; - MyObservable observable(broker); - MyIntermediate* intermediate = new MyIntermediate(broker, observable); - MyObserver observer(broker); - - // let the observer observers the intermediate that is actually forwarding the messages from the observable - intermediate->RegisterObserverCallback(new Callable(observer, &MyObserver::HandleCompletedMessage)); - - testCounter = 0; - observable.BroadcastMessage(MyObservable::MyCustomMessage(12)); - ASSERT_EQ(12, testCounter); - - delete intermediate; - - observable.BroadcastMessage(MyObservable::MyCustomMessage(20)); - ASSERT_EQ(12, testCounter); -} - -TEST(MessageBroker, TestCustomMessage) -{ - MessageBroker broker; - MyObservable observable(broker); - MyIntermediate intermediate(broker, observable); - MyObserver observer(broker); - - // let the observer observers the intermediate that is actually forwarding the messages from the observable - intermediate.RegisterObserverCallback(new Callable(observer, &MyObserver::HandleCompletedMessage)); - - testCounter = 0; - observable.BroadcastMessage(MyObservable::MyCustomMessage(12)); - ASSERT_EQ(12, testCounter); - - // the connection is permanent; if we emit the same message again, the observer will be notified again - testCounter = 0; - observable.BroadcastMessage(MyObservable::MyCustomMessage(20)); - ASSERT_EQ(20, testCounter); -} - - -#if 0 /* __cplusplus >= 201103L*/ - -TEST(MessageBroker, TestLambdaSimpleUseCase) -{ - MessageBroker broker; - MyObservable observable(broker); - MyObserver* observer = new MyObserver(broker); - - // create a permanent connection between an observable and an observer - observable.RegisterObserverCallback(new LambdaCallable(*observer, [&](const MyObservable::MyCustomMessage& message) {testCounter += 2 * message.payload_;})); - - testCounter = 0; - observable.BroadcastMessage(MyObservable::MyCustomMessage(12)); - ASSERT_EQ(24, testCounter); - - // delete the observer and check that the callback is not called anymore - delete observer; - - // the connection is permanent; if we emit the same message again, the observer will be notified again - testCounter = 0; - observable.BroadcastMessage(MyObservable::MyCustomMessage(20)); - ASSERT_EQ(0, testCounter); -} - -namespace { - class MyObserverWithLambda : public IObserver { - private: - int multiplier_; // this is a private variable we want to access in a lambda - - public: - MyObserverWithLambda(MessageBroker& broker, int multiplier, MyObservable& observable) - : IObserver(broker), - multiplier_(multiplier) - { - // register a callable to a lambda that access private members - observable.RegisterObserverCallback(new LambdaCallable(*this, [this](const MyObservable::MyCustomMessage& message) { - testCounter += multiplier_ * message.payload_; - })); - - } - }; -} - -TEST(MessageBroker, TestLambdaCaptureThisAndAccessPrivateMembers) -{ - MessageBroker broker; - MyObservable observable(broker); - MyObserverWithLambda* observer = new MyObserverWithLambda(broker, 3, observable); - - testCounter = 0; - observable.BroadcastMessage(MyObservable::MyCustomMessage(12)); - ASSERT_EQ(36, testCounter); - - // delete the observer and check that the callback is not called anymore - delete observer; - - // the connection is permanent; if we emit the same message again, the observer will be notified again - testCounter = 0; - observable.BroadcastMessage(MyObservable::MyCustomMessage(20)); - ASSERT_EQ(0, testCounter); -} - -#endif // C++ 11 diff -r 1c7ae79c426d -r 68579a31eeb4 UnitTestsSources/TestStructureSet.cpp diff -r 1c7ae79c426d -r 68579a31eeb4 UnitTestsSources/UnitTestsMain.cpp --- a/UnitTestsSources/UnitTestsMain.cpp Tue Feb 04 15:23:33 2020 +0100 +++ b/UnitTestsSources/UnitTestsMain.cpp Tue Feb 04 15:24:03 2020 +0100 @@ -25,6 +25,7 @@ #include "../Framework/Deprecated/Toolbox/DownloadStack.h" #include "../Framework/Deprecated/Toolbox/MessagingToolbox.h" #include "../Framework/Deprecated/Toolbox/OrthancSlicesLoader.h" +#include "../Framework/StoneInitialization.h" #include "../Framework/Toolbox/FiniteProjectiveCamera.h" #include "../Framework/Toolbox/GeometryToolbox.h" #include "../Framework/Volumes/ImageBuffer3D.h" @@ -566,20 +567,20 @@ } -static bool IsEqualVector(OrthancStone::Vector a, - OrthancStone::Vector b) +static bool IsEqualRotationVector(OrthancStone::Vector a, + OrthancStone::Vector b) { - if (a.size() == 3 && - b.size() == 3) + if (a.size() != b.size() || + a.size() != 3) + { + return false; + } + else { OrthancStone::LinearAlgebra::NormalizeVector(a); OrthancStone::LinearAlgebra::NormalizeVector(b); return OrthancStone::LinearAlgebra::IsCloseToZero(boost::numeric::ublas::norm_2(a - b)); } - else - { - return false; - } } @@ -593,29 +594,29 @@ OrthancStone::GeometryToolbox::AlignVectorsWithRotation(r, a, b); ASSERT_TRUE(OrthancStone::LinearAlgebra::IsRotationMatrix(r)); - ASSERT_TRUE(IsEqualVector(OrthancStone::LinearAlgebra::Product(r, a), b)); + ASSERT_TRUE(IsEqualRotationVector(OrthancStone::LinearAlgebra::Product(r, a), b)); OrthancStone::GeometryToolbox::AlignVectorsWithRotation(r, b, a); ASSERT_TRUE(OrthancStone::LinearAlgebra::IsRotationMatrix(r)); - ASSERT_TRUE(IsEqualVector(OrthancStone::LinearAlgebra::Product(r, b), a)); + ASSERT_TRUE(IsEqualRotationVector(OrthancStone::LinearAlgebra::Product(r, b), a)); OrthancStone::LinearAlgebra::AssignVector(a, 1, 0, 0); OrthancStone::LinearAlgebra::AssignVector(b, 0, 0, 1); OrthancStone::GeometryToolbox::AlignVectorsWithRotation(r, a, b); ASSERT_TRUE(OrthancStone::LinearAlgebra::IsRotationMatrix(r)); - ASSERT_TRUE(IsEqualVector(OrthancStone::LinearAlgebra::Product(r, a), b)); + ASSERT_TRUE(IsEqualRotationVector(OrthancStone::LinearAlgebra::Product(r, a), b)); OrthancStone::LinearAlgebra::AssignVector(a, 0, 1, 0); OrthancStone::LinearAlgebra::AssignVector(b, 0, 0, 1); OrthancStone::GeometryToolbox::AlignVectorsWithRotation(r, a, b); ASSERT_TRUE(OrthancStone::LinearAlgebra::IsRotationMatrix(r)); - ASSERT_TRUE(IsEqualVector(OrthancStone::LinearAlgebra::Product(r, a), b)); + ASSERT_TRUE(IsEqualRotationVector(OrthancStone::LinearAlgebra::Product(r, a), b)); OrthancStone::LinearAlgebra::AssignVector(a, 0, 0, 1); OrthancStone::LinearAlgebra::AssignVector(b, 0, 0, 1); OrthancStone::GeometryToolbox::AlignVectorsWithRotation(r, a, b); ASSERT_TRUE(OrthancStone::LinearAlgebra::IsRotationMatrix(r)); - ASSERT_TRUE(IsEqualVector(OrthancStone::LinearAlgebra::Product(r, a), b)); + ASSERT_TRUE(IsEqualRotationVector(OrthancStone::LinearAlgebra::Product(r, a), b)); OrthancStone::LinearAlgebra::AssignVector(a, 0, 0, 0); OrthancStone::LinearAlgebra::AssignVector(b, 0, 0, 1); @@ -624,11 +625,11 @@ // TODO: Deal with opposite vectors /* - OrthancStone::LinearAlgebra::AssignVector(a, 0, 0, -1); - OrthancStone::LinearAlgebra::AssignVector(b, 0, 0, 1); - OrthancStone::GeometryToolbox::AlignVectorsWithRotation(r, a, b); - OrthancStone::LinearAlgebra::Print(r); - OrthancStone::LinearAlgebra::Print(boost::numeric::ublas::prod(r, a)); + OrthancStone::LinearAlgebra::AssignVector(a, 0, 0, -1); + OrthancStone::LinearAlgebra::AssignVector(b, 0, 0, 1); + OrthancStone::GeometryToolbox::AlignVectorsWithRotation(r, a, b); + OrthancStone::LinearAlgebra::Print(r); + OrthancStone::LinearAlgebra::Print(boost::numeric::ublas::prod(r, a)); */ } @@ -639,13 +640,39 @@ ASSERT_TRUE(Deprecated::MessagingToolbox::ParseJson(response, source.c_str(), source.size())); } + + +static bool IsEqualVectorL1(OrthancStone::Vector a, + OrthancStone::Vector b) +{ + if (a.size() != b.size()) + { + return false; + } + else + { + for (size_t i = 0; i < a.size(); i++) + { + if (!OrthancStone::LinearAlgebra::IsNear(a[i], b[i], 0.0001)) + { + return false; + } + } + + return true; + } +} + + TEST(VolumeImageGeometry, Basic) { - OrthancStone::VolumeImageGeometry g; + using namespace OrthancStone; + + VolumeImageGeometry g; g.SetSizeInVoxels(10, 20, 30); g.SetVoxelDimensions(1, 2, 3); - OrthancStone::Vector p = g.GetCoordinates(0, 0, 0); + Vector p = g.GetCoordinates(0, 0, 0); ASSERT_EQ(3u, p.size()); ASSERT_DOUBLE_EQ(-1.0 / 2.0, p[0]); ASSERT_DOUBLE_EQ(-2.0 / 2.0, p[1]); @@ -656,69 +683,148 @@ ASSERT_DOUBLE_EQ(-2.0 / 2.0 + 20.0 * 2.0, p[1]); ASSERT_DOUBLE_EQ(-3.0 / 2.0 + 30.0 * 3.0, p[2]); - OrthancStone::VolumeProjection proj; + VolumeProjection proj; ASSERT_TRUE(g.DetectProjection(proj, g.GetAxialGeometry().GetNormal())); - ASSERT_EQ(OrthancStone::VolumeProjection_Axial, proj); + ASSERT_EQ(VolumeProjection_Axial, proj); ASSERT_TRUE(g.DetectProjection(proj, g.GetCoronalGeometry().GetNormal())); - ASSERT_EQ(OrthancStone::VolumeProjection_Coronal, proj); + ASSERT_EQ(VolumeProjection_Coronal, proj); ASSERT_TRUE(g.DetectProjection(proj, g.GetSagittalGeometry().GetNormal())); - ASSERT_EQ(OrthancStone::VolumeProjection_Sagittal, proj); + ASSERT_EQ(VolumeProjection_Sagittal, proj); - ASSERT_EQ(10u, g.GetProjectionWidth(OrthancStone::VolumeProjection_Axial)); - ASSERT_EQ(20u, g.GetProjectionHeight(OrthancStone::VolumeProjection_Axial)); - ASSERT_EQ(30u, g.GetProjectionDepth(OrthancStone::VolumeProjection_Axial)); - ASSERT_EQ(10u, g.GetProjectionWidth(OrthancStone::VolumeProjection_Coronal)); - ASSERT_EQ(30u, g.GetProjectionHeight(OrthancStone::VolumeProjection_Coronal)); - ASSERT_EQ(20u, g.GetProjectionDepth(OrthancStone::VolumeProjection_Coronal)); - ASSERT_EQ(20u, g.GetProjectionWidth(OrthancStone::VolumeProjection_Sagittal)); - ASSERT_EQ(30u, g.GetProjectionHeight(OrthancStone::VolumeProjection_Sagittal)); - ASSERT_EQ(10u, g.GetProjectionDepth(OrthancStone::VolumeProjection_Sagittal)); + ASSERT_EQ(10u, g.GetProjectionWidth(VolumeProjection_Axial)); + ASSERT_EQ(20u, g.GetProjectionHeight(VolumeProjection_Axial)); + ASSERT_EQ(30u, g.GetProjectionDepth(VolumeProjection_Axial)); + ASSERT_EQ(10u, g.GetProjectionWidth(VolumeProjection_Coronal)); + ASSERT_EQ(30u, g.GetProjectionHeight(VolumeProjection_Coronal)); + ASSERT_EQ(20u, g.GetProjectionDepth(VolumeProjection_Coronal)); + ASSERT_EQ(20u, g.GetProjectionWidth(VolumeProjection_Sagittal)); + ASSERT_EQ(30u, g.GetProjectionHeight(VolumeProjection_Sagittal)); + ASSERT_EQ(10u, g.GetProjectionDepth(VolumeProjection_Sagittal)); - p = g.GetVoxelDimensions(OrthancStone::VolumeProjection_Axial); + p = g.GetVoxelDimensions(VolumeProjection_Axial); ASSERT_EQ(3u, p.size()); ASSERT_DOUBLE_EQ(1, p[0]); ASSERT_DOUBLE_EQ(2, p[1]); ASSERT_DOUBLE_EQ(3, p[2]); - p = g.GetVoxelDimensions(OrthancStone::VolumeProjection_Coronal); + p = g.GetVoxelDimensions(VolumeProjection_Coronal); ASSERT_EQ(3u, p.size()); ASSERT_DOUBLE_EQ(1, p[0]); ASSERT_DOUBLE_EQ(3, p[1]); ASSERT_DOUBLE_EQ(2, p[2]); - p = g.GetVoxelDimensions(OrthancStone::VolumeProjection_Sagittal); + p = g.GetVoxelDimensions(VolumeProjection_Sagittal); ASSERT_EQ(3u, p.size()); ASSERT_DOUBLE_EQ(2, p[0]); ASSERT_DOUBLE_EQ(3, p[1]); ASSERT_DOUBLE_EQ(1, p[2]); - ASSERT_EQ(0, (int) OrthancStone::VolumeProjection_Axial); - ASSERT_EQ(1, (int) OrthancStone::VolumeProjection_Coronal); - ASSERT_EQ(2, (int) OrthancStone::VolumeProjection_Sagittal); + // Loop over all the voxels of the volume + for (unsigned int z = 0; z < g.GetDepth(); z++) + { + const float zz = (0.5f + static_cast(z)) / static_cast(g.GetDepth()); // Z-center of the voxel + + for (unsigned int y = 0; y < g.GetHeight(); y++) + { + const float yy = (0.5f + static_cast(y)) / static_cast(g.GetHeight()); // Y-center of the voxel + + for (unsigned int x = 0; x < g.GetWidth(); x++) + { + const float xx = (0.5f + static_cast(x)) / static_cast(g.GetWidth()); // X-center of the voxel + + const float sx = 1.0f; + const float sy = 2.0f; + const float sz = 3.0f; + + Vector p = g.GetCoordinates(xx, yy, zz); + + Vector q = (g.GetAxialGeometry().MapSliceToWorldCoordinates( + static_cast(x) * sx, + static_cast(y) * sy) + + z * sz * g.GetAxialGeometry().GetNormal()); + ASSERT_TRUE(IsEqualVectorL1(p, q)); + + q = (g.GetCoronalGeometry().MapSliceToWorldCoordinates( + static_cast(x) * sx, + static_cast(g.GetDepth() - 1 - z) * sz) + + y * sy * g.GetCoronalGeometry().GetNormal()); + ASSERT_TRUE(IsEqualVectorL1(p, q)); + + /** + * WARNING: In sagittal geometry, the normal points to + * REDUCING X-axis in the 3D world. This is necessary to keep + * the right-hand coordinate system. Hence the "-". + **/ + q = (g.GetSagittalGeometry().MapSliceToWorldCoordinates( + static_cast(y) * sy, + static_cast(g.GetDepth() - 1 - z) * sz) + + x * sx * (-g.GetSagittalGeometry().GetNormal())); + ASSERT_TRUE(IsEqualVectorL1(p, q)); + } + } + } + + ASSERT_EQ(0, (int) VolumeProjection_Axial); + ASSERT_EQ(1, (int) VolumeProjection_Coronal); + ASSERT_EQ(2, (int) VolumeProjection_Sagittal); for (int p = 0; p < 3; p++) { - OrthancStone::VolumeProjection projection = (OrthancStone::VolumeProjection) p; - const OrthancStone::CoordinateSystem3D& s = g.GetProjectionGeometry(projection); + VolumeProjection projection = (VolumeProjection) p; + const CoordinateSystem3D& s = g.GetProjectionGeometry(projection); ASSERT_THROW(g.GetProjectionSlice(projection, g.GetProjectionDepth(projection)), Orthanc::OrthancException); for (unsigned int i = 0; i < g.GetProjectionDepth(projection); i++) { - OrthancStone::CoordinateSystem3D plane = g.GetProjectionSlice(projection, i); + CoordinateSystem3D plane = g.GetProjectionSlice(projection, i); - ASSERT_TRUE(IsEqualVector(plane.GetOrigin(), s.GetOrigin() + static_cast(i) * - s.GetNormal() * g.GetVoxelDimensions(projection)[2])); - ASSERT_TRUE(IsEqualVector(plane.GetAxisX(), s.GetAxisX())); - ASSERT_TRUE(IsEqualVector(plane.GetAxisY(), s.GetAxisY())); + if (projection == VolumeProjection_Sagittal) + { + ASSERT_TRUE(IsEqualVectorL1(plane.GetOrigin(), s.GetOrigin() + static_cast(i) * + (-s.GetNormal()) * g.GetVoxelDimensions(projection)[2])); + } + else + { + ASSERT_TRUE(IsEqualVectorL1(plane.GetOrigin(), s.GetOrigin() + static_cast(i) * + s.GetNormal() * g.GetVoxelDimensions(projection)[2])); + } + + ASSERT_TRUE(IsEqualVectorL1(plane.GetAxisX(), s.GetAxisX())); + ASSERT_TRUE(IsEqualVectorL1(plane.GetAxisY(), s.GetAxisY())); unsigned int slice; - OrthancStone::VolumeProjection q; + VolumeProjection q; ASSERT_TRUE(g.DetectSlice(q, slice, plane)); ASSERT_EQ(projection, q); - ASSERT_EQ(i, slice); + ASSERT_EQ(i, slice); } } } + +TEST(LinearAlgebra, ParseVectorLocale) +{ + OrthancStone::Vector v; + + ASSERT_TRUE(OrthancStone::LinearAlgebra::ParseVector(v, "1.2")); + ASSERT_EQ(1u, v.size()); + ASSERT_FLOAT_EQ(1.2f, v[0]); + + ASSERT_TRUE(OrthancStone::LinearAlgebra::ParseVector(v, "-1.2e+2")); + ASSERT_EQ(1u, v.size()); + ASSERT_FLOAT_EQ(-120.0f, v[0]); + + ASSERT_TRUE(OrthancStone::LinearAlgebra::ParseVector(v, "-1e-2\\2")); + ASSERT_EQ(2u, v.size()); + ASSERT_FLOAT_EQ(-0.01f, v[0]); + ASSERT_FLOAT_EQ(2.0f, v[1]); + + ASSERT_TRUE(OrthancStone::LinearAlgebra::ParseVector(v, "1.3671875\\1.3671875")); + ASSERT_EQ(2u, v.size()); + ASSERT_FLOAT_EQ(1.3671875, v[0]); + ASSERT_FLOAT_EQ(1.3671875, v[1]); +} + + int main(int argc, char **argv) { Orthanc::Logging::Initialize();