changeset 828:28f99af358fa

Merge + FusionMprSdl
author Benjamin Golinvaux <bgo@osimis.io>
date Wed, 29 May 2019 16:15:04 +0200
parents 2fd96a637a59 (diff) 5c7b08bf84af (current diff)
children 3a984741686f
files Framework/Loaders/DicomStructureSetLoader.cpp Framework/Messages/MessageBroker.h Framework/Oracle/ThreadedOracle.h Samples/Sdl/FusionMprSdl.cpp Samples/Sdl/FusionMprSdl.h Samples/Sdl/Loader.cpp
diffstat 8 files changed, 1051 insertions(+), 17 deletions(-) [+]
line wrap: on
line diff
--- a/Framework/Loaders/DicomStructureSetLoader.cpp	Wed May 29 13:42:34 2019 +0200
+++ b/Framework/Loaders/DicomStructureSetLoader.cpp	Wed May 29 16:15:04 2019 +0200
@@ -128,7 +128,7 @@
       std::set<std::string> instances;
       loader.content_->GetReferencedInstances(instances);
 
-      loader.countReferencedInstances_ = instances.size();
+      loader.countReferencedInstances_ = static_cast<unsigned int>(instances.size());
 
       for (std::set<std::string>::const_iterator
              it = instances.begin(); it != instances.end(); ++it)
--- a/Framework/Messages/MessageBroker.h	Wed May 29 13:42:34 2019 +0200
+++ b/Framework/Messages/MessageBroker.h	Wed May 29 16:15:04 2019 +0200
@@ -18,8 +18,9 @@
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  **/
 
+#pragma once
 
-#pragma once
+#include "../StoneException.h"
 
 #include "boost/noncopyable.hpp"
 
@@ -40,6 +41,13 @@
     std::set<const IObserver*> activeObservers_;  // the list of observers that are currently alive (that have not been deleted)
 
   public:
+    MessageBroker()
+    {
+      static bool created = false;
+      ORTHANC_ASSERT(!created, "One broker to rule them all!");
+      created = true;
+    }
+
     void Register(const IObserver& observer)
     {
       activeObservers_.insert(&observer);
--- a/Framework/Oracle/ThreadedOracle.h	Wed May 29 13:42:34 2019 +0200
+++ b/Framework/Oracle/ThreadedOracle.h	Wed May 29 16:15:04 2019 +0200
@@ -74,6 +74,7 @@
 
     virtual ~ThreadedOracle();
 
+    // The reference is not stored.
     void SetOrthancParameters(const Orthanc::WebServiceParameters& orthanc);
 
     void SetThreadsCount(unsigned int count);
--- a/Framework/Scene2DViewport/AngleMeasureTool.cpp	Wed May 29 13:42:34 2019 +0200
+++ b/Framework/Scene2DViewport/AngleMeasureTool.cpp	Wed May 29 16:15:04 2019 +0200
@@ -27,13 +27,13 @@
 #include <boost/math/constants/constants.hpp>
 #include <boost/make_shared.hpp>
 
-// <HACK>
-// REMOVE THIS
-#ifndef NDEBUG
-extern void 
-TrackerSample_SetInfoDisplayMessage(std::string key, std::string value);
-#endif
-// </HACK>
+//// <HACK>
+//// REMOVE THIS
+//#ifndef NDEBUG
+//extern void 
+//TrackerSample_SetInfoDisplayMessage(std::string key, std::string value);
+//#endif
+//// </HACK>
 
 namespace OrthancStone
 {
@@ -175,6 +175,7 @@
           SetTextLayerOutlineProperties(
             GetScene(), layerHolder_, buf, ScenePoint2D(pointX, pointY));
 
+#if 0
           // TODO:make it togglable
           bool enableInfoDisplay = false;
           if (enableInfoDisplay)
@@ -224,6 +225,7 @@
             TrackerSample_SetInfoDisplayMessage("angleDeg",
               boost::lexical_cast<std::string>(angleDeg));
           }
+#endif
         }
       }
       else
--- a/Samples/Sdl/CMakeLists.txt	Wed May 29 13:42:34 2019 +0200
+++ b/Samples/Sdl/CMakeLists.txt	Wed May 29 16:15:04 2019 +0200
@@ -55,19 +55,19 @@
   ${ORTHANC_STONE_SOURCES}
   )
 
+#
+# BasicScene
+# 
+
 add_executable(BasicScene
   BasicScene.cpp
   )
 
 target_link_libraries(BasicScene OrthancStone)
 
-if(ENABLE_SDL_CONSOLE)
-  add_definitions(
-    -DENABLE_SDL_CONSOLE=1
-    )
-  LIST(APPEND TRACKERSAMPLE_SOURCE "../../../SDL-Console/SDL_Console.c")
-  LIST(APPEND TRACKERSAMPLE_SOURCE "../../../SDL-Console/SDL_Console.h")
-endif()
+#
+# BasicScene
+# 
 
 LIST(APPEND TRACKERSAMPLE_SOURCE "TrackerSample.cpp")
 LIST(APPEND TRACKERSAMPLE_SOURCE "TrackerSampleApp.cpp")
@@ -83,8 +83,23 @@
 
 target_link_libraries(TrackerSample OrthancStone)
 
+#
+# Loader
+# 
+
 add_executable(Loader
   Loader.cpp
   )
 
 target_link_libraries(Loader OrthancStone)
+
+#
+# FusionMprSdl
+# 
+
+add_executable(FusionMprSdl
+  FusionMprSdl.cpp
+  FusionMprSdl.h
+)
+
+target_link_libraries(FusionMprSdl OrthancStone)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Sdl/FusionMprSdl.cpp	Wed May 29 16:15:04 2019 +0200
@@ -0,0 +1,799 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 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 <http://www.gnu.org/licenses/>.
+ **/
+
+#include "FusionMprSdl.h"
+
+#include "../../Applications/Sdl/SdlOpenGLWindow.h"
+
+#include "../../Framework/StoneInitialization.h"
+
+#include "../../Framework/Scene2D/CairoCompositor.h"
+#include "../../Framework/Scene2D/ColorTextureSceneLayer.h"
+#include "../../Framework/Scene2D/OpenGLCompositor.h"
+#include "../../Framework/Scene2D/PanSceneTracker.h"
+#include "../../Framework/Scene2D/ZoomSceneTracker.h"
+#include "../../Framework/Scene2D/RotateSceneTracker.h"
+
+#include "../../Framework/Scene2DViewport/CreateLineMeasureTracker.h"
+#include "../../Framework/Scene2DViewport/CreateAngleMeasureTracker.h"
+#include "../../Framework/Scene2DViewport/IFlexiblePointerTracker.h"
+#include "../../Framework/Scene2DViewport/MeasureTool.h"
+#include "../../Framework/Scene2DViewport/PredeclaredTypes.h"
+
+#include "../../Framework/Volumes/VolumeSceneLayerSource.h"
+
+#include <Core/Images/Image.h>
+#include <Core/Images/ImageProcessing.h>
+#include <Core/Images/PngWriter.h>
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+
+#include <boost/shared_ptr.hpp>
+#include <boost/weak_ptr.hpp>
+#include <boost/make_shared.hpp>
+
+#include <stdio.h>
+#include "../../Framework/Oracle/GetOrthancWebViewerJpegCommand.h"
+#include "../../Framework/Oracle/ThreadedOracle.h"
+#include "../../Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h"
+#include "../../Framework/Loaders/OrthancMultiframeVolumeLoader.h"
+#include "../../Framework/Loaders/DicomStructureSetLoader.h"
+#include "../../Framework/Scene2D/GrayscaleStyleConfigurator.h"
+#include "../../Framework/Scene2D/LookupTableStyleConfigurator.h"
+#include "../../Framework/Volumes/DicomVolumeImageMPRSlicer.h"
+#include "Core/SystemToolbox.h"
+
+namespace OrthancStone
+{
+  const char* FusionMprMeasureToolToString(size_t i)
+  {
+    static const char* descs[] = {
+      "FusionMprGuiTool_Rotate",
+      "FusionMprGuiTool_Pan",
+      "FusionMprGuiTool_Zoom",
+      "FusionMprGuiTool_LineMeasure",
+      "FusionMprGuiTool_CircleMeasure",
+      "FusionMprGuiTool_AngleMeasure",
+      "FusionMprGuiTool_EllipseMeasure",
+      "FusionMprGuiTool_LAST"
+    };
+    if (i >= FusionMprGuiTool_LAST)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "Wrong tool index");
+    }
+    return descs[i];
+  }
+
+  boost::shared_ptr<Scene2D> FusionMprSdlApp::GetScene()
+  {
+    return controller_->GetScene();
+  }
+
+  boost::shared_ptr<const Scene2D> FusionMprSdlApp::GetScene() const
+  {
+    return controller_->GetScene();
+  }
+
+  void FusionMprSdlApp::SelectNextTool()
+  {
+    currentTool_ = static_cast<FusionMprGuiTool>(currentTool_ + 1);
+    if (currentTool_ == FusionMprGuiTool_LAST)
+      currentTool_ = static_cast<FusionMprGuiTool>(0);;
+    printf("Current tool is now: %s\n", FusionMprMeasureToolToString(currentTool_));
+  }
+
+  void FusionMprSdlApp::DisplayInfoText()
+  {
+    // do not try to use stuff too early!
+    if (compositor_.get() == NULL)
+      return;
+
+    std::stringstream msg;
+	
+	for (std::map<std::string, std::string>::const_iterator kv = infoTextMap_.begin();
+		kv != infoTextMap_.end(); ++kv)
+    {
+      msg << kv->first << " : " << kv->second << std::endl;
+    }
+	std::string msgS = msg.str();
+
+    TextSceneLayer* layerP = NULL;
+    if (GetScene()->HasLayer(FIXED_INFOTEXT_LAYER_ZINDEX))
+    {
+      TextSceneLayer& layer = dynamic_cast<TextSceneLayer&>(
+        GetScene()->GetLayer(FIXED_INFOTEXT_LAYER_ZINDEX));
+      layerP = &layer;
+    }
+    else
+    {
+      std::auto_ptr<TextSceneLayer> layer(new TextSceneLayer);
+      layerP = layer.get();
+      layer->SetColor(0, 255, 0);
+      layer->SetFontIndex(1);
+      layer->SetBorder(20);
+      layer->SetAnchor(BitmapAnchor_TopLeft);
+      //layer->SetPosition(0,0);
+      GetScene()->SetLayer(FIXED_INFOTEXT_LAYER_ZINDEX, layer.release());
+    }
+    // position the fixed info text in the upper right corner
+    layerP->SetText(msgS.c_str());
+    double cX = compositor_->GetCanvasWidth() * (-0.5);
+    double cY = compositor_->GetCanvasHeight() * (-0.5);
+    GetScene()->GetCanvasToSceneTransform().Apply(cX,cY);
+    layerP->SetPosition(cX, cY);
+  }
+
+  void FusionMprSdlApp::DisplayFloatingCtrlInfoText(const PointerEvent& e)
+  {
+    ScenePoint2D p = e.GetMainPosition().Apply(GetScene()->GetCanvasToSceneTransform());
+
+    char buf[128];
+    sprintf(buf, "S:(%0.02f,%0.02f) C:(%0.02f,%0.02f)", 
+      p.GetX(), p.GetY(), 
+      e.GetMainPosition().GetX(), e.GetMainPosition().GetY());
+
+    if (GetScene()->HasLayer(FLOATING_INFOTEXT_LAYER_ZINDEX))
+    {
+      TextSceneLayer& layer =
+        dynamic_cast<TextSceneLayer&>(GetScene()->GetLayer(FLOATING_INFOTEXT_LAYER_ZINDEX));
+      layer.SetText(buf);
+      layer.SetPosition(p.GetX(), p.GetY());
+    }
+    else
+    {
+      std::auto_ptr<TextSceneLayer> layer(new TextSceneLayer);
+      layer->SetColor(0, 255, 0);
+      layer->SetText(buf);
+      layer->SetBorder(20);
+      layer->SetAnchor(BitmapAnchor_BottomCenter);
+      layer->SetPosition(p.GetX(), p.GetY());
+      GetScene()->SetLayer(FLOATING_INFOTEXT_LAYER_ZINDEX, layer.release());
+    }
+  }
+
+  void FusionMprSdlApp::HideInfoText()
+  {
+    GetScene()->DeleteLayer(FLOATING_INFOTEXT_LAYER_ZINDEX);
+  }
+
+  void FusionMprSdlApp::HandleApplicationEvent(
+    const SDL_Event & event)
+  {
+    DisplayInfoText();
+
+    if (event.type == SDL_MOUSEMOTION)
+    {
+      int scancodeCount = 0;
+      const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount);
+
+      if (activeTracker_.get() == NULL &&
+        SDL_SCANCODE_LALT < scancodeCount &&
+        keyboardState[SDL_SCANCODE_LALT])
+      {
+        // The "left-ctrl" key is down, while no tracker is present
+        // Let's display the info text
+        PointerEvent e;
+        e.AddPosition(compositor_->GetPixelCenterCoordinates(
+          event.button.x, event.button.y));
+
+        DisplayFloatingCtrlInfoText(e);
+      }
+      else
+      {
+        HideInfoText();
+        //LOG(TRACE) << "(event.type == SDL_MOUSEMOTION)";
+        if (activeTracker_.get() != NULL)
+        {
+          //LOG(TRACE) << "(activeTracker_.get() != NULL)";
+          PointerEvent e;
+          e.AddPosition(compositor_->GetPixelCenterCoordinates(
+            event.button.x, event.button.y));
+          
+          //LOG(TRACE) << "event.button.x = " << event.button.x << "     " <<
+          //  "event.button.y = " << event.button.y;
+          LOG(TRACE) << "activeTracker_->PointerMove(e); " <<
+            e.GetMainPosition().GetX() << " " << e.GetMainPosition().GetY();
+          
+          activeTracker_->PointerMove(e);
+          if (!activeTracker_->IsAlive())
+            activeTracker_.reset();
+        }
+      }
+    }
+    else if (event.type == SDL_MOUSEBUTTONUP)
+    {
+      if (activeTracker_)
+      {
+        PointerEvent e;
+        e.AddPosition(compositor_->GetPixelCenterCoordinates(event.button.x, event.button.y));
+        activeTracker_->PointerUp(e);
+        if (!activeTracker_->IsAlive())
+          activeTracker_.reset();
+      }
+    }
+    else if (event.type == SDL_MOUSEBUTTONDOWN)
+    {
+      PointerEvent e;
+      e.AddPosition(compositor_->GetPixelCenterCoordinates(
+        event.button.x, event.button.y));
+      if (activeTracker_)
+      {
+        activeTracker_->PointerDown(e);
+        if (!activeTracker_->IsAlive())
+          activeTracker_.reset();
+      }
+      else
+      {
+        // we ATTEMPT to create a tracker if need be
+        activeTracker_ = CreateSuitableTracker(event, e);
+      }
+    }
+    else if (event.type == SDL_KEYDOWN &&
+      event.key.repeat == 0 /* Ignore key bounce */)
+    {
+      switch (event.key.keysym.sym)
+      {
+      case SDLK_ESCAPE:
+        if (activeTracker_)
+        {
+          activeTracker_->Cancel();
+          if (!activeTracker_->IsAlive())
+            activeTracker_.reset();
+        }
+        break;
+
+      case SDLK_t:
+        if (!activeTracker_)
+          SelectNextTool();
+        else
+        {
+          LOG(WARNING) << "You cannot change the active tool when an interaction"
+            " is taking place";
+        }
+        break;
+      case SDLK_s:
+        controller_->FitContent(compositor_->GetCanvasWidth(),
+          compositor_->GetCanvasHeight());
+        break;
+
+      case SDLK_z:
+        LOG(TRACE) << "SDLK_z has been pressed. event.key.keysym.mod == " << event.key.keysym.mod;
+        if (event.key.keysym.mod & KMOD_CTRL)
+        {
+          if (controller_->CanUndo())
+          {
+            LOG(TRACE) << "Undoing...";
+            controller_->Undo();
+          }
+          else
+          {
+            LOG(WARNING) << "Nothing to undo!!!";
+          }
+        }
+        break;
+
+      case SDLK_y:
+        LOG(TRACE) << "SDLK_y has been pressed. event.key.keysym.mod == " << event.key.keysym.mod;
+        if (event.key.keysym.mod & KMOD_CTRL)
+        {
+          if (controller_->CanRedo())
+          {
+            LOG(TRACE) << "Redoing...";
+            controller_->Redo();
+          }
+          else
+          {
+            LOG(WARNING) << "Nothing to redo!!!";
+          }
+        }
+        break;
+
+      case SDLK_c:
+        TakeScreenshot(
+          "screenshot.png",
+          compositor_->GetCanvasWidth(),
+          compositor_->GetCanvasHeight());
+        break;
+
+      default:
+        break;
+      }
+    }
+  }
+
+
+  void FusionMprSdlApp::OnSceneTransformChanged(
+    const ViewportController::SceneTransformChanged& message)
+  {
+    DisplayInfoText();
+  }
+
+  boost::shared_ptr<IFlexiblePointerTracker> FusionMprSdlApp::CreateSuitableTracker(
+    const SDL_Event & event,
+    const PointerEvent & e)
+  {
+    using namespace Orthanc;
+
+    switch (event.button.button)
+    {
+    case SDL_BUTTON_MIDDLE:
+      return boost::shared_ptr<IFlexiblePointerTracker>(new PanSceneTracker
+        (controller_, e));
+
+    case SDL_BUTTON_RIGHT:
+      return boost::shared_ptr<IFlexiblePointerTracker>(new ZoomSceneTracker
+        (controller_, e, compositor_->GetCanvasHeight()));
+
+    case SDL_BUTTON_LEFT:
+    {
+      //LOG(TRACE) << "CreateSuitableTracker: case SDL_BUTTON_LEFT:";
+      // TODO: we need to iterate on the set of measuring tool and perform
+      // a hit test to check if a tracker needs to be created for edition.
+      // Otherwise, depending upon the active tool, we might want to create
+      // a "measuring tool creation" tracker
+
+      // TODO: if there are conflicts, we should prefer a tracker that 
+      // pertains to the type of measuring tool currently selected (TBD?)
+      boost::shared_ptr<IFlexiblePointerTracker> hitTestTracker = TrackerHitTest(e);
+
+      if (hitTestTracker != NULL)
+      {
+        //LOG(TRACE) << "hitTestTracker != NULL";
+        return hitTestTracker;
+      }
+      else
+      {
+        switch (currentTool_)
+        {
+        case FusionMprGuiTool_Rotate:
+          //LOG(TRACE) << "Creating RotateSceneTracker";
+          return boost::shared_ptr<IFlexiblePointerTracker>(new RotateSceneTracker(
+            controller_, e));
+        case FusionMprGuiTool_Pan:
+          return boost::shared_ptr<IFlexiblePointerTracker>(new PanSceneTracker(
+            controller_, e));
+        case FusionMprGuiTool_Zoom:
+          return boost::shared_ptr<IFlexiblePointerTracker>(new ZoomSceneTracker(
+            controller_, e, compositor_->GetCanvasHeight()));
+        //case GuiTool_AngleMeasure:
+        //  return new AngleMeasureTracker(GetScene(), e);
+        //case GuiTool_CircleMeasure:
+        //  return new CircleMeasureTracker(GetScene(), e);
+        //case GuiTool_EllipseMeasure:
+        //  return new EllipseMeasureTracker(GetScene(), e);
+        case FusionMprGuiTool_LineMeasure:
+          return boost::shared_ptr<IFlexiblePointerTracker>(new CreateLineMeasureTracker(
+            IObserver::GetBroker(), controller_, e));
+        case FusionMprGuiTool_AngleMeasure:
+          return boost::shared_ptr<IFlexiblePointerTracker>(new CreateAngleMeasureTracker(
+            IObserver::GetBroker(), controller_, e));
+        case FusionMprGuiTool_CircleMeasure:
+          LOG(ERROR) << "Not implemented yet!";
+          return boost::shared_ptr<IFlexiblePointerTracker>();
+        case FusionMprGuiTool_EllipseMeasure:
+          LOG(ERROR) << "Not implemented yet!";
+          return boost::shared_ptr<IFlexiblePointerTracker>();
+        default:
+          throw OrthancException(ErrorCode_InternalError, "Wrong tool!");
+        }
+      }
+    }
+    default:
+      return boost::shared_ptr<IFlexiblePointerTracker>();
+    }
+  }
+
+
+  FusionMprSdlApp::FusionMprSdlApp(MessageBroker& broker)
+    : IObserver(broker)
+    , broker_(broker)
+    , oracleObservable_(broker)
+    , oracle_(*this)
+    , currentTool_(FusionMprGuiTool_Rotate)
+  {
+    //oracleObservable.RegisterObserverCallback
+    //(new Callable
+    //  <FusionMprSdlApp, SleepOracleCommand::TimeoutMessage>(*this, &FusionMprSdlApp::Handle));
+
+    //oracleObservable.RegisterObserverCallback
+    //(new Callable
+    //  <Toto, GetOrthancImageCommand::SuccessMessage>(*this, &FusionMprSdlApp::Handle));
+
+    //oracleObservable.RegisterObserverCallback
+    //(new Callable
+    //  <FusionMprSdlApp, GetOrthancWebViewerJpegCommand::SuccessMessage>(*this, &ToFusionMprSdlAppto::Handle));
+
+    oracleObservable_.RegisterObserverCallback
+    (new Callable
+      <FusionMprSdlApp, OracleCommandExceptionMessage>(*this, &FusionMprSdlApp::Handle));
+    
+    controller_ = boost::shared_ptr<ViewportController>(
+      new ViewportController(broker_));
+
+    controller_->RegisterObserverCallback(
+      new Callable<FusionMprSdlApp, ViewportController::SceneTransformChanged>
+      (*this, &FusionMprSdlApp::OnSceneTransformChanged));
+
+    TEXTURE_2x2_1_ZINDEX = 1;
+    TEXTURE_1x1_ZINDEX = 2;
+    TEXTURE_2x2_2_ZINDEX = 3;
+    LINESET_1_ZINDEX = 4;
+    LINESET_2_ZINDEX = 5;
+    FLOATING_INFOTEXT_LAYER_ZINDEX = 6;
+    FIXED_INFOTEXT_LAYER_ZINDEX = 7;
+  }
+
+  void FusionMprSdlApp::PrepareScene()
+  {
+    // Texture of 2x2 size
+    {
+      Orthanc::Image i(Orthanc::PixelFormat_RGB24, 2, 2, false);
+
+      uint8_t* p = reinterpret_cast<uint8_t*>(i.GetRow(0));
+      p[0] = 255;
+      p[1] = 0;
+      p[2] = 0;
+
+      p[3] = 0;
+      p[4] = 255;
+      p[5] = 0;
+
+      p = reinterpret_cast<uint8_t*>(i.GetRow(1));
+      p[0] = 0;
+      p[1] = 0;
+      p[2] = 255;
+
+      p[3] = 255;
+      p[4] = 0;
+      p[5] = 0;
+
+      GetScene()->SetLayer(TEXTURE_2x2_1_ZINDEX, new ColorTextureSceneLayer(i));
+    }
+  }
+
+  void FusionMprSdlApp::DisableTracker()
+  {
+    if (activeTracker_)
+    {
+      activeTracker_->Cancel();
+      activeTracker_.reset();
+    }
+  }
+
+  void FusionMprSdlApp::TakeScreenshot(const std::string& target,
+    unsigned int canvasWidth,
+    unsigned int canvasHeight)
+  {
+    CairoCompositor compositor(*GetScene(), canvasWidth, canvasHeight);
+    compositor.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, FONT_SIZE_0, Orthanc::Encoding_Latin1);
+    compositor.Refresh();
+
+    Orthanc::ImageAccessor canvas;
+    compositor.GetCanvas().GetReadOnlyAccessor(canvas);
+
+    Orthanc::Image png(Orthanc::PixelFormat_RGB24, canvas.GetWidth(), canvas.GetHeight(), false);
+    Orthanc::ImageProcessing::Convert(png, canvas);
+
+    Orthanc::PngWriter writer;
+    writer.WriteToFile(target, png);
+  }
+
+
+  boost::shared_ptr<IFlexiblePointerTracker> FusionMprSdlApp::TrackerHitTest(const PointerEvent & e)
+  {
+    // std::vector<boost::shared_ptr<MeasureTool>> measureTools_;
+    return boost::shared_ptr<IFlexiblePointerTracker>();
+  }
+
+  static void GLAPIENTRY
+    OpenGLMessageCallback(GLenum source,
+      GLenum type,
+      GLuint id,
+      GLenum severity,
+      GLsizei length,
+      const GLchar* message,
+      const void* userParam)
+  {
+    if (severity != GL_DEBUG_SEVERITY_NOTIFICATION)
+    {
+      fprintf(stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n",
+        (type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : ""),
+        type, severity, message);
+    }
+  }
+
+  static bool g_stopApplication = false;
+  
+
+  void FusionMprSdlApp::Handle(const DicomVolumeImage::GeometryReadyMessage& message)
+  {
+    printf("Geometry ready\n");
+
+    //plane_ = message.GetOrigin().GetGeometry().GetSagittalGeometry();
+    //plane_ = message.GetOrigin().GetGeometry().GetAxialGeometry();
+    plane_ = message.GetOrigin().GetGeometry().GetCoronalGeometry();
+    plane_.SetOrigin(message.GetOrigin().GetGeometry().GetCoordinates(0.5f, 0.5f, 0.5f));
+
+    //Refresh();
+  }
+
+
+  void FusionMprSdlApp::Handle(const OracleCommandExceptionMessage& message)
+  {
+    printf("EXCEPTION: [%s] on command type %d\n", message.GetException().What(), message.GetCommand().GetType());
+
+    switch (message.GetCommand().GetType())
+    {
+    case IOracleCommand::Type_GetOrthancWebViewerJpeg:
+      printf("URI: [%s]\n", dynamic_cast<const GetOrthancWebViewerJpegCommand&>
+        (message.GetCommand()).GetUri().c_str());
+      break;
+
+    default:
+      break;
+    }
+  }
+
+  void FusionMprSdlApp::SetVolume1(int depth,
+    const boost::shared_ptr<OrthancStone::IVolumeSlicer>& volume,
+    OrthancStone::ILayerStyleConfigurator* style)
+  {
+    source1_.reset(new OrthancStone::VolumeSceneLayerSource(*controller_->GetScene(), depth, volume));
+
+    if (style != NULL)
+    {
+      source1_->SetConfigurator(style);
+    }
+  }
+
+  void FusionMprSdlApp::SetVolume2(int depth,
+    const boost::shared_ptr<OrthancStone::IVolumeSlicer>& volume,
+    OrthancStone::ILayerStyleConfigurator* style)
+  {
+    source2_.reset(new OrthancStone::VolumeSceneLayerSource(*controller_->GetScene(), depth, volume));
+
+    if (style != NULL)
+    {
+      source2_->SetConfigurator(style);
+    }
+  }
+
+  void FusionMprSdlApp::SetStructureSet(int depth,
+    const boost::shared_ptr<OrthancStone::DicomStructureSetLoader>& volume)
+  {
+    source3_.reset(new OrthancStone::VolumeSceneLayerSource(*controller_->GetScene(), depth, volume));
+  }
+  
+  void FusionMprSdlApp::Run()
+  {
+    // False means we do NOT let Windows treat this as a legacy application
+    // that needs to be scaled
+    SdlOpenGLWindow window("Hello", 1024, 1024, false);
+
+    controller_->FitContent(window.GetCanvasWidth(), window.GetCanvasHeight());
+
+    glEnable(GL_DEBUG_OUTPUT);
+    glDebugMessageCallback(OpenGLMessageCallback, 0);
+
+    compositor_.reset(new OpenGLCompositor(window, *GetScene()));
+
+    compositor_->SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT,
+      FONT_SIZE_0, Orthanc::Encoding_Latin1);
+    compositor_->SetFont(1, Orthanc::EmbeddedResources::UBUNTU_FONT,
+      FONT_SIZE_1, Orthanc::Encoding_Latin1);
+
+
+    //////// from loader
+    {
+      Orthanc::WebServiceParameters p;
+      //p.SetUrl("http://localhost:8043/");
+      p.SetCredentials("orthanc", "orthanc");
+      oracle_.SetOrthancParameters(p);
+    }
+
+    //////// from Run
+
+    boost::shared_ptr<DicomVolumeImage>  ct(new DicomVolumeImage);
+    boost::shared_ptr<DicomVolumeImage>  dose(new DicomVolumeImage);
+
+
+    boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> ctLoader;
+    boost::shared_ptr<OrthancMultiframeVolumeLoader> doseLoader;
+    boost::shared_ptr<DicomStructureSetLoader>  rtstructLoader;
+
+    {
+      ctLoader.reset(new OrthancSeriesVolumeProgressiveLoader(ct, oracle_, oracleObservable_));
+      doseLoader.reset(new OrthancMultiframeVolumeLoader(dose, oracle_, oracleObservable_));
+      rtstructLoader.reset(new DicomStructureSetLoader(oracle_, oracleObservable_));
+    }
+
+    //toto->SetReferenceLoader(*ctLoader);
+    //doseLoader->RegisterObserverCallback
+    //(new Callable
+    //  <FusionMprSdlApp, DicomVolumeImage::GeometryReadyMessage>(*this, &FusionMprSdlApp::Handle));
+    ctLoader->RegisterObserverCallback
+    (new Callable
+      <FusionMprSdlApp, DicomVolumeImage::GeometryReadyMessage>(*this, &FusionMprSdlApp::Handle));
+
+    this->SetVolume1(0, ctLoader, new GrayscaleStyleConfigurator);
+
+    {
+      std::auto_ptr<LookupTableStyleConfigurator> config(new LookupTableStyleConfigurator);
+      config->SetLookupTable(Orthanc::EmbeddedResources::COLORMAP_HOT);
+
+      boost::shared_ptr<DicomVolumeImageMPRSlicer> tmp(new DicomVolumeImageMPRSlicer(dose));
+      this->SetVolume2(1, tmp, config.release());
+    }
+
+    this->SetStructureSet(2, rtstructLoader);
+
+#if 1
+    // BGO data
+    ctLoader->LoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");  // CT
+    //doseLoader->LoadInstance("830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb");  // RT-DOSE
+    //rtstructLoader->LoadInstance("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9");  // RT-STRUCT
+#else
+    //ctLoader->LoadSeries("cb3ea4d1-d08f3856-ad7b6314-74d88d77-60b05618");  // CT
+    //doseLoader->LoadInstance("41029085-71718346-811efac4-420e2c15-d39f99b6");  // RT-DOSE
+    //rtstructLoader->LoadInstance("83d9c0c3-913a7fee-610097d7-cbf0522d-fd75bee6");  // RT-STRUCT
+
+    // 2017-05-16
+    ctLoader->LoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");  // CT
+    doseLoader->LoadInstance("eac822ef-a395f94e-e8121fe0-8411fef8-1f7bffad");  // RT-DOSE
+    rtstructLoader->LoadInstance("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9");  // RT-STRUCT
+#endif
+
+    oracle_.Start();
+
+//// END from loader
+
+    while (!g_stopApplication)
+    {
+      compositor_->Refresh();
+
+//////// from loader
+      if (source1_.get() != NULL)
+      {
+        source1_->Update(plane_);
+      }
+
+      if (source2_.get() != NULL)
+      {
+        source2_->Update(plane_);
+      }
+
+      if (source3_.get() != NULL)
+      {
+        source3_->Update(plane_);
+      }
+//// END from loader
+
+      SDL_Event event;
+      while (!g_stopApplication && SDL_PollEvent(&event))
+      {
+        if (event.type == SDL_QUIT)
+        {
+          g_stopApplication = true;
+          break;
+        }
+        else if (event.type == SDL_WINDOWEVENT &&
+          event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
+        {
+          DisableTracker(); // was: tracker.reset(NULL);
+          compositor_->UpdateSize();
+        }
+        else if (event.type == SDL_KEYDOWN &&
+          event.key.repeat == 0 /* Ignore key bounce */)
+        {
+          switch (event.key.keysym.sym)
+          {
+          case SDLK_f:
+            window.GetWindow().ToggleMaximize();
+            break;
+
+          case SDLK_q:
+            g_stopApplication = true;
+            break;
+          default:
+            break;
+          }
+        }
+        HandleApplicationEvent(event);
+      }
+      SDL_Delay(1);
+    }
+
+    // the following is paramount because the compositor holds a reference
+    // to the scene and we do not want this reference to become dangling
+    compositor_.reset(NULL);
+
+    //// from loader
+
+    Orthanc::SystemToolbox::ServerBarrier();
+
+    /**
+     * WARNING => The oracle must be stopped BEFORE the objects using
+     * it are destroyed!!! This forces to wait for the completion of
+     * the running callback methods. Otherwise, the callbacks methods
+     * might still be running while their parent object is destroyed,
+     * resulting in crashes. This is very visible if adding a sleep(),
+     * as in (*).
+     **/
+
+    oracle_.Stop();
+    //// END from loader
+  }
+
+  void FusionMprSdlApp::SetInfoDisplayMessage(
+    std::string key, std::string value)
+  {
+    if (value == "")
+      infoTextMap_.erase(key);
+    else
+      infoTextMap_[key] = value;
+    DisplayInfoText();
+  }
+
+}
+
+
+boost::weak_ptr<OrthancStone::FusionMprSdlApp> g_app;
+
+void FusionMprSdl_SetInfoDisplayMessage(std::string key, std::string value)
+{
+  boost::shared_ptr<OrthancStone::FusionMprSdlApp> app = g_app.lock();
+  if (app)
+  {
+    app->SetInfoDisplayMessage(key, value);
+  }
+}
+
+/**
+ * IMPORTANT: The full arguments to "main()" are needed for SDL on
+ * Windows. Otherwise, one gets the linking error "undefined reference
+ * to `SDL_main'". https://wiki.libsdl.org/FAQWindows
+ **/
+int main(int argc, char* argv[])
+{
+  using namespace OrthancStone;
+
+  StoneInitialize();
+  Orthanc::Logging::EnableInfoLevel(true);
+//  Orthanc::Logging::EnableTraceLevel(true);
+
+  try
+  {
+    OrthancStone::MessageBroker broker;
+    boost::shared_ptr<FusionMprSdlApp> app(new FusionMprSdlApp(broker));
+    g_app = app;
+    app->PrepareScene();
+    app->Run();
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    LOG(ERROR) << "EXCEPTION: " << e.What();
+  }
+
+  StoneFinalize();
+
+  return 0;
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Sdl/FusionMprSdl.h	Wed May 29 16:15:04 2019 +0200
@@ -0,0 +1,202 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 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 <http://www.gnu.org/licenses/>.
+ **/
+
+#include "../../Framework/Messages/IObserver.h"
+#include "../../Framework/Messages/IMessageEmitter.h"
+#include "../../Framework/Oracle/OracleCommandExceptionMessage.h"
+#include "../../Framework/Scene2DViewport/ViewportController.h"
+#include "../../Framework/Volumes/DicomVolumeImage.h"
+#include "../../Framework/Oracle/ThreadedOracle.h"
+
+#include <boost/enable_shared_from_this.hpp>
+#include <boost/thread.hpp>
+#include <boost/noncopyable.hpp>
+
+#include <SDL.h>
+
+namespace OrthancStone
+{
+  class OpenGLCompositor;
+  class IVolumeSlicer;
+  class ILayerStyleConfigurator;
+  class DicomStructureSetLoader;
+  class IOracle;
+  class ThreadedOracle;
+  class VolumeSceneLayerSource;
+  class NativeFusionMprApplicationContext;
+
+   
+  enum FusionMprGuiTool
+  {
+    FusionMprGuiTool_Rotate = 0,
+    FusionMprGuiTool_Pan,
+    FusionMprGuiTool_Zoom,
+    FusionMprGuiTool_LineMeasure,
+    FusionMprGuiTool_CircleMeasure,
+    FusionMprGuiTool_AngleMeasure,
+    FusionMprGuiTool_EllipseMeasure,
+    FusionMprGuiTool_LAST
+  };
+
+  const char* MeasureToolToString(size_t i);
+
+  static const unsigned int FONT_SIZE_0 = 32;
+  static const unsigned int FONT_SIZE_1 = 24;
+
+  class Scene2D;
+
+  /**
+  This application subclasses IMessageEmitter to use a mutex before forwarding Oracle messages (that
+  can be sent from multiple threads)
+  */
+  class FusionMprSdlApp : public IObserver
+    , public boost::enable_shared_from_this<FusionMprSdlApp>
+    , public IMessageEmitter
+  {
+  public:
+    // 12 because.
+    FusionMprSdlApp(MessageBroker& broker);
+
+    void PrepareScene();
+    void Run();
+    void SetInfoDisplayMessage(std::string key, std::string value);
+    void DisableTracker();
+
+    boost::shared_ptr<Scene2D> GetScene();
+    boost::shared_ptr<const Scene2D> GetScene() const;
+
+    void HandleApplicationEvent(const SDL_Event& event);
+
+    /**
+    This method is called when the scene transform changes. It allows to
+    recompute the visual elements whose content depend upon the scene transform
+    */
+    void OnSceneTransformChanged(
+      const ViewportController::SceneTransformChanged& message);
+
+
+    virtual void EmitMessage(const IObserver& observer,
+      const IMessage& message) ORTHANC_OVERRIDE
+    {
+      try
+      {
+        boost::unique_lock<boost::shared_mutex>  lock(mutex_);
+        oracleObservable_.EmitMessage(observer, message);
+      }
+      catch (Orthanc::OrthancException& e)
+      {
+        LOG(ERROR) << "Exception while emitting a message: " << e.What();
+        throw;
+      }
+    }
+    
+  private:
+#if 1
+    // if threaded (not wasm)
+    MessageBroker& broker_;
+    IObservable oracleObservable_;
+    ThreadedOracle oracle_;
+    boost::shared_mutex mutex_; // to serialize messages from the ThreadedOracle
+#endif
+
+    void SelectNextTool();
+
+    /**
+    This returns a random point in the canvas part of the scene, but in
+    scene coordinates
+    */
+    ScenePoint2D GetRandomPointInScene() const;
+
+    boost::shared_ptr<IFlexiblePointerTracker> TrackerHitTest(const PointerEvent& e);
+
+    boost::shared_ptr<IFlexiblePointerTracker> CreateSuitableTracker(
+      const SDL_Event& event,
+      const PointerEvent& e);
+
+    void TakeScreenshot(
+      const std::string& target,
+      unsigned int canvasWidth,
+      unsigned int canvasHeight);
+
+    /**
+      This adds the command at the top of the undo stack
+    */
+    void Commit(boost::shared_ptr<TrackerCommand> cmd);
+    void Undo();
+    void Redo();
+
+
+    // TODO private
+    void Handle(const DicomVolumeImage::GeometryReadyMessage& message);
+    void Handle(const OracleCommandExceptionMessage& message);
+
+    void SetVolume1(
+      int depth,
+      const boost::shared_ptr<IVolumeSlicer>& volume,
+      ILayerStyleConfigurator* style);
+    
+    void SetVolume2(
+      int depth,
+      const boost::shared_ptr<IVolumeSlicer>& volume,
+      ILayerStyleConfigurator* style);
+
+    void SetStructureSet(
+      int depth, 
+      const boost::shared_ptr<DicomStructureSetLoader>& volume);
+
+
+
+  private:
+    void DisplayFloatingCtrlInfoText(const PointerEvent& e);
+    void DisplayInfoText();
+    void HideInfoText();
+
+  private:
+    CoordinateSystem3D  plane_;
+
+    boost::shared_ptr<VolumeSceneLayerSource>  source1_, source2_, source3_;
+
+    std::auto_ptr<OpenGLCompositor> compositor_;
+    /**
+    WARNING: the measuring tools do store a reference to the scene, and it
+    paramount that the scene gets destroyed AFTER the measurement tools.
+    */
+    boost::shared_ptr<ViewportController> controller_;
+
+    std::map<std::string, std::string> infoTextMap_;
+    boost::shared_ptr<IFlexiblePointerTracker> activeTracker_;
+
+    //static const int LAYER_POSITION = 150;
+
+    int TEXTURE_2x2_1_ZINDEX;
+    int TEXTURE_1x1_ZINDEX;
+    int TEXTURE_2x2_2_ZINDEX;
+    int LINESET_1_ZINDEX;
+    int LINESET_2_ZINDEX;
+    int FLOATING_INFOTEXT_LAYER_ZINDEX;
+    int FIXED_INFOTEXT_LAYER_ZINDEX;
+
+    FusionMprGuiTool currentTool_;
+  };
+
+}
+
+
+ 
\ No newline at end of file
--- a/Samples/Sdl/Loader.cpp	Wed May 29 13:42:34 2019 +0200
+++ b/Samples/Sdl/Loader.cpp	Wed May 29 16:15:04 2019 +0200
@@ -57,7 +57,7 @@
 
 
     virtual void EmitMessage(const IObserver& observer,
-                             const IMessage& message)
+                             const IMessage& message) ORTHANC_OVERRIDE
     {
       try
       {
@@ -302,6 +302,8 @@
 void Run(OrthancStone::NativeApplicationContext& context,
          OrthancStone::ThreadedOracle& oracle)
 {
+  // the oracle has been supplied with the context (as an IEmitter) upon
+  // creation
   boost::shared_ptr<OrthancStone::DicomVolumeImage>  ct(new OrthancStone::DicomVolumeImage);
   boost::shared_ptr<OrthancStone::DicomVolumeImage>  dose(new OrthancStone::DicomVolumeImage);
 
@@ -314,6 +316,11 @@
   {
     OrthancStone::NativeApplicationContext::WriterLock lock(context);
     toto.reset(new Toto(oracle, lock.GetOracleObservable()));
+
+    // the oracle is used to schedule commands
+    // the oracleObservable is used by the loaders to:
+    // - request the broker (lifetime mgmt)
+    // - register the loader callbacks (called indirectly by the oracle)
     ctLoader.reset(new OrthancStone::OrthancSeriesVolumeProgressiveLoader(ct, oracle, lock.GetOracleObservable()));
     doseLoader.reset(new OrthancStone::OrthancMultiframeVolumeLoader(dose, oracle, lock.GetOracleObservable()));
     rtstructLoader.reset(new OrthancStone::DicomStructureSetLoader(oracle, lock.GetOracleObservable()));