changeset 1336:379c00958553 broker

integration mainline->broker
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 31 Mar 2020 15:51:02 +0200
parents 04055b6b9e2c (diff) 35469f75faa2 (current diff)
children c7dbe89c82d7
files Applications/Samples/SingleFrameEditorApplication.h Framework/Radiography/RadiographyDicomLayer.cpp Framework/Radiography/RadiographyLayer.cpp Framework/Radiography/RadiographyLayer.h Framework/Radiography/RadiographyMaskLayer.h Framework/Radiography/RadiographyScene.cpp Framework/Radiography/RadiographyScene.h Framework/Radiography/RadiographyWidget.h
diffstat 290 files changed, 17352 insertions(+), 7616 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/Generic/GuiAdapter.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Applications/Generic/GuiAdapter.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -31,7 +31,7 @@
 #endif
 
 #if ORTHANC_ENABLE_THREADS == 1
-#  include "../../Framework/Messages/LockingEmitter.h"
+#  include "../../Framework/Deprecated/Messages/LockingEmitter.h"
 #endif
 
 #include <Core/Compatibility.h>
@@ -358,6 +358,8 @@
       func);
   }
 
+#if 0
+  // useless under Wasm where canvas resize is handled automatically
   void GuiAdapter::SetResizeCallback(
     std::string canvasId, void* userData, bool capture, OnWindowResizeFunc func)
   {
@@ -368,6 +370,7 @@
       capture,
       func);
   }
+#endif
 
   void GuiAdapter::RequestAnimationFrame(
     OnAnimationFrameFunc func, void* userData)
@@ -393,10 +396,11 @@
     emscripten_set_keyup_callback(canvasId.c_str(), userData, static_cast<EM_BOOL>(capture), func);
   }
 
-  void GuiAdapter::SetResizeCallback(std::string canvasId, void* userData, bool capture, OnWindowResizeFunc func)
-  {
-    emscripten_set_resize_callback(canvasId.c_str(), userData, static_cast<EM_BOOL>(capture), func);
-  }
+  // handled from within WebAssemblyViewport
+  //void GuiAdapter::SetResizeCallback(std::string canvasId, void* userData, bool capture, OnWindowResizeFunc func)
+  //{
+  //  emscripten_set_resize_callback(canvasId.c_str(), userData, static_cast<EM_BOOL>(capture), func);
+  //}
 
   void GuiAdapter::RequestAnimationFrame(OnAnimationFrameFunc func, void* userData)
   {
@@ -516,13 +520,11 @@
       dest.altKey = false;
   }
 
-
-
   // SDL ONLY
-  void GuiAdapter::SetResizeCallback(
-    std::string canvasId, void* userData, bool capture, OnWindowResizeFunc func)
+  void GuiAdapter::SetSdlResizeCallback(
+    std::string canvasId, void* userData, bool capture, OnSdlWindowResizeFunc func)
   {
-    resizeHandlers_.push_back(EventHandlerData<OnWindowResizeFunc>(canvasId, func, userData));
+    resizeHandlers_.push_back(EventHandlerData<OnSdlWindowResizeFunc>(canvasId, func, userData));
   }
 
   // SDL ONLY
@@ -575,6 +577,13 @@
   }
 
   // SDL ONLY
+  void GuiAdapter::SetGenericSdlEventCallback(
+    std::string canvasId, void* userData, bool capture, OnSdlEventCallback func)
+  {
+    sdlEventHandlers_.push_back(EventHandlerData<OnSdlEventCallback>(canvasId, func, userData));
+  }
+
+  // SDL ONLY
   void GuiAdapter::OnAnimationFrame()
   {
     std::vector<size_t> disabledAnimationHandlers;
@@ -596,19 +605,91 @@
   }
 
   // SDL ONLY
-  void GuiAdapter::OnResize()
+  void GuiAdapter::OnResize(unsigned int width, unsigned int height)
   {
     for (size_t i = 0; i < resizeHandlers_.size(); i++)
     {
       (*(resizeHandlers_[i].func))(
-        resizeHandlers_[i].canvasName, 0, resizeHandlers_[i].userData);
+        resizeHandlers_[i].canvasName, NULL, width, height, resizeHandlers_[i].userData);
+    }
+  }
+
+
+
+  void GuiAdapter::OnSdlGenericEvent(const SDL_Event& sdlEvent)
+  {
+    // Events related to a window are only sent to the related canvas
+    // User events are sent to everyone (we can't filter them here)
+    
+    /*
+    SDL_WindowEvent    SDL_WINDOWEVENT
+    SDL_KeyboardEvent     SDL_KEYDOWN
+                          SDL_KEYUP
+    SDL_TextEditingEvent  SDL_TEXTEDITING
+    SDL_TextInputEvent    SDL_TEXTINPUT
+    SDL_MouseMotionEvent  SDL_MOUSEMOTION
+    SDL_MouseButtonEvent  SDL_MOUSEBUTTONDOWN 
+                          SDL_MOUSEBUTTONUP
+    SDL_MouseWheelEvent   SDL_MOUSEWHEEL
+    SDL_UserEvent         SDL_USEREVENT through ::SDL_LASTEVENT-1
+    */
+
+    // if this string is left empty, it means the message will be sent to
+    // all widgets.
+    // otherwise, it contains the originating message window title
+
+    std::string windowTitle;
+    uint32_t windowId = 0;
+
+    if (sdlEvent.type == SDL_WINDOWEVENT)
+      windowId = sdlEvent.window.windowID;
+    else if (sdlEvent.type == SDL_KEYDOWN || sdlEvent.type == SDL_KEYUP)
+      windowId = sdlEvent.key.windowID;
+    else if (sdlEvent.type == SDL_TEXTEDITING)
+      windowId = sdlEvent.edit.windowID;
+    else if (sdlEvent.type == SDL_TEXTINPUT)
+      windowId = sdlEvent.text.windowID;
+    else if (sdlEvent.type == SDL_MOUSEMOTION)
+      windowId = sdlEvent.motion.windowID;
+    else if (sdlEvent.type == SDL_MOUSEBUTTONDOWN || sdlEvent.type == SDL_MOUSEBUTTONUP)
+      windowId = sdlEvent.button.windowID;
+    else if (sdlEvent.type == SDL_MOUSEWHEEL)
+      windowId = sdlEvent.wheel.windowID;
+    else if (sdlEvent.type >= SDL_USEREVENT && sdlEvent.type <= (SDL_LASTEVENT-1))
+      windowId = sdlEvent.user.windowID;
+
+    if (windowId != 0)
+    {
+      SDL_Window* sdlWindow = SDL_GetWindowFromID(windowId);
+      ORTHANC_ASSERT(sdlWindow != NULL, "Window ID \"" << windowId << "\" is not a valid SDL window ID!");
+      const char* windowTitleSz = SDL_GetWindowTitle(sdlWindow);
+      ORTHANC_ASSERT(windowTitleSz != NULL, "Window ID \"" << windowId << "\" has a NULL window title!");
+      windowTitle = windowTitleSz;
+      ORTHANC_ASSERT(windowTitle != "", "Window ID \"" << windowId << "\" has an empty window title!");
+    }
+
+    for (size_t i = 0; i < sdlEventHandlers_.size(); i++)
+    {
+      // normally, the handlers return a bool indicating whether they
+      // have handled the event or not, but we don't really care about this
+      std::string& canvasName = sdlEventHandlers_[i].canvasName;
+      
+      bool sendEvent = true;
+
+      if (windowTitle != "" && (canvasName != windowTitle))
+        sendEvent = false;
+
+      if (sendEvent)
+      {
+        OnSdlEventCallback func = sdlEventHandlers_[i].func;
+        (*func)(canvasName, sdlEvent, sdlEventHandlers_[i].userData);
+      }
     }
   }
 
   // SDL ONLY
   void GuiAdapter::OnMouseWheelEvent(uint32_t windowID, const GuiAdapterWheelEvent& event)
   {
-
     // the SDL window name IS the canvas name ("canvas" is used because this lib
     // is designed for Wasm
     SDL_Window* sdlWindow = SDL_GetWindowFromID(windowID);
@@ -726,17 +807,6 @@
         ORTHANC_ASSERT(false, "Wrong event.type: " << event.type << " in GuiAdapter::OnMouseEvent(...)");
         break;
       }
-
-      ////boost::shared_ptr<IGuiAdapterWidget> GetWidgetFromWindowId();
-      //boost::shared_ptr<IGuiAdapterWidget> foundWidget;
-      //VisitWidgets([foundWidget, windowID](auto widget)
-      //  {
-      //    if (widget->GetSdlWindowID() == windowID)
-      //      foundWidget = widget;
-      //  });
-      //ORTHANC_ASSERT(foundWidget, "WindowID " << windowID << " was not found in the registered widgets!");
-      //if(foundWidget)
-      //  foundWidget->
     }
   }
 
@@ -771,6 +841,31 @@
   }
 # endif
 
+#if 0
+  // TODO: remove this when generic sdl event handlers are implemented in 
+  // the VolumeSlicerWidget
+  // SDL ONLY
+  bool GuiAdapter::IsSdlViewPortRefreshEvent(const SDL_Event& event) const
+  {
+    SDL_Window* sdlWindow = SDL_GetWindowFromID(event.window.windowID);
+
+    ORTHANC_ASSERT(sdlWindow != NULL, "Window ID \"" << event.window.windowID << "\" is not a valid SDL window ID!");
+
+    const char* windowTitleSz = SDL_GetWindowTitle(sdlWindow);
+
+    // now we need to find the VolumeSlicerWidget from from the canvas name!
+    // (and retrieve the SdlViewport)
+    boost::shared_ptr<IGuiAdapterWidget> foundWidget;
+    VisitWidgets([&foundWidget, windowTitleSz](auto widget)
+    {
+      if (widget->GetCanvasIdentifier() == std::string(windowTitleSz))
+        foundWidget = widget;
+    });
+    ORTHANC_ASSERT(foundWidget, "The window named: \"" << windowTitleSz << "\" was not found in the registered widgets!");
+    return foundWidget->GetSdlViewport().IsRefreshEvent(event);
+  }
+#endif
+
   // SDL ONLY
   void GuiAdapter::Run(GuiAdapterRunFunc func, void* cookie)
   {
@@ -789,141 +884,171 @@
     while (!stop)
     {
       {
-        LockingEmitter::WriterLock lock(lockingEmitter_);
+        // TODO: lock all viewports here! (use a scoped object)
         if(func != NULL)
           (*func)(cookie);
         OnAnimationFrame(); // in SDL we must call it
       }
 
-      SDL_Event event;
-
-      while (!stop && SDL_PollEvent(&event))
+      while (!stop)
       {
-        LockingEmitter::WriterLock lock(lockingEmitter_);
-
-        if (event.type == SDL_QUIT)
-        {
-          // TODO: call exit callbacks here
-          stop = true;
-          break;
-        }
-        else if ((event.type == SDL_MOUSEMOTION) ||
-          (event.type == SDL_MOUSEBUTTONDOWN) ||
-          (event.type == SDL_MOUSEBUTTONUP))
-        {
-          int scancodeCount = 0;
-          const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount);
-          bool ctrlPressed(false);
-          bool shiftPressed(false);
-          bool altPressed(false);
-
-          if (SDL_SCANCODE_LCTRL < scancodeCount && keyboardState[SDL_SCANCODE_LCTRL])
-            ctrlPressed = true;
-          if (SDL_SCANCODE_RCTRL < scancodeCount && keyboardState[SDL_SCANCODE_RCTRL])
-            ctrlPressed = true;
-          if (SDL_SCANCODE_LSHIFT < scancodeCount && keyboardState[SDL_SCANCODE_LSHIFT])
-            shiftPressed = true;
-          if (SDL_SCANCODE_RSHIFT < scancodeCount && keyboardState[SDL_SCANCODE_RSHIFT])
-            shiftPressed = true;
-          if (SDL_SCANCODE_LALT < scancodeCount && keyboardState[SDL_SCANCODE_LALT])
-            altPressed = true;
-
-          GuiAdapterMouseEvent dest;
-          ConvertFromPlatform(dest, ctrlPressed, shiftPressed, altPressed, event);
-          OnMouseEvent(event.window.windowID, dest);
-#if 0
-          // for reference, how to create trackers
-          if (tracker)
-          {
-            PointerEvent e;
-            e.AddPosition(compositor.GetPixelCenterCoordinates(
-              event.button.x, event.button.y));
-            tracker->PointerMove(e);
-          }
-#endif
-        }
-        else if (event.type == SDL_MOUSEWHEEL)
-        {
-
-          int scancodeCount = 0;
-          const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount);
-          bool ctrlPressed(false);
-          bool shiftPressed(false);
-          bool altPressed(false);
+        std::vector<SDL_Event> sdlEvents;
+        std::map<Uint32,SDL_Event> userEventsMap;
+        
+        SDL_Event sdlEvent;
 
-          if (SDL_SCANCODE_LCTRL < scancodeCount && keyboardState[SDL_SCANCODE_LCTRL])
-            ctrlPressed = true;
-          if (SDL_SCANCODE_RCTRL < scancodeCount && keyboardState[SDL_SCANCODE_RCTRL])
-            ctrlPressed = true;
-          if (SDL_SCANCODE_LSHIFT < scancodeCount && keyboardState[SDL_SCANCODE_LSHIFT])
-            shiftPressed = true;
-          if (SDL_SCANCODE_RSHIFT < scancodeCount && keyboardState[SDL_SCANCODE_RSHIFT])
-            shiftPressed = true;
-          if (SDL_SCANCODE_LALT < scancodeCount && keyboardState[SDL_SCANCODE_LALT])
-            altPressed = true;
-
-          GuiAdapterWheelEvent dest;
-          ConvertFromPlatform(dest, ctrlPressed, shiftPressed, altPressed, event);
-          OnMouseWheelEvent(event.window.windowID, dest);
-
-          //KeyboardModifiers modifiers = GetKeyboardModifiers(keyboardState, scancodeCount);
-
-          //int x, y;
-          //SDL_GetMouseState(&x, &y);
-
-          //if (event.wheel.y > 0)
-          //{
-          //  locker.GetCentralViewport().MouseWheel(MouseWheelDirection_Up, x, y, modifiers);
-          //}
-          //else if (event.wheel.y < 0)
-          //{
-          //  locker.GetCentralViewport().MouseWheel(MouseWheelDirection_Down, x, y, modifiers);
-          //}
-        }
-        else if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
+        // FIRST: collect all pending events
+        while (SDL_PollEvent(&sdlEvent) != 0)
         {
-#if 0
-          tracker.reset();
-#endif
-          OnResize();
+          if ( (sdlEvent.type >= SDL_USEREVENT) && 
+               (sdlEvent.type <= SDL_USEREVENT) )
+          {
+            // we don't want to have multiple events with the same event.type
+            userEventsMap[sdlEvent.type] = sdlEvent;
+          }
+          else
+          {
+            sdlEvents.push_back(sdlEvent);
+          }
         }
-        else if (event.type == SDL_KEYDOWN && event.key.repeat == 0 /* Ignore key bounce */)
-        {
-          switch (event.key.keysym.sym)
-          {
-          case SDLK_f:
-            // window.GetWindow().ToggleMaximize(); //TODO: move to particular handler
-            break;
 
-          // This commented out code was used to debug the context
-          // loss/restoring code (2019-08-10)
-          // case SDLK_k:
-          //   {
-          //     SDL_Window* window = SDL_GetWindowFromID(event.window.windowID);
-          //     std::string windowTitle(SDL_GetWindowTitle(window));
-          //     Debug_SetContextToBeKilled(windowTitle);
-          //   }
-          //   break;
-          // case SDLK_l:
-          //   {
-          //     SDL_Window* window = SDL_GetWindowFromID(event.window.windowID);
-          //     std::string windowTitle(SDL_GetWindowTitle(window));
-          //     Debug_SetContextToBeRestored(windowTitle);
-          //   }
-          //   break;
+        // SECOND: collect all user events
+        for (std::map<Uint32,SDL_Event>::const_iterator it = userEventsMap.begin(); it != userEventsMap.end(); ++it)
+          sdlEvents.push_back(it->second);
+                
+        // now process the events
+        for (std::vector<SDL_Event>::const_iterator it = sdlEvents.begin(); it != sdlEvents.end(); ++it)
+        {
+          const SDL_Event& sdlEvent = *it;
+          // TODO: lock all viewports here! (use a scoped object)
 
-          case SDLK_q:
+          if (sdlEvent.type == SDL_QUIT)
+          {
+            // TODO: call exit callbacks here
             stop = true;
             break;
+          }
+          else if ((sdlEvent.type == SDL_MOUSEMOTION) ||
+            (sdlEvent.type == SDL_MOUSEBUTTONDOWN) ||
+                   (sdlEvent.type == SDL_MOUSEBUTTONUP))
+          {
+            int scancodeCount = 0;
+            const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount);
+            bool ctrlPressed(false);
+            bool shiftPressed(false);
+            bool altPressed(false);
 
-          default:
-            GuiAdapterKeyboardEvent dest;
-            ConvertFromPlatform(dest, event);
-            OnKeyboardEvent(event.window.windowID, dest);
-            break;
+            if (SDL_SCANCODE_LCTRL < scancodeCount && keyboardState[SDL_SCANCODE_LCTRL])
+              ctrlPressed = true;
+            if (SDL_SCANCODE_RCTRL < scancodeCount && keyboardState[SDL_SCANCODE_RCTRL])
+              ctrlPressed = true;
+            if (SDL_SCANCODE_LSHIFT < scancodeCount && keyboardState[SDL_SCANCODE_LSHIFT])
+              shiftPressed = true;
+            if (SDL_SCANCODE_RSHIFT < scancodeCount && keyboardState[SDL_SCANCODE_RSHIFT])
+              shiftPressed = true;
+            if (SDL_SCANCODE_LALT < scancodeCount && keyboardState[SDL_SCANCODE_LALT])
+              altPressed = true;
+
+            GuiAdapterMouseEvent dest;
+            ConvertFromPlatform(dest, ctrlPressed, shiftPressed, altPressed, sdlEvent);
+            OnMouseEvent(sdlEvent.window.windowID, dest);
+  #if 0
+            // for reference, how to create trackers
+            if (tracker)
+            {
+              PointerEvent e;
+              e.AddPosition(compositor.GetPixelCenterCoordinates(
+                sdlEvent.button.x, sdlEvent.button.y));
+              tracker->PointerMove(e);
+            }
+  #endif
           }
+          else if (sdlEvent.type == SDL_MOUSEWHEEL)
+          {
+
+            int scancodeCount = 0;
+            const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount);
+            bool ctrlPressed(false);
+            bool shiftPressed(false);
+            bool altPressed(false);
+
+            if (SDL_SCANCODE_LCTRL < scancodeCount && keyboardState[SDL_SCANCODE_LCTRL])
+              ctrlPressed = true;
+            if (SDL_SCANCODE_RCTRL < scancodeCount && keyboardState[SDL_SCANCODE_RCTRL])
+              ctrlPressed = true;
+            if (SDL_SCANCODE_LSHIFT < scancodeCount && keyboardState[SDL_SCANCODE_LSHIFT])
+              shiftPressed = true;
+            if (SDL_SCANCODE_RSHIFT < scancodeCount && keyboardState[SDL_SCANCODE_RSHIFT])
+              shiftPressed = true;
+            if (SDL_SCANCODE_LALT < scancodeCount && keyboardState[SDL_SCANCODE_LALT])
+              altPressed = true;
+
+            GuiAdapterWheelEvent dest;
+            ConvertFromPlatform(dest, ctrlPressed, shiftPressed, altPressed, sdlEvent);
+            OnMouseWheelEvent(sdlEvent.window.windowID, dest);
+
+            //KeyboardModifiers modifiers = GetKeyboardModifiers(keyboardState, scancodeCount);
+
+            //int x, y;
+            //SDL_GetMouseState(&x, &y);
+
+            //if (sdlEvent.wheel.y > 0)
+            //{
+            //  locker.GetCentralViewport().MouseWheel(MouseWheelDirection_Up, x, y, modifiers);
+            //}
+            //else if (sdlEvent.wheel.y < 0)
+            //{
+            //  locker.GetCentralViewport().MouseWheel(MouseWheelDirection_Down, x, y, modifiers);
+            //}
+          }
+          else if (sdlEvent.type == SDL_WINDOWEVENT &&
+            (sdlEvent.window.event == SDL_WINDOWEVENT_RESIZED ||
+             sdlEvent.window.event == SDL_WINDOWEVENT_SIZE_CHANGED))
+          {
+  #if 0
+            tracker.reset();
+  #endif
+            OnResize(sdlEvent.window.data1, sdlEvent.window.data2);
+          }
+          else if (sdlEvent.type == SDL_KEYDOWN && sdlEvent.key.repeat == 0 /* Ignore key bounce */)
+          {
+            switch (sdlEvent.key.keysym.sym)
+            {
+            case SDLK_f:
+              // window.GetWindow().ToggleMaximize(); //TODO: move to particular handler
+              break;
+
+            // This commented out code was used to debug the context
+            // loss/restoring code (2019-08-10)
+            // case SDLK_k:
+            //   {
+            //     SDL_Window* window = SDL_GetWindowFromID(sdlEvent.window.windowID);
+            //     std::string windowTitle(SDL_GetWindowTitle(window));
+            //     Debug_SetContextToBeKilled(windowTitle);
+            //   }
+            //   break;
+            // case SDLK_l:
+            //   {
+            //     SDL_Window* window = SDL_GetWindowFromID(sdlEvent.window.windowID);
+            //     std::string windowTitle(SDL_GetWindowTitle(window));
+            //     Debug_SetContextToBeRestored(windowTitle);
+            //   }
+            //   break;
+
+            case SDLK_q:
+              stop = true;
+              break;
+
+            default:
+              GuiAdapterKeyboardEvent dest;
+              ConvertFromPlatform(dest, sdlEvent);
+              OnKeyboardEvent(sdlEvent.window.windowID, dest);
+              break;
+            }
+          }
+        
+          OnSdlGenericEvent(sdlEvent);
         }
-        //        HandleApplicationEvent(controller, compositor, event, tracker);
       }
 
       SDL_Delay(1);
--- a/Applications/Generic/GuiAdapter.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Applications/Generic/GuiAdapter.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <vector>
@@ -53,6 +53,14 @@
 
 namespace OrthancStone
 {
+#if ORTHANC_ENABLE_SDL == 1
+  class SdlViewport;
+#endif
+
+#if 0
+
+  // events are now handled directly by the VolumeSlicerWidget... The 
+  // GuiAdapter doesn't know about widgets anymore
 
   /**
   This interface is used to store the widgets that are controlled by the 
@@ -64,8 +72,17 @@
   {
   public:
     virtual ~IGuiAdapterWidget() {}
+    
+#if #if ORTHANC_ENABLE_SDL == 1
+    /**
+    Returns the SdlViewport that this widget contains. If the underlying 
+    viewport type is *not* SDL, then an error is returned.
+    */
+    virtual SdlViewport& GetSdlViewport() = 0;
+#endif
+  };
 
-  };
+#endif
 
   enum GuiAdapterMouseButtonType
   {
@@ -95,16 +112,24 @@
   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);
-  typedef bool (*OnKeyDownFunc)   (std::string canvasId, const GuiAdapterKeyboardEvent*   keyEvent,   void* userData);
-  typedef bool (*OnKeyUpFunc)     (std::string canvasId, const GuiAdapterKeyboardEvent*   keyEvent,   void* userData);
+  typedef bool (*OnMouseEventFunc)    (std::string canvasId, const GuiAdapterMouseEvent* mouseEvent, void* userData);
+  typedef bool (*OnMouseWheelFunc)    (std::string canvasId, const GuiAdapterWheelEvent* wheelEvent, void* userData);
+  typedef bool (*OnKeyDownFunc)       (std::string canvasId, const GuiAdapterKeyboardEvent*   keyEvent,   void* userData);
+  typedef bool (*OnKeyUpFunc)         (std::string canvasId, const GuiAdapterKeyboardEvent*   keyEvent,   void* userData);
+  typedef bool (*OnAnimationFrameFunc)(double time, void* userData);
+  
+#if ORTHANC_ENABLE_SDL == 1
+  typedef bool (*OnSdlEventCallback)  (std::string canvasId, const SDL_Event& sdlEvent, void* userData);
 
-  typedef bool (*OnAnimationFrameFunc)(double time, void* userData);
-  typedef bool (*OnWindowResizeFunc)(std::string canvasId, const GuiAdapterUiEvent* uiEvent, void* userData);
+  typedef bool (*OnSdlWindowResizeFunc)(std::string canvasId, 
+                                        const GuiAdapterUiEvent* uiEvent, 
+                                        unsigned int width, 
+                                        unsigned int height, 
+                                        void* userData);
+
+
+#endif
 
 #else
 
@@ -224,11 +249,7 @@
   class GuiAdapter
   {
   public:
-#if ORTHANC_ENABLE_THREADS == 1
-    GuiAdapter(LockingEmitter& lockingEmitter) : lockingEmitter_(lockingEmitter)
-#else
     GuiAdapter()
-#endif
     {
       static int instanceCount = 0;
       ORTHANC_ASSERT(instanceCount == 0);
@@ -252,27 +273,32 @@
 
     */
 
-    void SetMouseDownCallback     (std::string canvasId, void* userData, bool capture, OnMouseEventFunc   func);
-    void SetMouseDblClickCallback (std::string canvasId, void* userData, bool capture, OnMouseEventFunc   func);
-    void SetMouseMoveCallback     (std::string canvasId, void* userData, bool capture, OnMouseEventFunc   func);
-    void SetMouseUpCallback       (std::string canvasId, void* userData, bool capture, OnMouseEventFunc   func);
-    void SetWheelCallback         (std::string canvasId, void* userData, bool capture, OnMouseWheelFunc   func);
-    void SetKeyDownCallback       (std::string canvasId, void* userData, bool capture, OnKeyDownFunc      func);
-    void SetKeyUpCallback         (std::string canvasId, void* userData, bool capture, OnKeyUpFunc        func);
-    
-    // if you pass "#window", under SDL, then any Window resize will trigger the callback
-    void SetResizeCallback (std::string canvasId, void* userData, bool capture, OnWindowResizeFunc func);
+    void SetMouseDownCallback       (std::string canvasId, void* userData, bool capture, OnMouseEventFunc   func);
+    void SetMouseDblClickCallback   (std::string canvasId, void* userData, bool capture, OnMouseEventFunc   func);
+    void SetMouseMoveCallback       (std::string canvasId, void* userData, bool capture, OnMouseEventFunc   func);
+    void SetMouseUpCallback         (std::string canvasId, void* userData, bool capture, OnMouseEventFunc   func);
+    void SetWheelCallback           (std::string canvasId, void* userData, bool capture, OnMouseWheelFunc   func);
+    void SetKeyDownCallback         (std::string canvasId, void* userData, bool capture, OnKeyDownFunc      func);
+    void SetKeyUpCallback           (std::string canvasId, void* userData, bool capture, OnKeyUpFunc        func);
+
+#if ORTHANC_ENABLE_SDL == 1
+
+    void SetGenericSdlEventCallback (std::string canvasId, void* userData, bool capture, OnSdlEventCallback func);
+
+    typedef bool (*OnSdlEventCallback)  (std::string canvasId, const SDL_Event& sdlEvent, void* userData);
+
+    // if you pass "#window", then any Window resize will trigger the callback
+    void SetSdlResizeCallback(std::string canvasId, 
+                              void* userData, 
+                              bool capture, 
+                              OnSdlWindowResizeFunc func);
+#endif
 
     void RequestAnimationFrame(OnAnimationFrameFunc func, void* userData);
 
     // TODO: implement and call to remove canvases [in SDL, although code should be generic]
     void SetOnExitCallback();
 
-    // void 
-    // OnWindowResize
-    // oracle
-    // broker
-
     /**
     Under SDL, this function does NOT return until all windows have been closed.
     Under wasm, it returns without doing anything, since the event loop is managed
@@ -280,26 +306,14 @@
     */
     void Run(GuiAdapterRunFunc func = NULL, void* cookie = NULL);
 
-#if ORTHANC_ENABLE_WASM != 1
-    /**
-    This method must be called in order for callback handler to be allowed to 
-    be registered.
-
-    We'll retrieve its name and use it as the canvas name in all subsequent 
-    calls
-    */
-    //void RegisterSdlWindow(SDL_Window* window);
-    //void UnregisterSdlWindow(SDL_Window* window);
-#endif
-
   private:
 
-#if ORTHANC_ENABLE_THREADS == 1
+#if ORTHANC_ENABLE_SDL == 1
     /**
-    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.
+    Gives observers a chance to react based on generic event handlers. This 
+    is used, for instance, when the viewport lock interface is invalidated.
     */
-    LockingEmitter& lockingEmitter_;
+    void OnSdlGenericEvent(const SDL_Event& sdlEvent);
 #endif
 
     /**
@@ -311,7 +325,7 @@
     std::vector<std::pair<OnAnimationFrameFunc, void*> >  
       animationFrameHandlers_;
     
-    void OnResize();
+    void OnResize(unsigned int width, unsigned int height);
 
 #if ORTHANC_ENABLE_SDL == 1
     template<typename Func>
@@ -328,14 +342,15 @@
       Func        func;
       void*       userData;
     };
-    std::vector<EventHandlerData<OnWindowResizeFunc> > resizeHandlers_;
-    std::vector<EventHandlerData<OnMouseEventFunc  > > mouseDownHandlers_;
-    std::vector<EventHandlerData<OnMouseEventFunc  > > mouseDblCickHandlers_;
-    std::vector<EventHandlerData<OnMouseEventFunc  > > mouseMoveHandlers_;
-    std::vector<EventHandlerData<OnMouseEventFunc  > > mouseUpHandlers_;
-    std::vector<EventHandlerData<OnMouseWheelFunc  > > mouseWheelHandlers_;
-    std::vector<EventHandlerData<OnKeyDownFunc > > keyDownHandlers_;
-    std::vector<EventHandlerData<OnKeyUpFunc > > keyUpHandlers_;
+    std::vector<EventHandlerData<OnSdlWindowResizeFunc> > resizeHandlers_;
+    std::vector<EventHandlerData<OnMouseEventFunc  > >    mouseDownHandlers_;
+    std::vector<EventHandlerData<OnMouseEventFunc  > >    mouseDblCickHandlers_;
+    std::vector<EventHandlerData<OnMouseEventFunc  > >    mouseMoveHandlers_;
+    std::vector<EventHandlerData<OnMouseEventFunc  > >    mouseUpHandlers_;
+    std::vector<EventHandlerData<OnMouseWheelFunc  > >    mouseWheelHandlers_;
+    std::vector<EventHandlerData<OnKeyDownFunc > >        keyDownHandlers_;
+    std::vector<EventHandlerData<OnKeyUpFunc > >          keyUpHandlers_;
+    std::vector<EventHandlerData<OnSdlEventCallback > >   sdlEventHandlers_;
 
     /**
     This executes all the registered headers if needed (in wasm, the browser
@@ -350,8 +365,6 @@
     */
     void OnMouseWheelEvent(uint32_t windowID, const GuiAdapterWheelEvent& event);
 
-    boost::shared_ptr<IGuiAdapterWidget> GetWidgetFromWindowId();
-
 #endif
 
     /**
--- a/Applications/Generic/NativeStoneApplicationContext.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Applications/Generic/NativeStoneApplicationContext.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -24,10 +24,10 @@
 
 namespace OrthancStone
 {
-  Deprecated::IWidget& NativeStoneApplicationContext::GlobalMutexLocker::SetCentralWidget(Deprecated::IWidget* widget)
+  void NativeStoneApplicationContext::GlobalMutexLocker::SetCentralWidget(
+    boost::shared_ptr<Deprecated::IWidget> 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
   {
--- a/Applications/Generic/NativeStoneApplicationContext.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Applications/Generic/NativeStoneApplicationContext.h	Tue Mar 31 15:51:02 2020 +0200
@@ -56,7 +56,7 @@
       {
       }
 
-      Deprecated::IWidget& SetCentralWidget(Deprecated::IWidget* widget);   // Takes ownership
+      void SetCentralWidget(boost::shared_ptr<Deprecated::IWidget> widget);
 
       Deprecated::IViewport& GetCentralViewport() 
       {
@@ -67,14 +67,9 @@
       {
         that_.updateDelayInMs_ = delayInMs;
       }
-
-      MessageBroker& GetMessageBroker()
-      {
-        return that_.GetMessageBroker();
-      }
     };
 
-    NativeStoneApplicationContext(MessageBroker& broker);
+    NativeStoneApplicationContext();
 
     void Start();
 
--- a/Applications/Generic/NativeStoneApplicationRunner.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Applications/Generic/NativeStoneApplicationRunner.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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<Deprecated::OracleWebService> 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)
     {
--- a/Applications/Generic/NativeStoneApplicationRunner.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Applications/Generic/NativeStoneApplicationRunner.h	Tue Mar 31 15:51:02 2020 +0200
@@ -34,14 +34,11 @@
   class NativeStoneApplicationRunner
   {
   protected:
-    MessageBroker&      broker_;
-    IStoneApplication&  application_;
+    boost::shared_ptr<IStoneApplication>  application_;
+    
   public:
-
-    NativeStoneApplicationRunner(MessageBroker& broker,
-                                 IStoneApplication& application)
-      : broker_(broker),
-        application_(application)
+    NativeStoneApplicationRunner(boost::shared_ptr<IStoneApplication> application)
+      : application_(application)
     {
     }
     int Execute(int argc,
--- a/Applications/IStoneApplication.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Applications/IStoneApplication.h	Tue Mar 31 15:51:02 2020 +0200
@@ -64,7 +64,11 @@
 #endif
 
     virtual std::string GetTitle() const = 0;
-    virtual Deprecated::IWidget* GetCentralWidget() = 0;
+    
+    virtual void SetCentralWidget(boost::shared_ptr<Deprecated::IWidget> widget) = 0;
+    
+    virtual boost::shared_ptr<Deprecated::IWidget> GetCentralWidget() = 0;
+    
     virtual void Finalize() = 0;
   };
 }
--- a/Applications/Samples/SampleApplicationBase.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Applications/Samples/SampleApplicationBase.h	Tue Mar 31 15:51:02 2020 +0200
@@ -40,9 +40,8 @@
   {
     class SampleApplicationBase : public IStoneApplication
     {
-    protected:
-      // ownership is transferred to the application context
-      Deprecated::WorldSceneWidget*  mainWidget_;
+    private:
+      boost::shared_ptr<Deprecated::IWidget>  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<Deprecated::IWidget> widget) ORTHANC_OVERRIDE
+      {
+        mainWidget_ = widget;
+      }
+
+      virtual boost::shared_ptr<Deprecated::IWidget> 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
--- a/Applications/Samples/SampleMainNative.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Applications/Samples/SampleMainNative.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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<SampleApplication> 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
 }
--- a/Applications/Samples/SimpleViewerApplicationSingleFile.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Applications/Samples/SimpleViewerApplicationSingleFile.h	Tue Mar 31 15:51:02 2020 +0200
@@ -44,7 +44,7 @@
   {
     class SimpleViewerApplication :
       public SampleSingleCanvasWithButtonsApplicationBase,
-      public IObserver
+      public ObserverBase<SimpleViewerApplication>
     {
     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::unique_ptr<ThumbnailInteractor>   thumbnailInteractor_;
       Deprecated::LayoutWidget*                        mainLayout_;
       Deprecated::LayoutWidget*                        thumbnailsLayout_;
-      std::vector<Deprecated::SliceViewerWidget*>      thumbnails_;
+      std::vector<boost::shared_ptr<Deprecated::SliceViewerWidget> >      thumbnails_;
 
       std::map<std::string, std::vector<std::string> > instancesIdsPerSeriesId_;
       std::map<std::string, Json::Value> 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<Deprecated::LayoutWidget> 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<Deprecated::SliceViewerWidget> 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<SimpleViewerApplication, Deprecated::OrthancApiClient::JsonResponseReadyMessage>
-            (*this, &SimpleViewerApplication::OnStudyListReceived));
+            new Deprecated::DeprecatedCallable<SimpleViewerApplication, Deprecated::OrthancApiClient::JsonResponseReadyMessage>
+            (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<SimpleViewerApplication, Deprecated::OrthancApiClient::JsonResponseReadyMessage>
-              (*this, &SimpleViewerApplication::OnSeriesReceived));
+              new Deprecated::DeprecatedCallable<SimpleViewerApplication, Deprecated::OrthancApiClient::JsonResponseReadyMessage>
+              (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<Deprecated::SliceViewerWidget*>(mainWidget_);
+          Deprecated::SliceViewerWidget& widget = dynamic_cast<Deprecated::SliceViewerWidget&>(*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<Deprecated::SliceViewerWidget> thumbnailWidget(new Deprecated::SliceViewerWidget("thumbnail-series-" + seriesId));
         thumbnails_.push_back(thumbnailWidget);
         thumbnailsLayout_->AddWidget(thumbnailWidget);
-        thumbnailWidget->RegisterObserverCallback(new Callable<SimpleViewerApplication, Deprecated::SliceViewerWidget::GeometryChangedMessage>(*this, &SimpleViewerApplication::OnWidgetGeometryChanged));
+        Register<Deprecated::SliceViewerWidget::GeometryChangedMessage>(*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<SimpleViewerApplication, Deprecated::OrthancApiClient::JsonResponseReadyMessage>
-          (*this, &SimpleViewerApplication::OnStudyReceived));
+        context_->GetOrthancApiClient()->GetJsonAsync(
+          "/studies/" + studyId, new Deprecated::DeprecatedCallable<SimpleViewerApplication, Deprecated::OrthancApiClient::JsonResponseReadyMessage>
+          (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<Deprecated::SliceViewerWidget*>(mainWidget_);
+        Deprecated::SliceViewerWidget& widget = dynamic_cast<Deprecated::SliceViewerWidget&>(*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
 
--- a/Applications/Samples/SingleFrameApplication.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Applications/Samples/SingleFrameApplication.h	Tue Mar 31 15:51:02 2020 +0200
@@ -38,7 +38,7 @@
   {
     class SingleFrameApplication :
       public SampleSingleCanvasApplicationBase,
-      public IObserver
+      public ObserverBase<SingleFrameApplication>
     {
     private:
       class Interactor : public Deprecated::IWorldSceneInteractor
@@ -127,7 +127,7 @@
 
       void OffsetSlice(int offset)
       {
-        if (source_ != NULL)
+        if (source_)
         {
           int slice = static_cast<int>(slice_) + offset;
 
@@ -149,21 +149,15 @@
       }
 
 
-      Deprecated::SliceViewerWidget& GetMainWidget()
-      {
-        return *dynamic_cast<Deprecated::SliceViewerWidget*>(mainWidget_);
-      }
-      
-
       void SetSlice(size_t index)
       {
-        if (source_ != NULL &&
+        if (source_ &&
             index < source_->GetSlicesCount())
         {
           slice_ = static_cast<unsigned int>(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<double>();
@@ -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<Deprecated::SliceViewerWidget>  widget_;
       std::unique_ptr<Interactor>         mainWidgetInteractor_;
-      const Deprecated::DicomSeriesVolumeSlicer*    source_;
+      boost::shared_ptr<Deprecated::DicomSeriesVolumeSlicer> source_;
       unsigned int                      slice_;
 
     public:
-      SingleFrameApplication(MessageBroker& broker) :
-        IObserver(broker),
-        source_(NULL),
+      SingleFrameApplication() :
         slice_(0)
       {
       }
@@ -243,13 +237,16 @@
         std::string instance = parameters["instance"].as<std::string>();
         int frame = parameters["frame"].as<unsigned int>();
 
-        mainWidget_ = new Deprecated::SliceViewerWidget(GetBroker(), "main-widget");
+        widget_.reset(new Deprecated::SliceViewerWidget("main-widget"));
+        SetCentralWidget(widget_);
 
-        std::unique_ptr<Deprecated::DicomSeriesVolumeSlicer> layer(new Deprecated::DicomSeriesVolumeSlicer(GetBroker(), context->GetOrthancApiClient()));
-        source_ = layer.get();
+        boost::shared_ptr<Deprecated::DicomSeriesVolumeSlicer> layer(new Deprecated::DicomSeriesVolumeSlicer);
+        layer->Connect(context->GetOrthancApiClient());
+        source_ = layer;
+
         layer->LoadFrame(instance, frame);
-        layer->RegisterObserverCallback(new Callable<SingleFrameApplication, Deprecated::IVolumeSlicer::GeometryReadyMessage>(*this, &SingleFrameApplication::OnMainWidgetGeometryReady));
-        GetMainWidget().AddLayer(layer.release());
+        Register<Deprecated::IVolumeSlicer::GeometryReadyMessage>(*layer, &SingleFrameApplication::OnMainWidgetGeometryReady);
+        widget_->AddLayer(layer);
 
         Deprecated::RenderStyle s;
 
@@ -258,11 +255,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_);
       }
     };
 
--- a/Applications/Samples/SingleFrameEditorApplication.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Applications/Samples/SingleFrameEditorApplication.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <Core/HttpClient.h>
@@ -55,7 +55,7 @@
   {
     class RadiographyEditorInteractor :
         public Deprecated::IWorldSceneInteractor,
-        public IObserver
+        public ObserverBase<RadiographyEditorInteractor>
     {
     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<RadiographyScene> scene(new RadiographyScene(GetBroker()));
-          RadiographySceneReader reader(*scene, context_->GetOrthancApiClient());
+          boost::shared_ptr<RadiographyScene> 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(),
                                           false /* autoCrop */, widget.GetInterpolation(), EXPORT_USING_PAM);
           }
@@ -426,16 +425,9 @@
     private:
       boost::shared_ptr<RadiographyScene>   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<std::string>();
         //int frame = parameters["frame"].as<unsigned int>();
 
-        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<RadiographyWidget> widget(new RadiographyWidget(scene_, "main-widget"));
+        widget->SetTransmitMouseOver(true);
+        widget->SetInteractor(interactor_);
+        SetCentralWidget(widget);
 
         //scene_->SetWindowing(128, 256);
       }
--- a/Applications/Sdl/SdlCairoSurface.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Applications/Sdl/SdlCairoSurface.h	Tue Mar 31 15:51:02 2020 +0200
@@ -29,6 +29,7 @@
 
 #include <Core/Compatibility.h>
 
+#include <SDL_render.h>
 #include <boost/thread/mutex.hpp>
 
 namespace OrthancStone
--- a/Applications/Sdl/SdlEngine.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Applications/Sdl/SdlEngine.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -99,9 +99,7 @@
 
 
   SdlEngine::SdlEngine(SdlWindow& window,
-                       NativeStoneApplicationContext& context,
-                       MessageBroker& broker) :
-    IObserver(broker),
+                       NativeStoneApplicationContext& context) :
     window_(window),
     context_(context),
     surface_(window),
--- a/Applications/Sdl/SdlEngine.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Applications/Sdl/SdlEngine.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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<SdlEngine>
   {
   private:
     SdlWindow&                window_;
@@ -46,8 +47,7 @@
 
   public:
     SdlEngine(SdlWindow& window,
-              NativeStoneApplicationContext& context,
-              MessageBroker& broker);
+              NativeStoneApplicationContext& context);
   
     void OnViewportChanged(const Deprecated::IViewport::ViewportChangedMessage& message)
     {
--- a/Applications/Sdl/SdlOrthancSurface.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Applications/Sdl/SdlOrthancSurface.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -27,6 +27,8 @@
 #include <Core/OrthancException.h>
 #include <Core/Images/Image.h>
 
+#include <SDL_render.h>
+
 namespace OrthancStone
 {
   SdlOrthancSurface::SdlOrthancSurface(SdlWindow& window) :
--- a/Applications/Sdl/SdlStoneApplicationRunner.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Applications/Sdl/SdlStoneApplicationRunner.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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<SdlEngine> sdl(new SdlEngine(window, context));
 
     {
       NativeStoneApplicationContext::GlobalMutexLocker locker(context);
 
-      locker.GetCentralViewport().RegisterObserverCallback(
-        new Callable<SdlEngine, Deprecated::IViewport::ViewportChangedMessage>
-        (sdl, &SdlEngine::OnViewportChanged));
+      sdl->Register<Deprecated::IViewport::ViewportChangedMessage>
+        (locker.GetCentralViewport(), &SdlEngine::OnViewportChanged);
 
       //context.GetCentralViewport().Register(sdl);  // (*)
     }
 
     context.Start();
-    sdl.Run();
+    sdl->Run();
 
     LOG(WARNING) << "Stopping the application";
 
--- a/Applications/Sdl/SdlStoneApplicationRunner.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Applications/Sdl/SdlStoneApplicationRunner.h	Tue Mar 31 15:51:02 2020 +0200
@@ -39,9 +39,8 @@
     bool          enableOpenGl_;
     
   public:
-    SdlStoneApplicationRunner(MessageBroker& broker,
-                              IStoneApplication& application) :
-      NativeStoneApplicationRunner(broker, application)
+    SdlStoneApplicationRunner(boost::shared_ptr<IStoneApplication> application) :
+      NativeStoneApplicationRunner(application)
     {
     }
 
--- a/Applications/StoneApplicationContext.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Applications/StoneApplicationContext.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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<Deprecated::IWebService> StoneApplicationContext::GetWebService()
   {
     if (webService_ == NULL)
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
     }
     
-    return *webService_;
+    return webService_;
   }
 
   
-  Deprecated::OrthancApiClient& StoneApplicationContext::GetOrthancApiClient()
+  boost::shared_ptr<Deprecated::OrthancApiClient> 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<Deprecated::IWebService> webService)
   {
-    webService_ = &webService;
+    webService_ = webService;
     InitializeOrthanc();
   }
 
--- a/Applications/StoneApplicationContext.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Applications/StoneApplicationContext.h	Tue Mar 31 15:51:02 2020 +0200
@@ -59,18 +59,15 @@
   class StoneApplicationContext : public boost::noncopyable
   {
   private:
-    MessageBroker&                   broker_;
-    Deprecated::IWebService*         webService_;
-    Deprecated::IDelayedCallExecutor*            delayedCallExecutor_;
-    std::unique_ptr<Deprecated::OrthancApiClient>  orthanc_;
+    boost::shared_ptr<Deprecated::IWebService>     webService_;
+    Deprecated::IDelayedCallExecutor*  delayedCallExecutor_;   // TODO => shared_ptr ??
+    boost::shared_ptr<Deprecated::OrthancApiClient>  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<Deprecated::IWebService> GetWebService();
 
-    bool HasWebService() const
-    {
-      return webService_ != NULL;
-    }
+    boost::shared_ptr<Deprecated::OrthancApiClient> GetOrthancApiClient();
 
-    Deprecated::IWebService& GetWebService();
-
-    Deprecated::OrthancApiClient& GetOrthancApiClient();
-
-    void SetWebService(Deprecated::IWebService& webService);
+    void SetWebService(boost::shared_ptr<Deprecated::IWebService> webService);
 
     void SetOrthancBaseUrl(const std::string& baseUrl);
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Docs/stone-object-model-reference.md	Tue Mar 31 15:51:02 2020 +0200
@@ -0,0 +1,439 @@
+## Scene2D and viewport-related object reference
+
+### `Scene2D` 
+
+Represents a collection of layers that display 2D data.
+
+These layers must implement `ISceneLayer`
+
+The layers must be created externally and set to a specific Z-order index 
+with the `SetLayer` method.
+
+The `Scene2D` object merely acts as a layer container. It has no rendering 
+or layer creation facility on its own.
+
+The `Scene2D` contains an `AffineTransform2D` structure that defines how 
+the various layer item coordinates are transformed before being displayed 
+on the viewport (aka canvas)
+
+It is up to each layer type-specific renderer to choose how this transformation
+is used. See the various kinds of layer below for more details.
+
+Examining the `Scene2D` contents can be done either by implementing the 
+`Scene2D::IVisitor` interface and calling `Apply(IVisitor& visitor)` or by 
+iterating between `GetMinDepth()` and `GetMaxDepth()` and calling the 
+`ISceneLayer& GetLayer(int depth)` getter.
+
+### `ISceneLayer` 
+
+Interface that must be implemented by `Scene2D` layers. This is a closed list
+that, as of 2020-03, contains:
+
+```
+  Type_InfoPanel,
+  Type_ColorTexture,
+  Type_Polyline,
+  Type_Text,
+  Type_FloatTexture,
+  Type_LookupTableTexture
+```
+
+Please note that this interface mandates the implementation of a `GetRevision`
+method returning an `uint64_t`. 
+
+The idea is that when a model gets converted to a set of `ISceneLayer` 
+instances, changes in the model that result in changes to the layers must 
+increase the revision number of these layers.
+
+That allows the rendering process to safely assume that a given layers whose
+revision does not change hasn't been modified (this helps with caching).
+
+Every mutable method in `ISceneLayer` instances that possibly change the visual
+representation of an `ISceneLayer` must increase this revision number.
+
+### Implementation: `FloatTextureSceneLayer`
+
+Layer that renders an `Orthanc::ImageAccessor` object that must be convertible
+to `Float32` image.
+
+The constructor only uses the image accessor to perform a copy. It can safely 
+be deleted afterwards.
+
+The input values are mapped to the output values by taking into account various
+properties that can be modified with:
+
+- `SetWindowing`: uses windowing presets like "bone" or "lung"
+- `SetCustomWindowing`: with manual window center and width
+- `SetInverted`: toggles black <-> white inversion after windowing
+- `SetApplyLog`: uses a non-linear response curve described in 
+  https://theailearner.com/2019/01/01/log-transformation/ that expands contrast
+  in dark areas while compressing contrast in bright ones. This is **not** 
+  implemented in the OpenGL renderer!
+
+The corresponding renderers are `OpenGLFloatTextureRenderer` and 
+`CairoFloatTextureRenderer`. The scene transformation is applied during
+rendering.
+
+### Implementation: `ColorTextureSceneLayer`
+
+Layer that renders an `Orthanc::ImageAccessor` object an RGBA image (alpha must
+be premultiplied). 
+
+The constructor only uses the image accessor to perform a copy. It can safely 
+be deleted afterwards.
+
+The corresponding renderers are `OpenGLColorTextureRenderer` and 
+`CairoColorTextureRenderer`. The scene transformation is applied during
+rendering.
+
+### Implementation: `LookupTableTextureSceneLayer`
+
+Layer that renders an `Orthanc::ImageAccessor` object that must be convertible
+to `Float32` image.
+
+The constructor only uses the image accessor to perform a copy. It can safely 
+be deleted afterwards.
+
+The final on-screen color of each pixel is determined by passing the input 
+`Float32` value through a 256-entry look-up table (LUT) that can be passed as 
+an array of either 256 x 3 bytes (for opaque RGB colors) or 256 x 4 bytes (for
+RGBA pixels). The LUT is not specified at construction time, but with 
+calls to `SetLookupTable` or `SetLookupTableGrayscale` (that fills the LUT 
+with a gradient from black to white, fully opaque)
+
+The range of input values that is mapped to the entirety of the LUT is, by
+default, the full image range, but can be customized with `SetRange`.
+
+The corresponding renderers are `OpenGLLookupTableTextureRenderer` and
+`CairoLookupTableTextureRenderer`. The scene transformation is applied during
+rendering.
+
+### Implementation: `PolylineSceneLayer`
+
+Layer that renders vector-based polygonal lines. 
+
+Polylines can be added with the `AddChain` method, that accepts a `Chain`, that
+is a typedef to `std::vector<ScenePoint2D>`, a flag to specify whether the 
+chain must be automatically close (last point of the vector connected to the
+first one) and the chain color (a `Color` structure).
+
+Please note that the line thickness is, contrary to the color, specified 
+per-chain but rather per-layer. 
+
+If you need multiple line thicknesses, multiple `PolylineSceneLayer` must be
+created.
+
+The corresponding renderers are `OpenGLAdvancedPolylineRenderer` and
+`CairoPolylineRenderer`. The scene transformation is applied during
+rendering.
+
+### Implementation: `TextSceneLayer`
+
+This layers renders a paragraph of text. 
+
+The inputs to the layer can be changed after creation and are:
+- The text iself, supplied as an UTF-8 encoded string in `SetText`
+- The font used for rendering, set by `SetFontIndex`.
+- The text anchoring, through `SetAnchor`: the text can be anchored to 
+  various positions, such as top lef, center, bottom center,... These 
+  various anchors are part of the `BitmapAnchor` enumeration.
+- The text position, relative to its anchor, through `SetPosition`.
+
+The font is supplied as an index. This is an index in the set of fonts 
+that has been registered in the viewport compositor. The following code 
+shows how to set such a font:
+
+```
+std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_.Lock());
+lock->GetCompositor().SetFont(0, 
+                              Orthanc::EmbeddedResources::UBUNTU_FONT, 
+                              32, Orthanc::Encoding_Latin1);
+// where 32 is the font size in pixels
+```
+
+This call uses the embedded `UBUNTU_FONT` resource that has been defined in
+the `CMakeLists.txt` file with:
+
+```
+set(ORTHANC_STONE_APPLICATION_RESOURCES 
+  UBUNTU_FONT  ${CMAKE_BINARY_DIR}/ubuntu-font-family-0.83/Ubuntu-R.ttf
+)
+```
+
+Please note that you must supply a font: there is no default font provided by
+the OpenGL or Cairo compositors.
+
+The corresponding renderers are `OpenGLTextRenderer` and
+`CairoTextRenderer`. The scene transformation is not applied during rendering,
+because the text anchoring, position and scaling are computed relative to the
+viewport/canvas.
+
+### Implementation: `InfoPanelSceneLayer`
+
+This layer is designed to display an image, supplied through an 
+`Orthanc::ImageAccessor` reference (only used at construction time).
+
+The image is not transformed according to the normal layer transformation but 
+is rather positioned relative to the canvas, with the same mechanism as the
+`TextSceneLayer` described above.
+
+The image position is specified with the sole means of the `SetAnchor` method.
+
+The corresponding renderers are `OpenGLInfoPanelRenderer` and 
+`CairoInfoPanelRenderer`.
+
+### `IViewport`
+
+https://bitbucket.org/sjodogne/orthanc-stone/src/broker/Framework/Viewport/IViewport.h
+
+(**not** the one in `Deprecated`)
+- Implemented by classes that:
+  - manage the on-screen display of a `Scene2D` trough a compositor.
+  - Own the `ICompositor` object that performs the rendering. 
+  - Own the `Scene2D` (TODO: currently through `ViewportController` --> `Scene2D`)
+  - Provide a `Lock` method that returns a RAII, that must be kept alive when 
+    modifying the underlying objects (controller, compositor, scene), but not 
+    longer.
+
+#### Implementation: `SdlOpenGLViewport`
+- Implementation of a viewport rendered on a SDL window, that uses OpenGL for 
+  rendering.
+- Instantiating this object creates an SDL window. Automatic scaling for hiDPI
+  displays can be toggled on or off. 
+
+#### Implementation: `WebGLViewport`
+- Implementation of a viewport rendered on a DOM canvas, that uses OpenGL for 
+  rendering.
+- Contrary to the SDL OpenGL viewport, the canvas must already be existing 
+  when the ctor is called.
+
+### `ICompositor`
+The interface providing a rendering service for `Scene2D` objects. 
+
+**Subclasses:** `CairoCompositor`, `OpenGLCompositor`
+ 
+You do not need to create compositor instances. They are created for you when
+instantiating a viewport.
+
+### `ViewportController`
+This concrete class is instantiated by its `IViewport` owner. 
+
+**TODO:** its functionality is not well defined and should be moved into the 
+viewport base class. Support for measuring tools should be moved to a special
+interactor.
+
+- contains:
+  - array of `MeasureTool`
+  - ref to `IViewport`
+  - `activeTracker_`
+  - owns a `Scene2D`
+  - weak ref to `UndoStack`
+  - cached `canvasToSceneFactor_`
+
+- contains logic to:
+  - pass commands to undostack (trivial)
+  - logic to locate `MeasureTool` in the HitTest
+  - OTOH, the meat of the measuring tool logic (highlighting etc..) is 
+    done in app-specific code (`VolumeSlicerWidget` for IBA)
+  - accept new Scene transform and notify listeners
+  - **the code that uses the interactor** (`HandleMousePress`) is only 
+    called by the new `WebAssemblyViewport` !!! **TODO** clean this mess
+
+### `IViewportInteractor`
+- must provide logic to respond to `CreateTracker`
+
+### `DefaultViewportInteractor`
+- provides Pan+Rotate+Zoom trackers
+
+### `WebGLViewportsRegistry`
+
+This class is a singleton (accessible through `GetWebGLViewportsRegistry()` 
+that deals with context losses in the WebGL contexts.
+
+You use it by creating a WebGLViewport in the following fashion:
+
+```
+boost::shared_ptr<OrthancStone::WebGLViewport> viewport(
+  OrthancStone::GetWebGLViewportsRegistry().Add(canvasId));
+```
+
+## Source data related
+
+### `IVolumeSlicer`
+
+A very simple interface with a single method:
+`IVolumeSlicer::IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane)`
+
+### `IVolumeSlicer::IExtractedSlice`
+
+On a slice has been extracted from a volume by an `IVolumeSlicer`, it can
+report its *revision number*. 
+
+If another call to `ExtractSlice` with the same cutting plane is made, but 
+the returned slice revision is different, it means that the volume has 
+changed and the scene layer must be refreshed.
+
+Please see `VolumeSceneLayerSource::Update` to check how this logic is 
+implemented.
+
+
+### `OrthancSeriesVolumeProgressiveLoader`
+
+This class implements `IVolumeSlicer` (and `IObservable`) and can be used to 
+load a volume stored in a Dicom series on an Orthanc server.
+
+Over the course of the series loading, various notifications are sent:
+
+The first one is `OrthancStone::DicomVolumeImage::GeometryReadyMessage` that
+is sent when the volume extent and geometric properties are known.
+
+Then, as slices get loaded and the volume is filled,  
+`OrthancStone::DicomVolumeImage::ContentUpdatedMessage` are sent.
+
+Once all the highest-quality slices have been loaded, the 
+`OrthancSeriesVolumeProgressiveLoader::VolumeImageReadyInHighQuality` 
+notification is sent.
+
+Please note that calling `ExtractSlice` *before* the geometry is loaded will
+yield an instance of `InvalidSlice` that cannot be used to create a layer.
+
+On the other hand, 
+
+### `VolumeSceneLayerSource`
+
+This class makes the bridge between a volume (supplied by an `IVolumeSlicer`
+interface) and a `Scene2D`. 
+
+Please note that the bulk of the work is done the objects implementing 
+`IVolumeSlicer` and this object merely connects things together.
+
+For instance, deciding whether an image (texture) or vector (polyline) layer
+is done by the `IVolumeSlicer` implementation.
+
+- contains:
+  - reference to Scene2D
+  - `layerIndex_` (fixed at ctor) that is the index, in the Scene2D layer 
+    stack, of the layer that will be created/updated
+  - `IVolumeSlicer`
+
+- contains logic to:
+  - extract a slice from the slicer and set/refresh the Scene2D layer at 
+    the supplied `layerIndex_`
+  - refresh this based on the slice revision or configuration revision
+  - accept a configuration that will be applied to the layer
+  - the `Update()` method will 
+
+## Updates and the configurators
+
+`ISceneLayer` does not expose mutable methods. 
+
+The way to change a layer once it has been created is through configurator 
+objets.
+
+If you plan to set (even only once) or modify some layer properties after 
+layer creation, you need to create a matching configurator objet.
+
+For instance, in the `VolumeSceneLayerSource`, the `SetConfigurator` method
+will store a `ILayerStyleConfigurator* configurator_`.
+
+In the `OrthancView` ctor, you can see how it is used:
+
+```
+std::unique_ptr<GrayscaleStyleConfigurator> style(
+  new GrayscaleStyleConfigurator);
+
+style->SetLinearInterpolation(true);
+
+...<some more code>...
+
+std::unique_ptr<LookupTableStyleConfigurator> config(
+  new LookupTableStyleConfigurator);
+
+config->SetLookupTable(Orthanc::EmbeddedResources::COLORMAP_HOT);
+
+```
+
+The configurator type are created according to the type of layer.¸
+
+Later, in `VolumeSceneLayerSource::Update(const CoordinateSystem3D& plane)`, 
+if the cutting plane has **not** changed and if the layer revision has **not**
+changed, we test `configurator_->GetRevision() != lastConfiguratorRevision_` 
+and, if different, we call `configurator_->ApplyStyle(scene_.GetLayer(layerDepth_));`
+
+This allows to change layer properties that do not depend on the layer model
+contents.
+
+On the other hand, if the layer revision has changed, when compared to the 
+last time it has been rendered (stored in `lastRevision_`), then we need to 
+ask the slice to create a brand new layer.
+
+Another way to see it is that layer rendering depend on model data and view 
+data. The model data is not mutable in the layer and, if the model changes, the 
+layer must be recreated.
+
+If only the view properties change (the configurator), we call ApplyStyle 
+(that **will** mutate some of the layer internals)
+
+Please note that the renderer does **not** know about the configurator : the 
+renderer uses properies in the layer and does not care whether those have 
+been set once at construction time or at every frame (configuration time).
+
+
+## Cookbook
+
+### Simple application
+
+#### Building
+
+In order to create a Stone application, you need to:
+
+- CMake-based application:
+    ```
+    include(${STONE_SOURCES_DIR}/Resources/CMake/OrthancStoneConfiguration.cmake)
+    ```
+  with this library target that you have to define:
+    ```
+    add_library(OrthancStone STATIC ${ORTHANC_STONE_SOURCES})
+    ```
+  then link with this library:
+    ```
+    target_link_libraries(MyStoneApplication OrthancStone)
+    ```
+
+Building is supported with emscripten, Visual C++ (>= 9.0), gcc...
+
+emscripten recommended version >= 1.38.41
+
+These are very rough guidelines. See the `Samples` folder for actual examples.
+
+#### Structure
+
+The code requires a loader (object that )
+
+Initialize:
+
+```
+Orthanc::Logging::Initialize();
+Orthanc::Logging::EnableInfoLevel(true);
+```
+Call, in WASM:
+```
+DISPATCH_JAVASCRIPT_EVENT("StoneInitialized");
+```
+
+
+
+# TODO order
+- **IBA** remove SDL Console failed attempt
+- **IBA** understand refresh issue --> push fix
+- **IBA** move main.cpp to tests/
+- **IBA** Remove deprecated tag from loaders and rename loaders (ensure "Progressive" only means spatially progressive, not in terms of quality.. make sure Yoann doesn't need Jpeg)
+- **IBA** 
+
+
+# Notes
+
+- It is NOT possible to abandon the existing loaders : they contain too much loader-specific getters
+
+
+
--- a/Framework/Deprecated/Layers/DicomSeriesVolumeSlicer.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Deprecated/Layers/DicomSeriesVolumeSlicer.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -87,59 +87,72 @@
   }
 
 
-  DicomSeriesVolumeSlicer::DicomSeriesVolumeSlicer(OrthancStone::MessageBroker& broker,
-                                                   OrthancApiClient& orthanc) :
-    IVolumeSlicer(broker),
-    IObserver(broker),
-    loader_(broker, orthanc),
+  DicomSeriesVolumeSlicer::DicomSeriesVolumeSlicer() :
     quality_(SliceImageQuality_FullPng)
   {
-    loader_.RegisterObserverCallback(
-      new OrthancStone::Callable<DicomSeriesVolumeSlicer, OrthancSlicesLoader::SliceGeometryReadyMessage>
-        (*this, &DicomSeriesVolumeSlicer::OnSliceGeometryReady));
-
-    loader_.RegisterObserverCallback(
-      new OrthancStone::Callable<DicomSeriesVolumeSlicer, OrthancSlicesLoader::SliceGeometryErrorMessage>
-      (*this, &DicomSeriesVolumeSlicer::OnSliceGeometryError));
-
-    loader_.RegisterObserverCallback(
-      new OrthancStone::Callable<DicomSeriesVolumeSlicer, OrthancSlicesLoader::SliceImageReadyMessage>
-        (*this, &DicomSeriesVolumeSlicer::OnSliceImageReady));
-
-    loader_.RegisterObserverCallback(
-      new OrthancStone::Callable<DicomSeriesVolumeSlicer, OrthancSlicesLoader::SliceImageErrorMessage>
-      (*this, &DicomSeriesVolumeSlicer::OnSliceImageError));
   }
 
+  void DicomSeriesVolumeSlicer::Connect(boost::shared_ptr<OrthancApiClient> orthanc)
+  {
+    loader_.reset(new OrthancSlicesLoader(orthanc));
+    Register<OrthancSlicesLoader::SliceGeometryReadyMessage>(*loader_, &DicomSeriesVolumeSlicer::OnSliceGeometryReady);
+    Register<OrthancSlicesLoader::SliceGeometryErrorMessage>(*loader_, &DicomSeriesVolumeSlicer::OnSliceGeometryError);
+    Register<OrthancSlicesLoader::SliceImageReadyMessage>(*loader_, &DicomSeriesVolumeSlicer::OnSliceImageReady);
+    Register<OrthancSlicesLoader::SliceImageErrorMessage>(*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<OrthancStone::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 +164,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_);
     }
   }
 }
--- a/Framework/Deprecated/Layers/DicomSeriesVolumeSlicer.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Deprecated/Layers/DicomSeriesVolumeSlicer.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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<DicomSeriesVolumeSlicer>
     //private OrthancSlicesLoader::ISliceLoaderObserver
   {
   public:
@@ -79,13 +80,14 @@
   private:
     class RendererFactory;
     
-    OrthancSlicesLoader  loader_;
+    boost::shared_ptr<OrthancSlicesLoader> loader_;
     SliceImageQuality    quality_;
 
   public:
-    DicomSeriesVolumeSlicer(OrthancStone::MessageBroker& broker,
-                            OrthancApiClient& orthanc);
+    DicomSeriesVolumeSlicer();
 
+    void Connect(boost::shared_ptr<OrthancApiClient> 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<OrthancStone::Vector>& points,
--- a/Framework/Deprecated/Layers/DicomStructureSetSlicer.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Deprecated/Layers/DicomStructureSetSlicer.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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<OrthancStone::Point2D> > polygons_;
+#else
       std::vector< std::pair<OrthancStone::Point2D, OrthancStone::Point2D> > 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<DicomStructureSetSlicer, StructureSetLoader::ContentChangedMessage>
-      (*this, &DicomStructureSetSlicer::OnStructureSetLoaded));
+    Register<StructureSetLoader::ContentChangedMessage>(loader_, &DicomStructureSetSlicer::OnStructureSetLoaded);
   }
 
 
--- a/Framework/Deprecated/Layers/DicomStructureSetSlicer.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Deprecated/Layers/DicomStructureSetSlicer.h	Tue Mar 31 15:51:02 2020 +0200
@@ -28,7 +28,7 @@
 {
   class DicomStructureSetSlicer :
     public IVolumeSlicer,
-    public OrthancStone::IObserver
+    public OrthancStone::ObserverBase<DicomStructureSetSlicer>
   {
   private:
     class Renderer;
@@ -42,8 +42,7 @@
     }
 
   public:
-    DicomStructureSetSlicer(OrthancStone::MessageBroker& broker,
-                            StructureSetLoader& loader);
+    DicomStructureSetSlicer(StructureSetLoader& loader);
 
     virtual bool GetExtent(std::vector<OrthancStone::Vector>& points,
                            const OrthancStone::CoordinateSystem3D& viewportPlane)
--- a/Framework/Deprecated/Layers/IVolumeSlicer.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Deprecated/Layers/IVolumeSlicer.h	Tue Mar 31 15:51:02 2020 +0200
@@ -122,11 +122,6 @@
     };
 
 
-    IVolumeSlicer(OrthancStone::MessageBroker& broker) :
-      IObservable(broker)
-    {
-    }
-    
     virtual ~IVolumeSlicer()
     {
     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Deprecated/Loaders/DicomStructureSetLoader.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -0,0 +1,428 @@
+/**
+ * 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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "DicomStructureSetLoader.h"
+
+#include "../../Scene2D/PolylineSceneLayer.h"
+#include "../../StoneException.h"
+#include "../../Toolbox/GeometryToolbox.h"
+
+#include <Core/Toolbox.h>
+
+#include <algorithm>
+
+#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<DicomStructureSetLoader>();
+
+      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<DicomStructureSetLoader>();
+
+      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::unique_ptr<OrthancStone::OrthancRestApiCommand> 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<DicomStructureSetLoader>();
+        
+      {
+        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<std::string>::iterator foundIt =
+              std::find(
+                loader.initiallyVisibleStructures_.begin(),
+                loader.initiallyVisibleStructures_.end(),
+                structureName);
+            std::vector<std::string>::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<std::string> instances;
+      std::set<std::string> nonEmptyInstances;
+      loader.content_->GetReferencedInstances(instances);
+      for (std::set<std::string>::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<unsigned int>(nonEmptyInstances.size());
+
+      for (std::set<std::string>::const_iterator
+        it = nonEmptyInstances.begin(); it != nonEmptyInstances.end(); ++it)
+      {
+        std::unique_ptr<OrthancStone::OrthancRestApiCommand> 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<bool>         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<bool> visibility = std::vector<bool>()) 
+      : 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::unique_ptr<OrthancStone::PolylineSceneLayer> 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<OrthancStone::Point2D> > 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<OrthancStone::Point2D, OrthancStone::Point2D> > 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::ILoadersContext& loadersContext) 
+    : LoaderStateMachine(loadersContext)
+    , loadersContext_(loadersContext)
+    , revision_(0)
+    , countProcessedInstances_(0)
+    , countReferencedInstances_(0)
+    , structuresReady_(false)
+  {
+  }
+   
+    
+  boost::shared_ptr<Deprecated::DicomStructureSetLoader> DicomStructureSetLoader::Create(OrthancStone::ILoadersContext& loadersContext)
+  {
+    boost::shared_ptr<DicomStructureSetLoader> obj(
+      new DicomStructureSetLoader(
+        loadersContext));
+    obj->LoaderStateMachine::PostConstructor();
+    return obj;
+
+  }
+
+  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<std::string>& initiallyVisibleStructures)
+  {
+    Start();
+      
+    instanceId_ = instanceId;
+    initiallyVisibleStructures_ = initiallyVisibleStructures;
+
+    {
+      std::unique_ptr<OrthancStone::OrthancRestApiCommand> 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_;
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Deprecated/Loaders/DicomStructureSetLoader.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../Toolbox/DicomStructureSet.h"
+#include "../../Volumes/IVolumeSlicer.h"
+#include "LoaderStateMachine.h"
+
+#include <vector>
+
+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
+    
+    OrthancStone::ILoadersContext&                    loadersContext_;
+    std::unique_ptr<OrthancStone::DicomStructureSet>  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<std::string> 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<bool>                  structureVisibility_;
+
+  protected:
+    DicomStructureSetLoader(OrthancStone::ILoadersContext& loadersContext);
+
+  public:
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, StructuresReady, DicomStructureSetLoader);
+
+    static boost::shared_ptr<DicomStructureSetLoader> Create(
+      OrthancStone::ILoadersContext& loadersContext);
+
+    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<std::string>& initiallyVisibleStructures = std::vector<std::string>());
+
+    virtual IExtractedSlice* ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE;
+
+    void SetStructuresReady();
+
+    bool AreStructuresReady() const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Deprecated/Loaders/DicomStructureSetLoader2.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+#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<DicomStructureSetLoader2, OrthancRestApiCommand::SuccessMessage>
+      (*this, &DicomStructureSetLoader2::HandleSuccessMessage));
+
+    oracleObservable.RegisterObserverCallback(
+      new Callable<DicomStructureSetLoader2, OracleCommandExceptionMessage>
+      (*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::unique_ptr<OrthancRestApiCommand> 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
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Deprecated/Loaders/DicomStructureSetLoader2.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+#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 <boost/noncopyable.hpp>
+
+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
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Deprecated/Loaders/LoaderCache.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -0,0 +1,384 @@
+/**
+ * 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 <http://www.gnu.org/licenses/>.
+ **/
+
+#include "LoaderCache.h"
+
+#include "../../StoneException.h"
+#include "OrthancSeriesVolumeProgressiveLoader.h"
+#include "OrthancMultiframeVolumeLoader.h"
+#include "DicomStructureSetLoader.h"
+
+#include "../../Loaders/ILoadersContext.h"
+
+
+#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2
+#include "DicomStructureSetLoader2.h"
+#endif 
+ //BGO_ENABLE_DICOMSTRUCTURESETLOADER2
+
+
+#if ORTHANC_ENABLE_WASM == 1
+# include <unistd.h>
+# 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 <Core/OrthancException.h>
+#include <Core/Toolbox.h>
+
+namespace Deprecated
+{
+  LoaderCache::LoaderCache(OrthancStone::ILoadersContext& loadersContext)
+    : loadersContext_(loadersContext)
+  {
+
+  }
+
+  boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> 
+    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())
+      {
+        std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock());
+
+        boost::shared_ptr<OrthancStone::DicomVolumeImage> volumeImage(new OrthancStone::DicomVolumeImage);
+        boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> loader;
+      
+        // true means "use progressive quality"
+        // false means "load high quality slices only"
+        loader = OrthancSeriesVolumeProgressiveLoader::Create(loadersContext_, volumeImage, false);
+        loader->LoadSeries(seriesUuid);
+        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<OrthancMultiframeVolumeLoader> 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<OrthancStone::DicomVolumeImageMPRSlicer> 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())
+      {
+        std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock());
+        boost::shared_ptr<OrthancStone::DicomVolumeImage> volumeImage(new OrthancStone::DicomVolumeImage);
+        boost::shared_ptr<OrthancMultiframeVolumeLoader> loader;
+        {
+          loader = OrthancMultiframeVolumeLoader::Create(loadersContext_, volumeImage);
+          loader->LoadInstance(instanceUuid);
+        }
+        multiframeVolumeLoaders_[instanceUuid] = loader;
+        boost::shared_ptr<OrthancStone::DicomVolumeImageMPRSlicer> 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<DicomStructureSetSlicer2> 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<std::string>& stringList)
+  {
+    if (stringList.size() == 0)
+    {
+      return "";
+    } 
+    else
+    {
+      std::vector<std::string> 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<DicomStructureSetLoader> 
+    LoaderCache::GetDicomStructureSetLoader(
+      std::string inInstanceUuid, 
+      const std::vector<std::string>& 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())
+      {
+        std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock());
+
+        boost::shared_ptr<DicomStructureSetLoader> loader;
+        {
+          loader = DicomStructureSetLoader::Create(loadersContext_);
+          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<DicomStructureSetLoader2> 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<DicomStructureSetLoader2> loader;
+        boost::shared_ptr<DicomStructureSet2> structureSet(new DicomStructureSet2());
+        boost::shared_ptr<DicomStructureSetSlicer2> 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()
+  {
+    std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock());
+    
+#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<typename T> void DebugDisplayObjRefCountsInMap(
+    const std::string& name, const std::map<std::string, boost::shared_ptr<T> >& myMap)
+  {
+    LOG(TRACE) << "Map \"" << name << "\" ref counts:";
+    size_t i = 0;
+    for (typename std::map<std::string, boost::shared_ptr<T> >::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
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Deprecated/Loaders/LoaderCache.h	Tue Mar 31 15:51:02 2020 +0200
@@ -0,0 +1,97 @@
+/**
+ * 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 <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+#include "../Messages/LockingEmitter.h"
+#include "../../Volumes/DicomVolumeImageMPRSlicer.h"
+#include "OrthancSeriesVolumeProgressiveLoader.h"
+#include "OrthancMultiframeVolumeLoader.h"
+#include "DicomStructureSetLoader.h"
+
+#include <boost/shared_ptr.hpp>
+
+#include <map>
+#include <string>
+#include <vector>
+
+namespace OrthancStone
+{
+  class ILoadersContext;
+}
+
+namespace Deprecated
+{
+  class LoaderCache
+  {
+  public:
+    LoaderCache(OrthancStone::ILoadersContext& loadersContext);
+
+    boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader>
+      GetSeriesVolumeProgressiveLoader      (std::string seriesUuid);
+    
+    boost::shared_ptr<OrthancStone::DicomVolumeImageMPRSlicer>
+      GetMultiframeDicomVolumeImageMPRSlicer(std::string instanceUuid);
+
+    boost::shared_ptr<OrthancMultiframeVolumeLoader>
+      GetMultiframeVolumeLoader(std::string instanceUuid);
+
+    boost::shared_ptr<DicomStructureSetLoader>
+      GetDicomStructureSetLoader(
+        std::string instanceUuid,
+        const std::vector<std::string>& initiallyVisibleStructures);
+
+#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2
+    boost::shared_ptr<DicomStructureSetLoader2>
+      GetDicomStructureSetLoader2(std::string instanceUuid);
+
+    boost::shared_ptr<DicomStructureSetSlicer2>
+      GetDicomStructureSetSlicer2(std::string instanceUuid);
+#endif 
+    //BGO_ENABLE_DICOMSTRUCTURESETLOADER2
+
+    void ClearCache();
+
+  private:
+    
+    void DebugDisplayObjRefCounts();
+
+    OrthancStone::ILoadersContext& loadersContext_;
+
+    std::map<std::string, boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> >
+      seriesVolumeProgressiveLoaders_;
+    std::map<std::string, boost::shared_ptr<OrthancMultiframeVolumeLoader> >
+      multiframeVolumeLoaders_;
+    std::map<std::string, boost::shared_ptr<OrthancStone::DicomVolumeImageMPRSlicer> >
+      dicomVolumeImageMPRSlicers_;
+    std::map<std::string, boost::shared_ptr<DicomStructureSetLoader> >
+      dicomStructureSetLoaders_;
+#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2
+    std::map<std::string, boost::shared_ptr<DicomStructureSetLoader2> >
+      dicomStructureSetLoaders2_;
+    std::map<std::string, boost::shared_ptr<DicomStructureSet2> >
+      dicomStructureSets2_;
+    std::map<std::string, boost::shared_ptr<DicomStructureSetSlicer2> >
+      dicomStructureSetSlicers2_;
+#endif 
+    //BGO_ENABLE_DICOMSTRUCTURESETLOADER2
+  };
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Deprecated/Loaders/LoaderStateMachine.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -0,0 +1,219 @@
+/**
+ * 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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "LoaderStateMachine.h"
+
+#include "../../Loaders/ILoadersContext.h"
+
+#include <Core/OrthancException.h>
+
+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::unique_ptr<OrthancStone::OracleCommandBase> 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;
+
+      {
+        std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock());
+        boost::shared_ptr<IObserver> observer(GetSharedObserver());
+        lock->Schedule(observer, 0, nextCommand); // TODO: priority!
+      }
+      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 <typename T>
+  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<State&>(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::ILoadersContext& loadersContext)
+    : loadersContext_(loadersContext)
+    , active_(false)
+    , simultaneousDownloads_(4)
+    , activeCommands_(0)
+  {
+    using OrthancStone::ILoadersContext;
+
+    LOG(TRACE) 
+      << "LoaderStateMachine(" << std::hex << this 
+      << std::dec << ")::LoaderStateMachine()";
+  }
+
+  void LoaderStateMachine::PostConstructor()
+  {
+    std::unique_ptr<OrthancStone::ILoadersContext::ILock>
+      lock(loadersContext_.Lock());
+
+    OrthancStone::IObservable& observable = lock->GetOracleObservable();
+
+    // TODO => Move this out of constructor
+    Register<OrthancStone::OrthancRestApiCommand::SuccessMessage>(
+      observable, &LoaderStateMachine::HandleSuccessMessage);
+    Register<OrthancStone::GetOrthancImageCommand::SuccessMessage>(
+      observable, &LoaderStateMachine::HandleSuccessMessage);
+    Register<OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage>(
+      observable, &LoaderStateMachine::HandleSuccessMessage);
+    Register<OrthancStone::OracleCommandExceptionMessage>(
+      observable, &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;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Deprecated/Loaders/LoaderStateMachine.h	Tue Mar 31 15:51:02 2020 +0200
@@ -0,0 +1,129 @@
+/**
+ * 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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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 <Core/IDynamicObject.h>
+
+#include <list>
+
+namespace OrthancStone
+{
+  class ILoadersContext;
+}
+
+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.
+
+     To use it, you need to create commands that derive from State.
+
+     You need to initialize them with the object that must be called when 
+     an answer is received. 
+  */
+
+  class LoaderStateMachine : public OrthancStone::ObserverBase<LoaderStateMachine>
+  {
+  public:
+    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 <typename T>
+      T& GetLoader() const
+      {
+        return dynamic_cast<T&>(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 <typename T>
+    void HandleSuccessMessage(const T& message);
+
+    typedef std::list<OrthancStone::IOracleCommand*>  PendingCommands;
+
+    OrthancStone::ILoadersContext&  loadersContext_;
+    bool                            active_;
+    unsigned int                    simultaneousDownloads_;
+    PendingCommands                 pendingCommands_;
+    unsigned int                    activeCommands_;
+
+
+  public:
+    LoaderStateMachine(OrthancStone::ILoadersContext& loadersContext);
+
+    void PostConstructor();
+
+    virtual ~LoaderStateMachine();
+
+    bool IsActive() const
+    {
+      return active_;
+    }
+
+    void SetSimultaneousDownloads(unsigned int count);  
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -0,0 +1,593 @@
+/**
+ * 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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OrthancMultiframeVolumeLoader.h"
+
+#include <Core/Endianness.h>
+#include <Core/Toolbox.h>
+
+namespace Deprecated
+{
+  class OrthancMultiframeVolumeLoader::LoadRTDoseGeometry : public LoaderStateMachine::State
+  {
+  private:
+    std::unique_ptr<Orthanc::DicomMap>  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<OrthancMultiframeVolumeLoader>().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<OrthancMultiframeVolumeLoader>();
+        
+      Json::Value body;
+      message.ParseJsonBody(body);
+        
+      if (body.type() != Json::objectValue)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+      }
+
+      std::unique_ptr<Orthanc::DicomMap> 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::unique_ptr<OrthancStone::OrthancRestApiCommand> 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<OrthancMultiframeVolumeLoader>().SetTransferSyntax(message.GetAnswer());
+    }
+  };
+
+  class OrthancMultiframeVolumeLoader::LoadUncompressedPixelData : public State
+  {
+  public:
+    LoadUncompressedPixelData(OrthancMultiframeVolumeLoader& that) :
+    State(that)
+    {
+    }
+      
+    virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message)
+    {
+      GetLoader<OrthancMultiframeVolumeLoader>().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::unique_ptr<OrthancStone::OrthancRestApiCommand> 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<const uint32_t*>(source));
+  }
+
+  ORTHANC_FORCE_INLINE
+    static void CopyPixel(uint16_t& target, const void* source)
+  {
+    // TODO - check alignement?
+    target = le16toh(*reinterpret_cast<const uint16_t*>(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<uint16_t*>(&target);
+    CopyPixel(*targetUp, source);
+  }
+
+  template <typename T>
+  void OrthancMultiframeVolumeLoader::CopyPixelDataAndComputeDistribution(
+    const std::string& pixelData, std::map<T,uint64_t>& 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<const uint8_t*>(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<const uint8_t*>(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<T*>(writer.GetAccessor().GetRow(y));
+
+          for (unsigned int x = 0; x < width; x++)
+          {
+            CopyPixel(*target, source);
+
+            distribution[*target] += 1;
+
+            target++;
+            source += bpp;
+          }
+        }
+      }
+    }
+  }
+
+  template <typename T>
+  void OrthancMultiframeVolumeLoader::ComputeMinMaxWithOutlierRejection(
+    const std::map<T, uint64_t>& distribution)
+  {
+    if (distribution.size() == 0)
+    {
+      LOG(ERROR) << "ComputeMinMaxWithOutlierRejection -- Volume image empty.";
+    }
+    else
+    {
+      OrthancStone::ImageBuffer3D& target = volume_->GetPixelData();
+
+      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<T, uint64_t>::const_iterator it = distribution.begin();
+        uint64_t totalCount = 0;
+        distributionRawMin_ = static_cast<float>(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<float>(pixelValue);
+        }
+        LOG(INFO) << "Volume image. First distribution value = " 
+          << static_cast<float>(distributionRawMin_) 
+          << " | Last distribution value = " 
+          << static_cast<float>(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<uint64_t>(
+        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<T, uint64_t>::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<T, uint64_t>::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<float>(resultMin);
+      computedDistributionMax_ = static_cast<float>(resultMax);
+    }
+  }
+
+  template <typename T>
+  void OrthancMultiframeVolumeLoader::CopyPixelDataAndComputeMinMax(
+    const std::string& pixelData)
+  {
+    std::map<T, uint64_t> distribution;
+    CopyPixelDataAndComputeDistribution(pixelData, distribution);
+    ComputeMinMaxWithOutlierRejection(distribution);
+  }
+
+  void OrthancMultiframeVolumeLoader::SetUncompressedPixelData(const std::string& pixelData)
+  {
+    switch (volume_->GetPixelData().GetFormat())
+    {
+      case Orthanc::PixelFormat_Grayscale32:
+        CopyPixelDataAndComputeMinMax<uint32_t>(pixelData);
+        break;
+      case Orthanc::PixelFormat_Grayscale16:
+        CopyPixelDataAndComputeMinMax<uint16_t>(pixelData);
+        break;
+      case Orthanc::PixelFormat_SignedGrayscale16:
+        CopyPixelDataAndComputeMinMax<int16_t>(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(
+    OrthancStone::ILoadersContext& loadersContext,
+    boost::shared_ptr<OrthancStone::DicomVolumeImage> volume,
+    float outliersHalfRejectionRate) 
+    : LoaderStateMachine(loadersContext)
+    , volume_(volume)
+    , pixelDataLoaded_(false)
+    , outliersHalfRejectionRate_(outliersHalfRejectionRate)
+    , distributionRawMin_(0)
+    , distributionRawMax_(0)
+    , computedDistributionMin_(0)
+    , computedDistributionMax_(0)
+  {
+    if (volume.get() == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+  }
+
+
+  boost::shared_ptr<OrthancMultiframeVolumeLoader>
+    OrthancMultiframeVolumeLoader::Create(
+      OrthancStone::ILoadersContext& loadersContext, 
+      boost::shared_ptr<OrthancStone::DicomVolumeImage> volume, 
+      float outliersHalfRejectionRate /*= 0.0005*/)
+  {
+    boost::shared_ptr<OrthancMultiframeVolumeLoader> obj(
+      new OrthancMultiframeVolumeLoader(
+        loadersContext,
+        volume,
+        outliersHalfRejectionRate));
+    obj->LoaderStateMachine::PostConstructor();
+    return obj;
+  }
+
+  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::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand);
+      command->SetHttpHeader("Accept-Encoding", "gzip");
+      command->SetUri("/instances/" + instanceId + "/tags");
+      command->AcquirePayload(new LoadGeometry(*this));
+      Schedule(command.release());
+    }
+
+    {
+      std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand);
+      command->SetUri("/instances/" + instanceId + "/metadata/TransferSyntax");
+      command->AcquirePayload(new LoadTransferSyntax(*this));
+      Schedule(command.release());
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.h	Tue Mar 31 15:51:02 2020 +0200
@@ -0,0 +1,123 @@
+/**
+ * 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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "LoaderStateMachine.h"
+#include "../../Volumes/DicomVolumeImage.h"
+#include "../Volumes/IGeometryProvider.h"
+
+#include <boost/shared_ptr.hpp>
+
+namespace Deprecated
+{
+  class OrthancMultiframeVolumeLoader :
+    public LoaderStateMachine,
+    public OrthancStone::IObservable,
+    public IGeometryProvider
+  {
+  private:
+    class LoadRTDoseGeometry;
+    class LoadGeometry;
+    class LoadTransferSyntax;    
+    class LoadUncompressedPixelData;
+
+    boost::shared_ptr<OrthancStone::DicomVolumeImage>  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 <typename T>
+    void CopyPixelDataAndComputeMinMax(const std::string& pixelData);
+      
+    /** Service method for CopyPixelDataAndComputeMinMax*/
+    template <typename T>
+    void CopyPixelDataAndComputeDistribution(
+      const std::string& pixelData, 
+      std::map<T, uint64_t>& distribution);
+
+    /** Service method for CopyPixelDataAndComputeMinMax*/
+    template <typename T>
+    void ComputeMinMaxWithOutlierRejection(
+      const std::map<T, uint64_t>& distribution);
+
+    void SetUncompressedPixelData(const std::string& pixelData);
+
+    bool HasGeometry() const;
+    const OrthancStone::VolumeImageGeometry& GetImageGeometry() const;
+
+  protected:
+    OrthancMultiframeVolumeLoader(
+      OrthancStone::ILoadersContext& loadersContext,
+      boost::shared_ptr<OrthancStone::DicomVolumeImage> volume,
+      float outliersHalfRejectionRate);
+  public:
+
+    static boost::shared_ptr<OrthancMultiframeVolumeLoader> Create(
+      OrthancStone::ILoadersContext& loadersContext,
+      boost::shared_ptr<OrthancStone::DicomVolumeImage> volume,
+      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);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Deprecated/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -0,0 +1,577 @@
+/**
+ * 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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OrthancSeriesVolumeProgressiveLoader.h"
+
+#include "../../StoneException.h"
+#include "../../Loaders/ILoadersContext.h"
+#include "../../Loaders/BasicFetchingItemsSorter.h"
+#include "../../Loaders/BasicFetchingStrategy.h"
+#include "../../Toolbox/GeometryToolbox.h"
+#include "../../Volumes/DicomVolumeImageMPRSlicer.h"
+
+#include <Core/Images/ImageProcessing.h>
+#include <Core/OrthancException.h>
+
+namespace Deprecated
+{
+  using OrthancStone::ILoadersContext;
+
+  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<const OrthancStone::DicomInstanceParameters&>(slices.GetSlicePayload(i));
+        slices_.push_back(new OrthancStone::DicomInstanceParameters(slice));
+      }
+
+      CheckVolume();
+
+      double spacingZ;
+
+      if (slices.ComputeSpacingBetweenSlices(spacingZ))
+      {
+        LOG(TRACE) << "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<unsigned int>(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<unsigned int>& >(command.GetPayload()).GetValue();
+  }
+
+
+  void OrthancSeriesVolumeProgressiveLoader::ScheduleNextSliceDownload()
+  {
+    assert(strategy_.get() != NULL);
+      
+    unsigned int sliceIndex = 0, quality = 0;
+      
+    if (strategy_->GetNext(sliceIndex, quality))
+    {
+      if (!progressiveQuality_)
+      {
+        ORTHANC_ASSERT(quality == QUALITY_00, "INTERNAL ERROR. quality != QUALITY_00 in "
+                       << "OrthancSeriesVolumeProgressiveLoader::ScheduleNextSliceDownload");
+      }
+
+      const OrthancStone::DicomInstanceParameters& slice = seriesGeometry_.GetSliceParameters(sliceIndex);
+          
+      const std::string& instance = slice.GetOrthancInstanceIdentifier();
+      if (instance.empty())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+
+      std::unique_ptr<OrthancStone::OracleCommandBase> command;
+        
+      if (!progressiveQuality_ || quality == QUALITY_02)
+      {
+        std::unique_ptr<OrthancStone::GetOrthancImageCommand> 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());
+        //LOG(INFO) 
+        //  << "OrthancSeriesVolumeProgressiveLoader.ScheduleNextSliceDownload()"
+        //  << " sliceIndex = " << sliceIndex << " slice quality = " << quality 
+        //  << " URI = " << tmp->GetUri();
+        command.reset(tmp.release());
+      }
+      else // progressive mode is true AND quality is not final (different from QUALITY_02
+      {
+        std::unique_ptr<OrthancStone::GetOrthancWebViewerJpegCommand> 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)); // QUALITY_00 is Jpeg50 while QUALITY_01 is Jpeg90
+        tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat());
+        LOG(TRACE)
+          << "OrthancSeriesVolumeProgressiveLoader.ScheduleNextSliceDownload()"
+          << " sliceIndex = " << sliceIndex << " slice quality = " << quality;
+        command.reset(tmp.release());
+      }
+
+      command->AcquirePayload(new Orthanc::SingleValueObject<unsigned int>(sliceIndex));
+      
+      {
+        std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock());
+        boost::shared_ptr<IObserver> observer(GetSharedObserver());
+        lock->Schedule(observer, 0, command.release()); // TODO: priority!
+      }
+    }
+    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::unique_ptr<OrthancStone::DicomInstanceParameters> 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();
+
+      // If we are in progressive mode, the Fetching strategy will first request QUALITY_00, then QUALITY_01, then
+      // QUALITY_02... Otherwise, it's only QUALITY_00
+      unsigned int maxQuality = QUALITY_00;
+      if (progressiveQuality_)
+        maxQuality = QUALITY_02;
+
+      strategy_.reset(new OrthancStone::BasicFetchingStrategy(
+        sorter_->CreateSorter(static_cast<unsigned int>(slicesCount)),
+        maxQuality));
+
+      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)
+  {
+    ORTHANC_ASSERT(sliceIndex < slicesQuality_.size() &&
+           slicesQuality_.size() == volume_->GetPixelData().GetDepth());
+      
+    if (!progressiveQuality_)
+    {
+      ORTHANC_ASSERT(quality                    == QUALITY_00);
+      ORTHANC_ASSERT(slicesQuality_[sliceIndex] == QUALITY_00);
+    }
+
+    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_));
+    }
+    LOG(TRACE) << "SetSliceContent sliceIndex = " << sliceIndex << " -- will "
+      << " now call ScheduleNextSliceDownload()";
+    ScheduleNextSliceDownload();
+  }
+
+  void OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent(
+    const OrthancStone::GetOrthancImageCommand::SuccessMessage& message)
+  {
+    unsigned int quality = QUALITY_00;
+    if (progressiveQuality_)
+      quality = QUALITY_02;
+
+    SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), 
+                                         message.GetImage(),
+                                         quality);
+  }
+
+  void OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent(
+    const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message)
+  {
+    ORTHANC_ASSERT(progressiveQuality_, "INTERNAL ERROR: OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent"
+                   << " called while progressiveQuality_ is false!");
+
+    LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent";
+    unsigned int quality;
+      
+    switch (dynamic_cast<const OrthancStone::GetOrthancWebViewerJpegCommand&>(message.GetOrigin()).GetQuality())
+    {
+      case 50:
+        quality = QUALITY_00;
+        break;
+
+      case 90:
+        quality = QUALITY_01;
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+      
+    SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), quality);
+  }
+
+  OrthancSeriesVolumeProgressiveLoader::OrthancSeriesVolumeProgressiveLoader(
+    OrthancStone::ILoadersContext& loadersContext,
+    boost::shared_ptr<OrthancStone::DicomVolumeImage> volume,
+    bool progressiveQuality)
+    : loadersContext_(loadersContext)
+    , active_(false)
+    , progressiveQuality_(progressiveQuality)
+    , simultaneousDownloads_(4)
+    , volume_(volume)
+    , sorter_(new OrthancStone::BasicFetchingItemsSorter::Factory)
+    , volumeImageReadyInHighQuality_(false)
+  {
+  }
+
+  boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> 
+    OrthancSeriesVolumeProgressiveLoader::Create(
+      OrthancStone::ILoadersContext& loadersContext,
+      boost::shared_ptr<OrthancStone::DicomVolumeImage> volume,
+      bool progressiveQuality)
+  {
+    std::auto_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext.Lock());
+
+    boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> obj(
+        new OrthancSeriesVolumeProgressiveLoader(
+          loadersContext, volume, progressiveQuality));
+
+    obj->Register<OrthancStone::OrthancRestApiCommand::SuccessMessage>(
+      lock->GetOracleObservable(),
+      &OrthancSeriesVolumeProgressiveLoader::LoadGeometry);
+
+    obj->Register<OrthancStone::GetOrthancImageCommand::SuccessMessage>(
+      lock->GetOracleObservable(),
+      &OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent);
+
+    obj->Register<OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage>(
+      lock->GetOracleObservable(),
+      &OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent);
+
+    return obj;
+  }
+
+
+  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)
+  {
+    if (active_)
+    {
+      LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries(const std::string& seriesId): (active_)";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      active_ = true;
+
+      std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand);
+      command->SetUri("/series/" + seriesId + "/instances-tags");
+      {
+        std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock());
+        boost::shared_ptr<IObserver> observer(GetSharedObserver());
+        lock->Schedule(observer, 0, command.release()); //TODO: priority!
+      }
+    }
+  }
+  
+
+  OrthancStone::IVolumeSlicer::IExtractedSlice* 
+  OrthancSeriesVolumeProgressiveLoader::ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane)
+  {
+    if (volume_->HasGeometry())
+    {
+      return new ExtractedSlice(*this, cuttingPlane);
+    }
+    else
+    {
+      return new IVolumeSlicer::InvalidSlice;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Deprecated/Loaders/OrthancSeriesVolumeProgressiveLoader.h	Tue Mar 31 15:51:02 2020 +0200
@@ -0,0 +1,180 @@
+/**
+ * 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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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 "../Volumes/IGeometryProvider.h"
+
+
+#include <boost/shared_ptr.hpp>
+
+namespace OrthancStone
+{
+  class ILoadersContext;
+}
+
+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<OrthancSeriesVolumeProgressiveLoader>,
+    public OrthancStone::IObservable,
+    public OrthancStone::IVolumeSlicer,
+    public IGeometryProvider
+  {
+  private:
+    static const unsigned int QUALITY_00 = 0;
+    static const unsigned int QUALITY_01 = 1;
+    static const unsigned int QUALITY_02 = 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::unique_ptr<OrthancStone::VolumeImageGeometry>     geometry_;
+      std::vector<OrthancStone::DicomInstanceParameters*>  slices_;
+      std::vector<uint64_t>                  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::ILoadersContext&  loadersContext_;
+    bool                                          active_;
+    bool                                          progressiveQuality_;
+    unsigned int                                  simultaneousDownloads_;
+    SeriesGeometry                                seriesGeometry_;
+    boost::shared_ptr<OrthancStone::DicomVolumeImage> volume_;
+    std::unique_ptr<OrthancStone::IFetchingItemsSorter::IFactory> sorter_;
+    std::unique_ptr<OrthancStone::IFetchingStrategy> strategy_;
+    std::vector<unsigned int>     slicesQuality_;
+    bool                          volumeImageReadyInHighQuality_;
+    
+    OrthancSeriesVolumeProgressiveLoader(
+      OrthancStone::ILoadersContext& loadersContext,
+      boost::shared_ptr<OrthancStone::DicomVolumeImage> volume,
+      bool progressiveQuality);
+  
+  public:
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, VolumeImageReadyInHighQuality, OrthancSeriesVolumeProgressiveLoader);
+
+    /**
+    See doc for the progressiveQuality_ field
+    */
+    static boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> Create(
+      OrthancStone::ILoadersContext& context,
+      boost::shared_ptr<OrthancStone::DicomVolumeImage> volume,
+      bool progressiveQuality = false);
+
+    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 OrthancStone::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 OrthancStone::CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Deprecated/Messages/LockingEmitter.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+#include "LockingEmitter.h"
+
+#include <Core/OrthancException.h>
+
+namespace Deprecated
+{
+  void LockingEmitter::EmitMessage(boost::weak_ptr<OrthancStone::IObserver> observer,
+                                   const OrthancStone::IMessage& message)
+  {
+    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();
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Deprecated/Messages/LockingEmitter.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+#include <Core/Enumerations.h>
+#include <Core/OrthancException.h>
+
+#include "../../Messages/IMessageEmitter.h"
+#include "../../Messages/IObservable.h"
+
+#include <Core/Enumerations.h>  // For ORTHANC_OVERRIDE
+
+#include <boost/thread/shared_mutex.hpp>
+
+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<OrthancStone::IObserver> observer,
+                             const OrthancStone::IMessage& message) ORTHANC_OVERRIDE;
+
+
+    class ReaderLock : public boost::noncopyable
+    {
+    private:
+      LockingEmitter& that_;
+      boost::shared_lock<boost::shared_mutex>  lock_;
+
+    public:
+      ReaderLock(LockingEmitter& that) :
+        that_(that),
+        lock_(that.mutex_)
+      {
+      }
+    };
+
+
+    class WriterLock : public boost::noncopyable
+    {
+    private:
+      LockingEmitter& that_;
+      boost::unique_lock<boost::shared_mutex>  lock_;
+
+    public:
+      WriterLock(LockingEmitter& that) :
+        that_(that),
+        lock_(that.mutex_)
+      {
+      }
+
+      OrthancStone::IObservable& GetOracleObservable()
+      {
+        return that_.oracleObservable_;
+      }
+    };
+  };
+}
--- a/Framework/Deprecated/SmartLoader.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Deprecated/SmartLoader.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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> 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::unique_ptr<IVolumeSlicer> layerSource;
+    boost::shared_ptr<IVolumeSlicer> layerSource;
     std::string sliceKeyId = instanceId + ":" + boost::lexical_cast<std::string>(frame);
     SmartLoader::CachedSlice* cachedSlice = NULL;
 
@@ -151,22 +142,23 @@
     }
     else
     {
-      layerSource.reset(new DicomSeriesVolumeSlicer(IObserver::GetBroker(), orthancApiClient_));
+      layerSource.reset(new DicomSeriesVolumeSlicer);
+      dynamic_cast<DicomSeriesVolumeSlicer*>(layerSource.get())->Connect(orthancApiClient_);
       dynamic_cast<DicomSeriesVolumeSlicer*>(layerSource.get())->SetImageQuality(imageQuality_);
-      layerSource->RegisterObserverCallback(new OrthancStone::Callable<SmartLoader, IVolumeSlicer::GeometryReadyMessage>(*this, &SmartLoader::OnLayerGeometryReady));
-      layerSource->RegisterObserverCallback(new OrthancStone::Callable<SmartLoader, DicomSeriesVolumeSlicer::FrameReadyMessage>(*this, &SmartLoader::OnFrameReady));
-      layerSource->RegisterObserverCallback(new OrthancStone::Callable<SmartLoader, IVolumeSlicer::LayerReadyMessage>(*this, &SmartLoader::OnLayerReady));
+      Register<IVolumeSlicer::GeometryReadyMessage>(*layerSource, &SmartLoader::OnLayerGeometryReady);
+      Register<DicomSeriesVolumeSlicer::FrameReadyMessage>(*layerSource, &SmartLoader::OnFrameReady);
+      Register<IVolumeSlicer::LayerReadyMessage>(*layerSource, &SmartLoader::OnLayerReady);
       dynamic_cast<DicomSeriesVolumeSlicer*>(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> cachedSlice(new CachedSlice(IObserver::GetBroker()));
+    boost::shared_ptr<CachedSlice> cachedSlice(new CachedSlice);
     cachedSlice->slice_.reset(new Slice(instanceId, frame));
     cachedSlice->status_ = CachedSliceStatus_ScheduledToLoad;
     std::string sliceKeyId = instanceId + ":" + boost::lexical_cast<std::string>(frame);
@@ -199,12 +191,12 @@
 
     cachedSlices_[sliceKeyId] = boost::shared_ptr<CachedSlice>(cachedSlice);
 
-    std::unique_ptr<IVolumeSlicer> layerSource(new DicomSeriesVolumeSlicer(IObserver::GetBroker(), orthancApiClient_));
-
+    std::unique_ptr<IVolumeSlicer> layerSource(new DicomSeriesVolumeSlicer);
+    dynamic_cast<DicomSeriesVolumeSlicer*>(layerSource.get())->Connect(orthancApiClient_);
     dynamic_cast<DicomSeriesVolumeSlicer*>(layerSource.get())->SetImageQuality(imageQuality_);
-    layerSource->RegisterObserverCallback(new OrthancStone::Callable<SmartLoader, IVolumeSlicer::GeometryReadyMessage>(*this, &SmartLoader::OnLayerGeometryReady));
-    layerSource->RegisterObserverCallback(new OrthancStone::Callable<SmartLoader, DicomSeriesVolumeSlicer::FrameReadyMessage>(*this, &SmartLoader::OnFrameReady));
-    layerSource->RegisterObserverCallback(new OrthancStone::Callable<SmartLoader, IVolumeSlicer::LayerReadyMessage>(*this, &SmartLoader::OnLayerReady));
+    Register<IVolumeSlicer::GeometryReadyMessage>(*layerSource, &SmartLoader::OnLayerGeometryReady);
+    Register<DicomSeriesVolumeSlicer::FrameReadyMessage>(*layerSource, &SmartLoader::OnFrameReady);
+    Register<IVolumeSlicer::LayerReadyMessage>(*layerSource, &SmartLoader::OnLayerReady);
     dynamic_cast<DicomSeriesVolumeSlicer*>(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> cachedSlice(new CachedSlice(IObserver::GetBroker()));
+    boost::shared_ptr<CachedSlice> 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> cachedSlice(new CachedSlice(IObserver::GetBroker()));
+    boost::shared_ptr<CachedSlice> cachedSlice(new CachedSlice);
     cachedSlice->image_.reset(Orthanc::Image::Clone(message.GetFrame()));
     cachedSlice->effectiveQuality_ = message.GetImageQuality();
     cachedSlice->slice_.reset(message.GetSlice().Clone());
--- a/Framework/Deprecated/SmartLoader.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Deprecated/SmartLoader.h	Tue Mar 31 15:51:02 2020 +0200
@@ -30,7 +30,7 @@
 {
   class SliceViewerWidget;
 
-  class SmartLoader : public OrthancStone::IObservable, public OrthancStone::IObserver
+  class SmartLoader : public OrthancStone::IObservable, public OrthancStone::ObserverBase<SmartLoader>
   {
     class CachedSlice;
 
@@ -42,10 +42,10 @@
     PreloadingInstances preloadingInstances_;
 
     SliceImageQuality     imageQuality_;
-    OrthancApiClient&     orthancApiClient_;
+    boost::shared_ptr<OrthancApiClient>  orthancApiClient_;
 
   public:
-    SmartLoader(OrthancStone::MessageBroker& broker, OrthancApiClient& orthancApiClient);  // TODO: add maxPreloadStorageSizeInBytes
+    SmartLoader(boost::shared_ptr<OrthancApiClient> orthancApiClient);  // TODO: add maxPreloadStorageSizeInBytes
 
 //    void PreloadStudy(const std::string studyId);
 //    void PreloadSeries(const std::string seriesId);
--- a/Framework/Deprecated/Toolbox/BaseWebService.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Deprecated/Toolbox/BaseWebService.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -37,13 +37,13 @@
   class BaseWebService::BaseWebServicePayload : public Orthanc::IDynamicObject
   {
   private:
-    std::unique_ptr< OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage> >   userSuccessHandler_;
-    std::unique_ptr< OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage> >     userFailureHandler_;
+    std::unique_ptr< MessageHandler<IWebService::HttpRequestSuccessMessage> >   userSuccessHandler_;
+    std::unique_ptr< MessageHandler<IWebService::HttpRequestErrorMessage> >     userFailureHandler_;
     std::unique_ptr< Orthanc::IDynamicObject>                                   userPayload_;
 
   public:
-    BaseWebServicePayload(OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* userSuccessHandler,
-                          OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* userFailureHandler,
+    BaseWebServicePayload(MessageHandler<IWebService::HttpRequestSuccessMessage>* userSuccessHandler,
+                          MessageHandler<IWebService::HttpRequestErrorMessage>* 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<IWebService::HttpRequestSuccessMessage>* successCallback,
-                                OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
+                                MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,
+                                MessageHandler<IWebService::HttpRequestErrorMessage>* 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<BaseWebService, IWebService::HttpRequestSuccessMessage>
-                       (*this, &BaseWebService::CacheAndNotifyHttpSuccess),
-                       new OrthancStone::Callable<BaseWebService, IWebService::HttpRequestErrorMessage>
-                       (*this, &BaseWebService::NotifyHttpError),
+                       new DeprecatedCallable<BaseWebService, IWebService::HttpRequestSuccessMessage>
+                       (GetSharedObserver(), &BaseWebService::CacheAndNotifyHttpSuccess),
+                       new DeprecatedCallable<BaseWebService, IWebService::HttpRequestErrorMessage>
+                       (GetSharedObserver(), &BaseWebService::NotifyHttpError),
                        timeoutInSeconds);
     }
     else
--- a/Framework/Deprecated/Toolbox/BaseWebService.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Deprecated/Toolbox/BaseWebService.h	Tue Mar 31 15:51:02 2020 +0200
@@ -22,6 +22,7 @@
 #pragma once
 
 #include "IWebService.h"
+#include "../../Messages/ObserverBase.h"
 
 #include <string>
 #include <map>
@@ -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<BaseWebService>
   {
   public:
     class CachedHttpRequestSuccessMessage
@@ -90,10 +91,7 @@
     std::deque<std::string> 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<IWebService::HttpRequestSuccessMessage>* successCallback,
-                          OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,
+                          MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,
+                          MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,
                           unsigned int timeoutInSeconds = 60);
 
   protected:
     virtual void GetAsyncInternal(const std::string& uri,
                           const HttpHeaders& headers,
                           Orthanc::IDynamicObject* payload  /* takes ownership */,
-                          OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,
-                          OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,
+                          MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,
+                          MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,
                           unsigned int timeoutInSeconds = 60) = 0;
 
     virtual void NotifyHttpSuccessLater(boost::shared_ptr<BaseWebService::CachedHttpRequestSuccessMessage> cachedHttpMessage,
                                         Orthanc::IDynamicObject* payload, // takes ownership
-                                        OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback) = 0;
+                                        MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback) = 0;
 
   private:
     void NotifyHttpSuccess(const IWebService::HttpRequestSuccessMessage& message);
--- a/Framework/Deprecated/Toolbox/IDelayedCallExecutor.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Deprecated/Toolbox/IDelayedCallExecutor.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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<IDelayedCallExecutor::TimeoutMessage>* callback,
+    virtual void Schedule(MessageHandler<IDelayedCallExecutor::TimeoutMessage>* callback,
                           unsigned int timeoutInMs = 1000) = 0;
   };
 }
--- a/Framework/Deprecated/Toolbox/IWebService.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Deprecated/Toolbox/IWebService.h	Tue Mar 31 15:51:02 2020 +0200
@@ -33,6 +33,53 @@
 
 namespace Deprecated
 {
+  template <typename TMessage>
+  class MessageHandler : public OrthancStone::ICallable
+  {
+  };
+
+
+  template <typename TObserver,
+            typename TMessage>
+  class DeprecatedCallable : public MessageHandler<TMessage>
+  {
+  private:
+    typedef void (TObserver::* MemberMethod) (const TMessage&);
+
+    boost::weak_ptr<OrthancStone::IObserver>  observer_;
+    MemberMethod                function_;
+
+  public:
+    DeprecatedCallable(boost::shared_ptr<TObserver> observer,
+                       MemberMethod function) :
+      observer_(observer),
+      function_(function)
+    {
+    }
+
+    virtual void Apply(const OrthancStone::IMessage& message)
+    {
+      boost::shared_ptr<OrthancStone::IObserver> lock(observer_);
+      if (lock)
+      {
+        TObserver& observer = dynamic_cast<TObserver&>(*lock);
+        const TMessage& typedMessage = dynamic_cast<const TMessage&>(message);
+        (observer.*function_) (typedMessage);
+      }
+    }
+
+    virtual const OrthancStone::MessageIdentifier& GetMessageIdentifier()
+    {
+      return TMessage::GetStaticIdentifier();
+    }
+
+    virtual boost::weak_ptr<OrthancStone::IObserver> 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<std::string, std::string> 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<IWebService::HttpRequestSuccessMessage>* successCallback,
-                          OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,
+                          MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,
+                          MessageHandler<IWebService::HttpRequestErrorMessage>* 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<IWebService::HttpRequestSuccessMessage>* successCallback,
-                           OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,
+                           MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,
+                           MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,
                            unsigned int timeoutInSeconds = 60) = 0;
 
     virtual void DeleteAsync(const std::string& uri,
                              const HttpHeaders& headers,
                              Orthanc::IDynamicObject* payload  /* takes ownership */,
-                             OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,
-                             OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,
+                             MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,
+                             MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,
                              unsigned int timeoutInSeconds = 60) = 0;
   };
 }
--- a/Framework/Deprecated/Toolbox/MessagingToolbox.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Deprecated/Toolbox/MessagingToolbox.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -18,7 +18,6 @@
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  **/
 
-
 #include "MessagingToolbox.h"
 
 #include <Core/Images/Image.h>
@@ -30,9 +29,16 @@
 #include <Core/Logging.h>
 
 #include <boost/lexical_cast.hpp>
+
+#ifdef _MSC_VER
+// 'Json::Reader': Use CharReader and CharReaderBuilder instead
+#pragma warning(disable:4996)
+#endif
+
 #include <json/reader.h>
 #include <json/writer.h>
 
+
 namespace Deprecated
 {
   namespace MessagingToolbox
--- a/Framework/Deprecated/Toolbox/OrthancApiClient.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Deprecated/Toolbox/OrthancApiClient.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -68,12 +68,12 @@
   class OrthancApiClient::WebServicePayload : public Orthanc::IDynamicObject
   {
   private:
-    std::unique_ptr< OrthancStone::MessageHandler<EmptyResponseReadyMessage> >             emptyHandler_;
-    std::unique_ptr< OrthancStone::MessageHandler<JsonResponseReadyMessage> >              jsonHandler_;
-    std::unique_ptr< OrthancStone::MessageHandler<BinaryResponseReadyMessage> >            binaryHandler_;
-    std::unique_ptr< OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage> >  failureHandler_;
+    std::unique_ptr< MessageHandler<EmptyResponseReadyMessage> >             emptyHandler_;
+    std::unique_ptr< MessageHandler<JsonResponseReadyMessage> >              jsonHandler_;
+    std::unique_ptr< MessageHandler<BinaryResponseReadyMessage> >            binaryHandler_;
+    std::unique_ptr< MessageHandler<IWebService::HttpRequestErrorMessage> >  failureHandler_;
     std::unique_ptr< Orthanc::IDynamicObject >                               userPayload_;
-    OrthancStone::MessageBroker&                                                         broker_;
+
     void NotifyConversionError(const IWebService::HttpRequestSuccessMessage& message) const
     {
       if (failureHandler_.get() != NULL)
@@ -84,14 +84,12 @@
     }
     
   public:
-    WebServicePayload(OrthancStone::MessageBroker& broker,
-                      OrthancStone::MessageHandler<EmptyResponseReadyMessage>* handler,
-                      OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureHandler,
+    WebServicePayload(MessageHandler<EmptyResponseReadyMessage>* handler,
+                      MessageHandler<IWebService::HttpRequestErrorMessage>* failureHandler,
                       Orthanc::IDynamicObject* userPayload) :
       emptyHandler_(handler),
       failureHandler_(failureHandler),
-      userPayload_(userPayload),
-      broker_(broker)
+      userPayload_(userPayload)
 
     {
       if (handler == NULL)
@@ -100,14 +98,12 @@
       }
     }
 
-    WebServicePayload(OrthancStone::MessageBroker& broker,
-                      OrthancStone::MessageHandler<BinaryResponseReadyMessage>* handler,
-                      OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureHandler,
+    WebServicePayload(MessageHandler<BinaryResponseReadyMessage>* handler,
+                      MessageHandler<IWebService::HttpRequestErrorMessage>* failureHandler,
                       Orthanc::IDynamicObject* userPayload) :
       binaryHandler_(handler),
       failureHandler_(failureHandler),
-      userPayload_(userPayload),
-      broker_(broker)
+      userPayload_(userPayload)
     {
       if (handler == NULL)
       {
@@ -115,14 +111,12 @@
       }
     }
 
-    WebServicePayload(OrthancStone::MessageBroker& broker,
-                      OrthancStone::MessageHandler<JsonResponseReadyMessage>* handler,
-                      OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureHandler,
+    WebServicePayload(MessageHandler<JsonResponseReadyMessage>* handler,
+                      MessageHandler<IWebService::HttpRequestErrorMessage>* failureHandler,
                       Orthanc::IDynamicObject* userPayload) :
       jsonHandler_(handler),
       failureHandler_(failureHandler),
-      userPayload_(userPayload),
-      broker_(broker)
+      userPayload_(userPayload)
     {
       if (handler == NULL)
       {
@@ -134,35 +128,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 +167,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 +177,26 @@
 
   void OrthancApiClient::GetJsonAsync(
       const std::string& uri,
-      OrthancStone::MessageHandler<JsonResponseReadyMessage>* successCallback,
-      OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
+      MessageHandler<JsonResponseReadyMessage>* successCallback,
+      MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
       Orthanc::IDynamicObject* payload)
   {
     IWebService::HttpHeaders emptyHeaders;
     web_.GetAsync(baseUrl_ + uri,
                   emptyHeaders,
-                  new WebServicePayload(IObservable::GetBroker(), successCallback, failureCallback, payload),
-                  new OrthancStone::Callable<OrthancApiClient, IWebService::HttpRequestSuccessMessage>
-                  (*this, &OrthancApiClient::NotifyHttpSuccess),
-                  new OrthancStone::Callable<OrthancApiClient, IWebService::HttpRequestErrorMessage>
-                  (*this, &OrthancApiClient::NotifyHttpError));
+                  new WebServicePayload(successCallback, failureCallback, payload),
+                  new DeprecatedCallable<OrthancApiClient, IWebService::HttpRequestSuccessMessage>
+                  (GetSharedObserver(), &OrthancApiClient::NotifyHttpSuccess),
+                  new DeprecatedCallable<OrthancApiClient, IWebService::HttpRequestErrorMessage>
+                  (GetSharedObserver(), &OrthancApiClient::NotifyHttpError));
   }
 
 
   void OrthancApiClient::GetBinaryAsync(
       const std::string& uri,
       const std::string& contentType,
-      OrthancStone::MessageHandler<BinaryResponseReadyMessage>* successCallback,
-      OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
+      MessageHandler<BinaryResponseReadyMessage>* successCallback,
+      MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
       Orthanc::IDynamicObject* payload)
   {
     IWebService::HttpHeaders headers;
@@ -225,34 +207,34 @@
   void OrthancApiClient::GetBinaryAsync(
       const std::string& uri,
       const IWebService::HttpHeaders& headers,
-      OrthancStone::MessageHandler<BinaryResponseReadyMessage>* successCallback,
-      OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
+      MessageHandler<BinaryResponseReadyMessage>* successCallback,
+      MessageHandler<IWebService::HttpRequestErrorMessage>* 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<OrthancApiClient, IWebService::HttpRequestSuccessMessage>
-                  (*this, &OrthancApiClient::NotifyHttpSuccess),
-                  new OrthancStone::Callable<OrthancApiClient, IWebService::HttpRequestErrorMessage>
-                  (*this, &OrthancApiClient::NotifyHttpError));
+                  new WebServicePayload(successCallback, failureCallback, payload),
+                  new DeprecatedCallable<OrthancApiClient, IWebService::HttpRequestSuccessMessage>
+                  (GetSharedObserver(), &OrthancApiClient::NotifyHttpSuccess),
+                  new DeprecatedCallable<OrthancApiClient, IWebService::HttpRequestErrorMessage>
+                  (GetSharedObserver(), &OrthancApiClient::NotifyHttpError));
   }
 
   
   void OrthancApiClient::PostBinaryAsyncExpectJson(
       const std::string& uri,
       const std::string& body,
-      OrthancStone::MessageHandler<JsonResponseReadyMessage>* successCallback,
-      OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
+      MessageHandler<JsonResponseReadyMessage>* successCallback,
+      MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
       Orthanc::IDynamicObject* payload)
   {
     web_.PostAsync(baseUrl_ + uri, IWebService::HttpHeaders(), body,
-                   new WebServicePayload(IObservable::GetBroker(), successCallback, failureCallback, payload),
-                   new OrthancStone::Callable<OrthancApiClient, IWebService::HttpRequestSuccessMessage>
-                   (*this, &OrthancApiClient::NotifyHttpSuccess),
-                   new OrthancStone::Callable<OrthancApiClient, IWebService::HttpRequestErrorMessage>
-                   (*this, &OrthancApiClient::NotifyHttpError));
+                   new WebServicePayload(successCallback, failureCallback, payload),
+                   new DeprecatedCallable<OrthancApiClient, IWebService::HttpRequestSuccessMessage>
+                   (GetSharedObserver(), &OrthancApiClient::NotifyHttpSuccess),
+                   new DeprecatedCallable<OrthancApiClient, IWebService::HttpRequestErrorMessage>
+                   (GetSharedObserver(), &OrthancApiClient::NotifyHttpError));
 
   }
 
@@ -266,23 +248,23 @@
   void OrthancApiClient::PostBinaryAsync(
       const std::string& uri,
       const std::string& body,
-      OrthancStone::MessageHandler<EmptyResponseReadyMessage>* successCallback,
-      OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
+      MessageHandler<EmptyResponseReadyMessage>* successCallback,
+      MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
       Orthanc::IDynamicObject* payload   /* takes ownership */)
   {
     web_.PostAsync(baseUrl_ + uri, IWebService::HttpHeaders(), body,
-                   new WebServicePayload(IObservable::GetBroker(), successCallback, failureCallback, payload),
-                   new OrthancStone::Callable<OrthancApiClient, IWebService::HttpRequestSuccessMessage>
-                   (*this, &OrthancApiClient::NotifyHttpSuccess),
-                   new OrthancStone::Callable<OrthancApiClient, IWebService::HttpRequestErrorMessage>
-                   (*this, &OrthancApiClient::NotifyHttpError));
+                   new WebServicePayload(successCallback, failureCallback, payload),
+                   new DeprecatedCallable<OrthancApiClient, IWebService::HttpRequestSuccessMessage>
+                   (GetSharedObserver(), &OrthancApiClient::NotifyHttpSuccess),
+                   new DeprecatedCallable<OrthancApiClient, IWebService::HttpRequestErrorMessage>
+                   (GetSharedObserver(), &OrthancApiClient::NotifyHttpError));
   }
 
   void OrthancApiClient::PostJsonAsyncExpectJson(
       const std::string& uri,
       const Json::Value& data,
-      OrthancStone::MessageHandler<JsonResponseReadyMessage>* successCallback,
-      OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
+      MessageHandler<JsonResponseReadyMessage>* successCallback,
+      MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
       Orthanc::IDynamicObject* payload)
   {
     std::string body;
@@ -302,8 +284,8 @@
   void OrthancApiClient::PostJsonAsync(
       const std::string& uri,
       const Json::Value& data,
-      OrthancStone::MessageHandler<EmptyResponseReadyMessage>* successCallback,
-      OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
+      MessageHandler<EmptyResponseReadyMessage>* successCallback,
+      MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
       Orthanc::IDynamicObject* payload   /* takes ownership */)
   {
     std::string body;
@@ -313,16 +295,16 @@
 
   void OrthancApiClient::DeleteAsync(
       const std::string& uri,
-      OrthancStone::MessageHandler<EmptyResponseReadyMessage>* successCallback,
-      OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
+      MessageHandler<EmptyResponseReadyMessage>* successCallback,
+      MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
       Orthanc::IDynamicObject* payload)
   {
     web_.DeleteAsync(baseUrl_ + uri, IWebService::HttpHeaders(),
-                     new WebServicePayload(IObservable::GetBroker(), successCallback, failureCallback, payload),
-                     new OrthancStone::Callable<OrthancApiClient, IWebService::HttpRequestSuccessMessage>
-                     (*this, &OrthancApiClient::NotifyHttpSuccess),
-                     new OrthancStone::Callable<OrthancApiClient, IWebService::HttpRequestErrorMessage>
-                     (*this, &OrthancApiClient::NotifyHttpError));
+                     new WebServicePayload(successCallback, failureCallback, payload),
+                     new DeprecatedCallable<OrthancApiClient, IWebService::HttpRequestSuccessMessage>
+                     (GetSharedObserver(), &OrthancApiClient::NotifyHttpSuccess),
+                     new DeprecatedCallable<OrthancApiClient, IWebService::HttpRequestErrorMessage>
+                     (GetSharedObserver(), &OrthancApiClient::NotifyHttpError));
   }
 
 
--- a/Framework/Deprecated/Toolbox/OrthancApiClient.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Deprecated/Toolbox/OrthancApiClient.h	Tue Mar 31 15:51:02 2020 +0200
@@ -25,13 +25,24 @@
 #include <json/json.h>
 
 #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<OrthancApiClient>
   {
   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<JsonResponseReadyMessage>* successCallback,
-                      OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,
+                      MessageHandler<JsonResponseReadyMessage>* successCallback,
+                      MessageHandler<IWebService::HttpRequestErrorMessage>* 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<BinaryResponseReadyMessage>* successCallback,
-                        OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,
+                        MessageHandler<BinaryResponseReadyMessage>* successCallback,
+                        MessageHandler<IWebService::HttpRequestErrorMessage>* 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<BinaryResponseReadyMessage>* successCallback,
-                        OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,
+                        MessageHandler<BinaryResponseReadyMessage>* successCallback,
+                        MessageHandler<IWebService::HttpRequestErrorMessage>* 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<JsonResponseReadyMessage>* successCallback,
-                                   OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,
+                                   MessageHandler<JsonResponseReadyMessage>* successCallback,
+                                   MessageHandler<IWebService::HttpRequestErrorMessage>* 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<JsonResponseReadyMessage>* successCallback,
-                                 OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,
+                                 MessageHandler<JsonResponseReadyMessage>* successCallback,
+                                 MessageHandler<IWebService::HttpRequestErrorMessage>* 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<EmptyResponseReadyMessage>* successCallback,
-                       OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,
+                       MessageHandler<EmptyResponseReadyMessage>* successCallback,
+                       MessageHandler<IWebService::HttpRequestErrorMessage>* 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<EmptyResponseReadyMessage>* successCallback,
-                         OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,
+                         MessageHandler<EmptyResponseReadyMessage>* successCallback,
+                         MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,
                          Orthanc::IDynamicObject* payload = NULL   /* takes ownership */);
 
     // schedule a DELETE request expecting an empty response.
     void DeleteAsync(const std::string& uri,
-                     OrthancStone::MessageHandler<EmptyResponseReadyMessage>* successCallback,
-                     OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,
+                     MessageHandler<EmptyResponseReadyMessage>* successCallback,
+                     MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,
                      Orthanc::IDynamicObject* payload = NULL   /* takes ownership */);
 
     void NotifyHttpSuccess(const IWebService::HttpRequestSuccessMessage& message);
--- a/Framework/Deprecated/Toolbox/OrthancSlicesLoader.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Deprecated/Toolbox/OrthancSlicesLoader.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -639,10 +639,7 @@
   }
   
   
-  OrthancSlicesLoader::OrthancSlicesLoader(OrthancStone::MessageBroker& broker,
-                                           OrthancApiClient& orthanc) :
-    OrthancStone::IObservable(broker),
-    OrthancStone::IObserver(broker),
+  OrthancSlicesLoader::OrthancSlicesLoader(boost::shared_ptr<OrthancApiClient> orthanc) :
     orthanc_(orthanc),
     state_(State_Initialization)
   {
@@ -658,10 +655,10 @@
     else
     {
       state_ = State_LoadingGeometry;
-      orthanc_.GetJsonAsync("/series/" + seriesId + "/instances-tags",
-                            new OrthancStone::Callable<OrthancSlicesLoader, OrthancApiClient::JsonResponseReadyMessage>(*this, &OrthancSlicesLoader::ParseSeriesGeometry),
-                            new OrthancStone::Callable<OrthancSlicesLoader, IWebService::HttpRequestErrorMessage>(*this, &OrthancSlicesLoader::OnGeometryError),
-                            NULL);
+      orthanc_->GetJsonAsync("/series/" + seriesId + "/instances-tags",
+                             new DeprecatedCallable<OrthancSlicesLoader, OrthancApiClient::JsonResponseReadyMessage>(GetSharedObserver(), &OrthancSlicesLoader::ParseSeriesGeometry),
+                             new DeprecatedCallable<OrthancSlicesLoader, IWebService::HttpRequestErrorMessage>(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<OrthancSlicesLoader, OrthancApiClient::JsonResponseReadyMessage>(*this, &OrthancSlicesLoader::ParseInstanceGeometry),
-                            new OrthancStone::Callable<OrthancSlicesLoader, IWebService::HttpRequestErrorMessage>(*this, &OrthancSlicesLoader::OnGeometryError),
-                            Operation::DownloadInstanceGeometry(instanceId));
+      orthanc_->GetJsonAsync("/instances/" + instanceId + "/tags?ignore-length=3004-000c",
+                             new DeprecatedCallable<OrthancSlicesLoader, OrthancApiClient::JsonResponseReadyMessage>(GetSharedObserver(), &OrthancSlicesLoader::ParseInstanceGeometry),
+                             new DeprecatedCallable<OrthancSlicesLoader, IWebService::HttpRequestErrorMessage>(GetSharedObserver(), &OrthancSlicesLoader::OnGeometryError),
+                             Operation::DownloadInstanceGeometry(instanceId));
     }
   }
   
@@ -696,10 +693,10 @@
     {
       state_ = State_LoadingGeometry;
 
-      orthanc_.GetJsonAsync("/instances/" + instanceId + "/tags",
-                            new OrthancStone::Callable<OrthancSlicesLoader, OrthancApiClient::JsonResponseReadyMessage>(*this, &OrthancSlicesLoader::ParseFrameGeometry),
-                            new OrthancStone::Callable<OrthancSlicesLoader, IWebService::HttpRequestErrorMessage>(*this, &OrthancSlicesLoader::OnGeometryError),
-                            Operation::DownloadFrameGeometry(instanceId, frame));
+      orthanc_->GetJsonAsync("/instances/" + instanceId + "/tags",
+                             new DeprecatedCallable<OrthancSlicesLoader, OrthancApiClient::JsonResponseReadyMessage>(GetSharedObserver(), &OrthancSlicesLoader::ParseFrameGeometry),
+                             new DeprecatedCallable<OrthancSlicesLoader, IWebService::HttpRequestErrorMessage>(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<OrthancSlicesLoader, 
-        OrthancApiClient::BinaryResponseReadyMessage>
-          (*this, &OrthancSlicesLoader::ParseSliceImagePng),
-      new OrthancStone::Callable<OrthancSlicesLoader, 
-        IWebService::HttpRequestErrorMessage>
-          (*this, &OrthancSlicesLoader::OnSliceImageError),
-      Operation::DownloadSliceImage(
-        static_cast<unsigned int>(index), slice, SliceImageQuality_FullPng));
-}
+    orthanc_->GetBinaryAsync(uri, "image/png",
+                             new DeprecatedCallable<OrthancSlicesLoader, 
+                             OrthancApiClient::BinaryResponseReadyMessage>
+                             (GetSharedObserver(), &OrthancSlicesLoader::ParseSliceImagePng),
+                             new DeprecatedCallable<OrthancSlicesLoader, 
+                             IWebService::HttpRequestErrorMessage>
+                             (GetSharedObserver(), &OrthancSlicesLoader::OnSliceImageError),
+                             Operation::DownloadSliceImage(
+                               static_cast<unsigned int>(index), slice, SliceImageQuality_FullPng));
+  }
   
   void OrthancSlicesLoader::ScheduleSliceImagePam(const Slice& slice,
                                                   size_t index)
   {
     std::string uri = 
       ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" +
-      boost::lexical_cast<std::string>(slice.GetFrame()));
+       boost::lexical_cast<std::string>(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<OrthancSlicesLoader, 
-        OrthancApiClient::BinaryResponseReadyMessage>
-          (*this, &OrthancSlicesLoader::ParseSliceImagePam),
-      new OrthancStone::Callable<OrthancSlicesLoader, 
-        IWebService::HttpRequestErrorMessage>
-          (*this, &OrthancSlicesLoader::OnSliceImageError),
-      Operation::DownloadSliceImage(static_cast<unsigned int>(index), 
-                                    slice, SliceImageQuality_FullPam));
+    orthanc_->GetBinaryAsync(uri, "image/x-portable-arbitrarymap",
+                             new DeprecatedCallable<OrthancSlicesLoader, 
+                             OrthancApiClient::BinaryResponseReadyMessage>
+                             (GetSharedObserver(), &OrthancSlicesLoader::ParseSliceImagePam),
+                             new DeprecatedCallable<OrthancSlicesLoader, 
+                             IWebService::HttpRequestErrorMessage>
+                             (GetSharedObserver(), &OrthancSlicesLoader::OnSliceImageError),
+                             Operation::DownloadSliceImage(static_cast<unsigned int>(index), 
+                                                           slice, SliceImageQuality_FullPam));
   }
 
 
@@ -849,15 +846,15 @@
                        "-" + slice.GetOrthancInstanceId() + "_" +
                        boost::lexical_cast<std::string>(slice.GetFrame()));
 
-    orthanc_.GetJsonAsync(uri,
-      new OrthancStone::Callable<OrthancSlicesLoader, 
-        OrthancApiClient::JsonResponseReadyMessage>
-          (*this, &OrthancSlicesLoader::ParseSliceImageJpeg),
-      new OrthancStone::Callable<OrthancSlicesLoader, 
-        IWebService::HttpRequestErrorMessage>
-          (*this, &OrthancSlicesLoader::OnSliceImageError),
-        Operation::DownloadSliceImage(
-          static_cast<unsigned int>(index), slice, quality));
+    orthanc_->GetJsonAsync(uri,
+                           new DeprecatedCallable<OrthancSlicesLoader, 
+                           OrthancApiClient::JsonResponseReadyMessage>
+                           (GetSharedObserver(), &OrthancSlicesLoader::ParseSliceImageJpeg),
+                           new DeprecatedCallable<OrthancSlicesLoader, 
+                           IWebService::HttpRequestErrorMessage>
+                           (GetSharedObserver(), &OrthancSlicesLoader::OnSliceImageError),
+                           Operation::DownloadSliceImage(
+                             static_cast<unsigned int>(index), slice, quality));
   }
   
   
@@ -890,15 +887,15 @@
     {
       std::string uri = ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" +
                          boost::lexical_cast<std::string>(slice.GetFrame()) + "/raw.gz");
-      orthanc_.GetBinaryAsync(uri, IWebService::HttpHeaders(),
-        new OrthancStone::Callable<OrthancSlicesLoader, 
-          OrthancApiClient::BinaryResponseReadyMessage>
-            (*this, &OrthancSlicesLoader::ParseSliceRawImage),
-        new OrthancStone::Callable<OrthancSlicesLoader,
-          IWebService::HttpRequestErrorMessage>
-            (*this, &OrthancSlicesLoader::OnSliceImageError),
-        Operation::DownloadSliceRawImage(
-          static_cast<unsigned int>(index), slice));
+      orthanc_->GetBinaryAsync(uri, IWebService::HttpHeaders(),
+                               new DeprecatedCallable<OrthancSlicesLoader, 
+                               OrthancApiClient::BinaryResponseReadyMessage>
+                               (GetSharedObserver(), &OrthancSlicesLoader::ParseSliceRawImage),
+                               new DeprecatedCallable<OrthancSlicesLoader,
+                               IWebService::HttpRequestErrorMessage>
+                               (GetSharedObserver(), &OrthancSlicesLoader::OnSliceImageError),
+                               Operation::DownloadSliceRawImage(
+                                 static_cast<unsigned int>(index), slice));
     }
   }
 }
--- a/Framework/Deprecated/Toolbox/OrthancSlicesLoader.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Deprecated/Toolbox/OrthancSlicesLoader.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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<OrthancSlicesLoader>
   {
   public:
     ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, SliceGeometryReadyMessage, OrthancSlicesLoader);
@@ -143,7 +146,7 @@
 
     class Operation;
 
-    OrthancApiClient&  orthanc_;
+    boost::shared_ptr<OrthancApiClient>  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<OrthancApiClient> orthancApi);
 
     void ScheduleLoadSeries(const std::string& seriesId);
 
--- a/Framework/Deprecated/Viewport/IViewport.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Deprecated/Viewport/IViewport.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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;
--- a/Framework/Deprecated/Viewport/WidgetViewport.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Deprecated/Viewport/WidgetViewport.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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<IWidget> 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;
   }
 
 
--- a/Framework/Deprecated/Viewport/WidgetViewport.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Deprecated/Viewport/WidgetViewport.h	Tue Mar 31 15:51:02 2020 +0200
@@ -33,7 +33,7 @@
   class WidgetViewport : public IViewport
   {
   private:
-    std::unique_ptr<IWidget>        centralWidget_;
+    boost::shared_ptr<IWidget>    centralWidget_;
     IStatusBar*                   statusBar_;
     std::unique_ptr<IMouseTracker>  mouseTracker_;
     bool                          isMouseOver_;
@@ -43,13 +43,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<IWidget> widget);
 
     virtual void NotifyBackgroundChanged();
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Deprecated/Volumes/IGeometryProvider.h	Tue Mar 31 15:51:02 2020 +0200
@@ -0,0 +1,35 @@
+/**
+ * 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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../Volumes/VolumeImageGeometry.h"
+
+namespace Deprecated
+{
+  class IGeometryProvider
+  {
+  public:
+    virtual ~IGeometryProvider() {}
+    virtual bool HasGeometry() const = 0;
+    virtual const OrthancStone::VolumeImageGeometry& GetImageGeometry() const = 0;
+  };
+}
--- a/Framework/Deprecated/Volumes/ISlicedVolume.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Deprecated/Volumes/ISlicedVolume.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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;
--- a/Framework/Deprecated/Volumes/IVolumeLoader.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Deprecated/Volumes/IVolumeLoader.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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)
-    {
-    }
   };
 }
--- a/Framework/Deprecated/Volumes/StructureSetLoader.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Deprecated/Volumes/StructureSetLoader.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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<StructureSetLoader, OrthancApiClient::JsonResponseReadyMessage>(*this, &StructureSetLoader::OnLookupCompleted));
+                                         new DeprecatedCallable<StructureSetLoader, OrthancApiClient::JsonResponseReadyMessage>(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<StructureSetLoader, OrthancApiClient::JsonResponseReadyMessage>(*this, &StructureSetLoader::OnReferencedSliceLoaded));
+                          new DeprecatedCallable<StructureSetLoader, OrthancApiClient::JsonResponseReadyMessage>(GetSharedObserver(), &StructureSetLoader::OnReferencedSliceLoaded));
   }
 
   
@@ -97,7 +94,7 @@
     else
     {
       orthanc_.GetJsonAsync("/instances/" + instance + "/tags?ignore-length=3006-0050",
-                            new OrthancStone::Callable<StructureSetLoader, OrthancApiClient::JsonResponseReadyMessage>(*this, &StructureSetLoader::OnStructureSetLoaded));
+                            new DeprecatedCallable<StructureSetLoader, OrthancApiClient::JsonResponseReadyMessage>(GetSharedObserver(), &StructureSetLoader::OnStructureSetLoaded));
     }
   }
 
--- a/Framework/Deprecated/Volumes/StructureSetLoader.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Deprecated/Volumes/StructureSetLoader.h	Tue Mar 31 15:51:02 2020 +0200
@@ -21,6 +21,7 @@
 
 #pragma once
 
+#include "../../Messages/ObserverBase.h"
 #include "../../Toolbox/DicomStructureSet.h"
 #include "../Toolbox/OrthancApiClient.h"
 #include "IVolumeLoader.h"
@@ -31,7 +32,7 @@
 {
   class StructureSetLoader :
     public IVolumeLoader,
-    public OrthancStone::IObserver
+    public OrthancStone::ObserverBase<StructureSetLoader>
   {
   private:
     OrthancApiClient&                 orthanc_;
@@ -44,8 +45,7 @@
     void OnLookupCompleted(const OrthancApiClient::JsonResponseReadyMessage& message);
 
   public:
-    StructureSetLoader(OrthancStone::MessageBroker& broker,
-                       OrthancApiClient& orthanc);
+    StructureSetLoader(OrthancApiClient& orthanc);
 
     void ScheduleLoadInstance(const std::string& instance);
 
--- a/Framework/Deprecated/Widgets/LayoutWidget.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Deprecated/Widgets/LayoutWidget.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -85,14 +85,14 @@
   class LayoutWidget::ChildWidget : public boost::noncopyable
   {
   private:
-    std::unique_ptr<IWidget>  widget_;
+    boost::shared_ptr<IWidget>  widget_;
     int                     left_;
     int                     top_;
     unsigned int            width_;
     unsigned int            height_;
 
   public:
-    ChildWidget(IWidget* widget) :
+    ChildWidget(boost::shared_ptr<IWidget> widget) :
       widget_(widget)
     {
       assert(widget != NULL);
@@ -354,7 +354,7 @@
   }
 
 
-  IWidget& LayoutWidget::AddWidget(IWidget* widget)  // Takes ownership
+  void LayoutWidget::AddWidget(boost::shared_ptr<IWidget> widget)  // Takes ownership
   {
     if (widget == NULL)
     {
@@ -375,8 +375,6 @@
     {
       hasAnimation_ = true;
     }
-
-    return *widget;
   }
 
 
--- a/Framework/Deprecated/Widgets/LayoutWidget.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Deprecated/Widgets/LayoutWidget.h	Tue Mar 31 15:51:02 2020 +0200
@@ -94,7 +94,7 @@
       return paddingInternal_;
     }
 
-    IWidget& AddWidget(IWidget* widget);  // Takes ownership
+    void AddWidget(boost::shared_ptr<IWidget> widget);
 
     virtual void SetStatusBar(IStatusBar& statusBar);
 
--- a/Framework/Deprecated/Widgets/SliceViewerWidget.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Deprecated/Widgets/SliceViewerWidget.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -35,206 +35,174 @@
 
 namespace Deprecated
 {
-  class SliceViewerWidget::Scene : public boost::noncopyable
+  void SliceViewerWidget::Scene::DeleteLayer(size_t index)
+  {
+    if (index >= renderers_.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    assert(countMissing_ <= renderers_.size());
+
+    if (renderers_[index] != NULL)
+    {
+      assert(countMissing_ < renderers_.size());
+      delete renderers_[index];
+      renderers_[index] = NULL;
+      countMissing_++;
+    }
+  }
+
+  
+  SliceViewerWidget::Scene::Scene(const OrthancStone::CoordinateSystem3D& plane,
+                                  double thickness,
+                                  size_t countLayers) :
+    plane_(plane),
+    thickness_(thickness),
+    countMissing_(countLayers),
+    renderers_(countLayers, NULL)
+  {
+    if (thickness <= 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+  
+  SliceViewerWidget::Scene::~Scene()
+  {
+    for (size_t i = 0; i < renderers_.size(); i++)
+    {
+      DeleteLayer(i);
+    }
+  }
+
+  void SliceViewerWidget::Scene::SetLayer(size_t index,
+                                          ILayerRenderer* renderer)  // Takes ownership
   {
-  private:
-    OrthancStone::CoordinateSystem3D            plane_;
-    double                        thickness_;
-    size_t                        countMissing_;
-    std::vector<ILayerRenderer*>  renderers_;
+    if (renderer == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+
+    DeleteLayer(index);
+
+    renderers_[index] = renderer;
+    countMissing_--;
+  }
+
+
+  bool SliceViewerWidget::Scene::RenderScene(OrthancStone::CairoContext& context,
+                                             const ViewportGeometry& view,
+                                             const OrthancStone::CoordinateSystem3D& viewportPlane)
+  {
+    bool fullQuality = true;
+    cairo_t *cr = context.GetObject();
 
-  public:
-    void DeleteLayer(size_t index)
+    for (size_t i = 0; i < renderers_.size(); i++)
     {
-      if (index >= renderers_.size())
+      if (renderers_[i] != NULL)
       {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        const OrthancStone::CoordinateSystem3D& framePlane = renderers_[i]->GetLayerPlane();
+          
+        double x0, y0, x1, y1, x2, y2;
+        viewportPlane.ProjectPoint(x0, y0, framePlane.GetOrigin());
+        viewportPlane.ProjectPoint(x1, y1, framePlane.GetOrigin() + framePlane.GetAxisX());
+        viewportPlane.ProjectPoint(x2, y2, framePlane.GetOrigin() + framePlane.GetAxisY());
+
+        /**
+         * Now we solve the system of linear equations Ax + b = x', given:
+         *   A [0 ; 0] + b = [x0 ; y0]
+         *   A [1 ; 0] + b = [x1 ; y1]
+         *   A [0 ; 1] + b = [x2 ; y2]
+         * <=>
+         *   b = [x0 ; y0]
+         *   A [1 ; 0] = [x1 ; y1] - b = [x1 - x0 ; y1 - y0]
+         *   A [0 ; 1] = [x2 ; y2] - b = [x2 - x0 ; y2 - y0]
+         * <=>
+         *   b = [x0 ; y0]
+         *   [a11 ; a21] = [x1 - x0 ; y1 - y0]
+         *   [a12 ; a22] = [x2 - x0 ; y2 - y0]
+         **/
+
+        cairo_matrix_t transform;
+        cairo_matrix_init(&transform, x1 - x0, y1 - y0, x2 - x0, y2 - y0, x0, y0);
+
+        cairo_save(cr);
+        cairo_transform(cr, &transform);
+          
+        if (!renderers_[i]->RenderLayer(context, view))
+        {
+          cairo_restore(cr);
+          return false;
+        }
+
+        cairo_restore(cr);
       }
 
-      assert(countMissing_ <= renderers_.size());
-
-      if (renderers_[index] != NULL)
+      if (renderers_[i] != NULL &&
+          !renderers_[i]->IsFullQuality())
       {
-        assert(countMissing_ < renderers_.size());
-        delete renderers_[index];
-        renderers_[index] = NULL;
-        countMissing_++;
-      }
-    }
-
-    Scene(const OrthancStone::CoordinateSystem3D& plane,
-          double thickness,
-          size_t countLayers) :
-      plane_(plane),
-      thickness_(thickness),
-      countMissing_(countLayers),
-      renderers_(countLayers, NULL)
-    {
-      if (thickness <= 0)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-      }
-    }
-
-    ~Scene()
-    {
-      for (size_t i = 0; i < renderers_.size(); i++)
-      {
-        DeleteLayer(i);
+        fullQuality = false;
       }
     }
 
-    void SetLayer(size_t index,
-                  ILayerRenderer* renderer)  // Takes ownership
-    {
-      if (renderer == NULL)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-      }
-
-      DeleteLayer(index);
-
-      renderers_[index] = renderer;
-      countMissing_--;
-    }
-
-    const OrthancStone::CoordinateSystem3D& GetPlane() const
+    if (!fullQuality)
     {
-      return plane_;
-    }
+      double x, y;
+      view.MapDisplayToScene(x, y, static_cast<double>(view.GetDisplayWidth()) / 2.0, 10);
 
-    bool HasRenderer(size_t index)
-    {
-      return renderers_[index] != NULL;
-    }
+      cairo_translate(cr, x, y);
 
-    bool IsComplete() const
-    {
-      return countMissing_ == 0;
-    }
-
-    unsigned int GetCountMissing() const
-    {
-      return static_cast<unsigned int>(countMissing_);
+#if 1
+      double s = 5.0 / view.GetZoom();
+      cairo_rectangle(cr, -s, -s, 2.0 * s, 2.0 * s);
+#else
+      // TODO Drawing filled circles makes WebAssembly crash!
+      cairo_arc(cr, 0, 0, 5.0 / view.GetZoom(), 0, 2.0 * boost::math::constants::pi<double>());
+#endif
+        
+      cairo_set_line_width(cr, 2.0 / view.GetZoom());
+      cairo_set_source_rgb(cr, 1, 1, 1);
+      cairo_stroke_preserve(cr);
+      cairo_set_source_rgb(cr, 1, 0, 0);
+      cairo_fill(cr);
     }
 
-    bool RenderScene(OrthancStone::CairoContext& context,
-                     const ViewportGeometry& view,
-                     const OrthancStone::CoordinateSystem3D& viewportPlane)
-    {
-      bool fullQuality = true;
-      cairo_t *cr = context.GetObject();
+    return true;
+  }
 
-      for (size_t i = 0; i < renderers_.size(); i++)
-      {
-        if (renderers_[i] != NULL)
-        {
-          const OrthancStone::CoordinateSystem3D& framePlane = renderers_[i]->GetLayerPlane();
-          
-          double x0, y0, x1, y1, x2, y2;
-          viewportPlane.ProjectPoint(x0, y0, framePlane.GetOrigin());
-          viewportPlane.ProjectPoint(x1, y1, framePlane.GetOrigin() + framePlane.GetAxisX());
-          viewportPlane.ProjectPoint(x2, y2, framePlane.GetOrigin() + framePlane.GetAxisY());
+  void SliceViewerWidget::Scene::SetLayerStyle(size_t index,
+                                               const RenderStyle& style)
+  {
+    if (renderers_[index] != NULL)
+    {
+      renderers_[index]->SetLayerStyle(style);
+    }
+  }
 
-          /**
-           * Now we solve the system of linear equations Ax + b = x', given:
-           *   A [0 ; 0] + b = [x0 ; y0]
-           *   A [1 ; 0] + b = [x1 ; y1]
-           *   A [0 ; 1] + b = [x2 ; y2]
-           * <=>
-           *   b = [x0 ; y0]
-           *   A [1 ; 0] = [x1 ; y1] - b = [x1 - x0 ; y1 - y0]
-           *   A [0 ; 1] = [x2 ; y2] - b = [x2 - x0 ; y2 - y0]
-           * <=>
-           *   b = [x0 ; y0]
-           *   [a11 ; a21] = [x1 - x0 ; y1 - y0]
-           *   [a12 ; a22] = [x2 - x0 ; y2 - y0]
-           **/
-
-          cairo_matrix_t transform;
-          cairo_matrix_init(&transform, x1 - x0, y1 - y0, x2 - x0, y2 - y0, x0, y0);
+  bool SliceViewerWidget::Scene::ContainsPlane(const OrthancStone::CoordinateSystem3D& plane) const
+  {
+    bool isOpposite;
+    if (!OrthancStone::GeometryToolbox::IsParallelOrOpposite(isOpposite,
+                                                             plane.GetNormal(),
+                                                             plane_.GetNormal()))
+    {
+      return false;
+    }
+    else
+    {
+      double z = (plane_.ProjectAlongNormal(plane.GetOrigin()) -
+                  plane_.ProjectAlongNormal(plane_.GetOrigin()));
 
-          cairo_save(cr);
-          cairo_transform(cr, &transform);
-          
-          if (!renderers_[i]->RenderLayer(context, view))
-          {
-            cairo_restore(cr);
-            return false;
-          }
-
-          cairo_restore(cr);
-        }
-
-        if (renderers_[i] != NULL &&
-            !renderers_[i]->IsFullQuality())
-        {
-          fullQuality = false;
-        }
+      if (z < 0)
+      {
+        z = -z;
       }
 
-      if (!fullQuality)
-      {
-        double x, y;
-        view.MapDisplayToScene(x, y, static_cast<double>(view.GetDisplayWidth()) / 2.0, 10);
-
-        cairo_translate(cr, x, y);
-
-#if 1
-        double s = 5.0 / view.GetZoom();
-        cairo_rectangle(cr, -s, -s, 2.0 * s, 2.0 * s);
-#else
-        // TODO Drawing filled circles makes WebAssembly crash!
-        cairo_arc(cr, 0, 0, 5.0 / view.GetZoom(), 0, 2.0 * boost::math::constants::pi<double>());
-#endif
-        
-        cairo_set_line_width(cr, 2.0 / view.GetZoom());
-        cairo_set_source_rgb(cr, 1, 1, 1);
-        cairo_stroke_preserve(cr);
-        cairo_set_source_rgb(cr, 1, 0, 0);
-        cairo_fill(cr);
-      }
-
-      return true;
+      return z <= thickness_;
     }
-
-    void SetLayerStyle(size_t index,
-                       const RenderStyle& style)
-    {
-      if (renderers_[index] != NULL)
-      {
-        renderers_[index]->SetLayerStyle(style);
-      }
-    }
-
-    bool ContainsPlane(const OrthancStone::CoordinateSystem3D& plane) const
-    {
-      bool isOpposite;
-      if (!OrthancStone::GeometryToolbox::IsParallelOrOpposite(isOpposite,
-                                                               plane.GetNormal(),
-                                                               plane_.GetNormal()))
-      {
-        return false;
-      }
-      else
-      {
-        double z = (plane_.ProjectAlongNormal(plane.GetOrigin()) -
-                    plane_.ProjectAlongNormal(plane_.GetOrigin()));
-
-        if (z < 0)
-        {
-          z = -z;
-        }
-
-        return z <= thickness_;
-      }
-    }
-
-    double GetThickness() const
-    {
-      return thickness_;
-    }
-  };
+  }
 
   
   bool SliceViewerWidget::LookupLayer(size_t& index /* out */,
@@ -250,7 +218,7 @@
     {
       index = found->second;
       assert(index < layers_.size() &&
-             layers_[index] == &layer);
+             layers_[index].get() == &layer);
       return true;
     }
   }
@@ -369,42 +337,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<SliceViewerWidget, IVolumeSlicer::GeometryReadyMessage>
-                                   (*this, &SliceViewerWidget::OnGeometryReady));
-    // currently ignore errors layer->RegisterObserverCallback(new Callable<SliceViewerWidget, IVolumeSlicer::GeometryErrorMessage>(*this, &SliceViewerWidget::...));
-    layer.RegisterObserverCallback(new OrthancStone::Callable<SliceViewerWidget, IVolumeSlicer::SliceContentChangedMessage>
-                                   (*this, &SliceViewerWidget::OnSliceChanged));
-    layer.RegisterObserverCallback(new OrthancStone::Callable<SliceViewerWidget, IVolumeSlicer::ContentChangedMessage>
-                                   (*this, &SliceViewerWidget::OnContentChanged));
-    layer.RegisterObserverCallback(new OrthancStone::Callable<SliceViewerWidget, IVolumeSlicer::LayerReadyMessage>
-                                   (*this, &SliceViewerWidget::OnLayerReady));
-    layer.RegisterObserverCallback(new OrthancStone::Callable<SliceViewerWidget, IVolumeSlicer::LayerErrorMessage>
-                                   (*this, &SliceViewerWidget::OnLayerError));
+    // currently ignoring errors of type IVolumeSlicer::GeometryErrorMessage
+
+    Register<IVolumeSlicer::GeometryReadyMessage>(layer, &SliceViewerWidget::OnGeometryReady);
+    Register<IVolumeSlicer::SliceContentChangedMessage>(layer, &SliceViewerWidget::OnSliceChanged);
+    Register<IVolumeSlicer::ContentChangedMessage>(layer, &SliceViewerWidget::OnContentChanged);
+    Register<IVolumeSlicer::LayerReadyMessage>(layer, &SliceViewerWidget::OnLayerReady);
+    Register<IVolumeSlicer::LayerErrorMessage>(layer, &SliceViewerWidget::OnLayerError);
   }
 
 
-  size_t SliceViewerWidget::AddLayer(IVolumeSlicer* layer)  // Takes ownership
+  size_t SliceViewerWidget::AddLayer(boost::shared_ptr<IVolumeSlicer> layer)
   {
     if (layer == NULL)
     {
@@ -414,7 +367,7 @@
     size_t index = layers_.size();
     layers_.push_back(layer);
     styles_.push_back(RenderStyle());
-    layersIndex_[layer] = index;
+    layersIndex_[layer.get()] = index;
 
     ResetPendingScene();
 
@@ -426,7 +379,8 @@
   }
 
 
-  void SliceViewerWidget::ReplaceLayer(size_t index, IVolumeSlicer* layer)  // Takes ownership
+  void SliceViewerWidget::ReplaceLayer(size_t index,
+                                       boost::shared_ptr<IVolumeSlicer> layer)
   {
     if (layer == NULL)
     {
@@ -438,9 +392,8 @@
       throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
 
-    delete layers_[index];
     layers_[index] = layer;
-    layersIndex_[layer] = index;
+    layersIndex_[layer.get()] = index;
 
     ResetPendingScene();
 
@@ -457,13 +410,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();
--- a/Framework/Deprecated/Widgets/SliceViewerWidget.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Deprecated/Widgets/SliceViewerWidget.h	Tue Mar 31 15:51:02 2020 +0200
@@ -24,7 +24,7 @@
 #include "WorldSceneWidget.h"
 #include "../Layers/IVolumeSlicer.h"
 #include "../../Toolbox/Extent2D.h"
-#include "../../Messages/IObserver.h"
+#include "../../Messages/ObserverBase.h"
 
 #include <map>
 
@@ -32,7 +32,7 @@
 {
   class SliceViewerWidget :
     public WorldSceneWidget,
-    public OrthancStone::IObserver,
+    public OrthancStone::ObserverBase<SliceViewerWidget>,
     public OrthancStone::IObservable
   {
   public:
@@ -66,13 +66,67 @@
     SliceViewerWidget(const SliceViewerWidget&);
     SliceViewerWidget& operator=(const SliceViewerWidget&);
 
-    class Scene;
+    class Scene : public boost::noncopyable
+    {
+    private:
+      OrthancStone::CoordinateSystem3D  plane_;
+      double                            thickness_;
+      size_t                            countMissing_;
+      std::vector<ILayerRenderer*>      renderers_;
+
+    public:
+      void DeleteLayer(size_t index);
+
+      Scene(const OrthancStone::CoordinateSystem3D& plane,
+            double thickness,
+            size_t countLayers);
+
+      ~Scene();
+
+      void SetLayer(size_t index,
+                    ILayerRenderer* renderer);  // Takes ownership
+
+      const OrthancStone::CoordinateSystem3D& GetPlane() const
+      {
+        return plane_;
+      }
+
+      bool HasRenderer(size_t index)
+      {
+        return renderers_[index] != NULL;
+      }
+
+      bool IsComplete() const
+      {
+        return countMissing_ == 0;
+      }
+
+      unsigned int GetCountMissing() const
+      {
+        return static_cast<unsigned int>(countMissing_);
+      }
+
+      bool RenderScene(OrthancStone::CairoContext& context,
+                       const ViewportGeometry& view,
+                       const OrthancStone::CoordinateSystem3D& viewportPlane);
+
+      void SetLayerStyle(size_t index,
+                         const RenderStyle& style);
+
+      bool ContainsPlane(const OrthancStone::CoordinateSystem3D& plane) const;
+
+      double GetThickness() const
+      {
+        return thickness_;
+      }
+    };
+
     
     typedef std::map<const IVolumeSlicer*, size_t>  LayersIndex;
 
     bool                         started_;
     LayersIndex                  layersIndex_;
-    std::vector<IVolumeSlicer*>  layers_;
+    std::vector<boost::shared_ptr<IVolumeSlicer> >  layers_;
     std::vector<RenderStyle>     styles_;
     OrthancStone::CoordinateSystem3D           plane_;
     std::unique_ptr<Scene>         currentScene_;
@@ -100,8 +154,7 @@
     void ResetChangedLayers();
 
   public:
-    SliceViewerWidget(OrthancStone::MessageBroker& broker, 
-                      const std::string& name);
+    SliceViewerWidget(const std::string& name);
 
     virtual OrthancStone::Extent2D GetSceneExtent();
 
@@ -120,11 +173,13 @@
     void InvalidateLayer(size_t layer);
     
   public:
-    virtual ~SliceViewerWidget();
+    virtual ~SliceViewerWidget()
+    {
+    }
 
-    size_t AddLayer(IVolumeSlicer* layer);  // Takes ownership
+    size_t AddLayer(boost::shared_ptr<IVolumeSlicer> layer);
 
-    void ReplaceLayer(size_t layerIndex, IVolumeSlicer* layer); // Takes ownership
+    void ReplaceLayer(size_t layerIndex, boost::shared_ptr<IVolumeSlicer> layer); // Takes ownership
 
     void RemoveLayer(size_t layerIndex);
 
--- a/Framework/Fonts/GlyphTextureAlphabet.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Fonts/GlyphTextureAlphabet.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -28,7 +28,7 @@
 #include <Core/Images/ImageProcessing.h>
 #include <Core/OrthancException.h>
 
-#ifdef __EMSCRIPTEN__
+#if defined(__EMSCRIPTEN__)
 /* 
 Avoid this error:
 .../boost/math/special_functions/round.hpp:86:12: warning: implicit conversion from 'std::__2::numeric_limits<int>::type' (aka 'int') to 'float' changes value from 2147483647 to 2147483648 [-Wimplicit-int-float-conversion]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/DicomResourcesLoader.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -0,0 +1,909 @@
+/**
+ * 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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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 <Core/DicomParsing/ParsedDicomFile.h>
+#endif
+
+#include <boost/filesystem/path.hpp>
+
+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<DicomResourcesLoader>     loader_;
+    boost::shared_ptr<LoadedDicomResources>     target_;
+    int                                         priority_;
+    DicomSource                                 source_;
+    boost::shared_ptr<Orthanc::IDynamicObject>  userPayload_;
+
+  public:
+    Handler(boost::shared_ptr<DicomResourcesLoader> loader,
+            boost::shared_ptr<LoadedDicomResources> target,
+            int priority,
+            const DicomSource& source,
+            boost::shared_ptr<Orthanc::IDynamicObject> 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<DicomResourcesLoader> GetLoader()
+    {
+      assert(loader_);
+      return loader_;
+    }
+
+    boost::shared_ptr<LoadedDicomResources> GetTarget()
+    {
+      assert(target_);
+      return target_;
+    }
+
+    int GetPriority() const
+    {
+      return priority_;
+    }
+
+    const DicomSource& GetSource() const
+    {
+      return source_;
+    }
+
+    const boost::shared_ptr<Orthanc::IDynamicObject> GetUserPayload() const
+    {
+      return userPayload_;
+    }
+  };
+
+
+  class DicomResourcesLoader::StringHandler : public DicomResourcesLoader::Handler
+  {
+  public:
+    StringHandler(boost::shared_ptr<DicomResourcesLoader> loader,
+                  boost::shared_ptr<LoadedDicomResources> target,
+                  int priority,
+                  const DicomSource& source,
+                  boost::shared_ptr<Orthanc::IDynamicObject> 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<DicomResourcesLoader> loader,
+                    boost::shared_ptr<LoadedDicomResources> target,
+                    int priority,
+                    const DicomSource& source,
+                    boost::shared_ptr<Orthanc::IDynamicObject> 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<unsigned int>  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<DicomResourcesLoader> loader,
+                   boost::shared_ptr<LoadedDicomResources> target,
+                   int priority,
+                   const DicomSource& source,
+                   boost::shared_ptr<unsigned int> remainingCommands,
+                   boost::shared_ptr<Orthanc::IDynamicObject> userPayload) :
+      StringHandler(loader, target, priority, source, userPayload),
+      remainingCommands_(remainingCommands)
+    {
+      if (!remainingCommands)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+
+      (*remainingCommands) ++;
+    }
+
+    boost::shared_ptr<unsigned int> GetRemainingCommands()
+    {
+      assert(remainingCommands_);
+      return remainingCommands_;
+    }
+  };
+
+    
+  class DicomResourcesLoader::OrthancInstanceTagsHandler : public OrthancHandler
+  {
+  public:
+    OrthancInstanceTagsHandler(boost::shared_ptr<DicomResourcesLoader> loader,
+                               boost::shared_ptr<LoadedDicomResources> target,
+                               int priority,
+                               const DicomSource& source,
+                               boost::shared_ptr<unsigned int> remainingCommands,
+                               boost::shared_ptr<Orthanc::IDynamicObject> 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<DicomResourcesLoader> loader,
+                                   boost::shared_ptr<LoadedDicomResources> target,
+                                   int  priority,
+                                   const DicomSource& source,
+                                   boost::shared_ptr<unsigned int> remainingCommands,
+                                   boost::shared_ptr<Orthanc::IDynamicObject> 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<DicomResourcesLoader> loader,
+                                       boost::shared_ptr<LoadedDicomResources> target,
+                                       int  priority,
+                                       const DicomSource& source,
+                                       boost::shared_ptr<unsigned int> remainingCommands,
+                                       Orthanc::ResourceType bottomLevel,
+                                       boost::shared_ptr<Orthanc::IDynamicObject> 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::unique_ptr<Orthanc::DicomMap> 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<DicomResourcesLoader> loader,
+                    boost::shared_ptr<LoadedDicomResources> target,
+                    int priority,
+                    const DicomSource& source,
+                    boost::shared_ptr<Orthanc::IDynamicObject> 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<StringHandler&>(message.GetOrigin().GetPayload()).HandleString(message.GetAnswer());
+    }
+  }
+
+
+  void DicomResourcesLoader::Handle(const OrthancRestApiCommand::SuccessMessage& message)
+  {
+    if (message.GetOrigin().HasPayload())
+    {
+      dynamic_cast<StringHandler&>(message.GetOrigin().GetPayload()).HandleString(message.GetAnswer());
+    }
+  }
+
+
+  void DicomResourcesLoader::Handle(const ReadFileCommand::SuccessMessage& message)
+  {
+    if (message.GetOrigin().HasPayload())
+    {
+      dynamic_cast<StringHandler&>(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<Handler&>(message.GetOrigin().GetPayload());
+
+      std::set<Orthanc::DicomTag> 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<LoadedDicomResources> target,
+                                                             int priority,
+                                                             const DicomSource& source,
+                                                             const std::string& instanceId,
+                                                             boost::shared_ptr<unsigned int> remainingCommands,
+                                                             boost::shared_ptr<Orthanc::IDynamicObject> userPayload)
+  {
+    std::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+    command->SetUri("/instances/" + instanceId + "/tags");
+    command->AcquirePayload(new OrthancInstanceTagsHandler(shared_from_this(), target, priority,
+                                                           source, remainingCommands, userPayload));
+
+    {
+      std::unique_ptr<ILoadersContext::ILock> lock(context_.Lock());
+      lock->Schedule(GetSharedObserver(), priority, command.release());
+    }
+  }
+
+
+  void DicomResourcesLoader::ScheduleLoadOrthancOneChildInstance(boost::shared_ptr<LoadedDicomResources> target,
+                                                                 int priority,
+                                                                 const DicomSource& source,
+                                                                 Orthanc::ResourceType level,
+                                                                 const std::string& id,
+                                                                 boost::shared_ptr<unsigned int> remainingCommands,
+                                                                 boost::shared_ptr<Orthanc::IDynamicObject> userPayload)
+  {
+    std::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+    command->SetUri("/" + GetUri(level) + "/" + id + "/instances");
+    command->AcquirePayload(new OrthancOneChildInstanceHandler(shared_from_this(), target, priority,
+                                                               source, remainingCommands, userPayload));
+
+    {
+      std::unique_ptr<ILoadersContext::ILock> 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<IObserver> DicomResourcesLoader::Factory::Create(ILoadersContext::ILock& stone)
+  {
+    boost::shared_ptr<DicomResourcesLoader> result(new DicomResourcesLoader(stone.GetContext()));
+    result->Register<HttpCommand::SuccessMessage>(stone.GetOracleObservable(), &DicomResourcesLoader::Handle);
+    result->Register<OracleCommandExceptionMessage>(stone.GetOracleObservable(), &DicomResourcesLoader::Handle);
+    result->Register<OrthancRestApiCommand::SuccessMessage>(stone.GetOracleObservable(), &DicomResourcesLoader::Handle);
+    result->Register<ReadFileCommand::SuccessMessage>(stone.GetOracleObservable(), &DicomResourcesLoader::Handle);
+
+#if ORTHANC_ENABLE_DCMTK == 1
+    result->Register<ParseDicomSuccessMessage>(stone.GetOracleObservable(), &DicomResourcesLoader::Handle);
+#endif
+    
+    return boost::shared_ptr<IObserver>(result);
+  }
+
+
+  static void SetIncludeTags(std::map<std::string, std::string>& arguments,
+                             const std::set<Orthanc::DicomTag>& includeTags)
+  {
+    if (!includeTags.empty())
+    {
+      std::string s;
+      bool first = true;
+
+      for (std::set<Orthanc::DicomTag>::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<LoadedDicomResources> target,
+                                                 int priority,
+                                                 const DicomSource& source,
+                                                 const std::string& uri,
+                                                 const std::set<Orthanc::DicomTag>& includeTags,
+                                                 Orthanc::IDynamicObject* userPayload)
+  {
+    boost::shared_ptr<Orthanc::IDynamicObject> protection(userPayload);
+    
+    if (!source.IsDicomWeb())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, "Not a DICOMweb source");
+    }
+
+    std::map<std::string, std::string> arguments, headers;
+    SetIncludeTags(arguments, includeTags);
+  
+    std::unique_ptr<IOracleCommand> command(
+      source.CreateDicomWebCommand(uri, arguments, headers, 
+                                   new DicomWebHandler(shared_from_this(), target, priority, source, protection)));
+      
+    {
+      std::unique_ptr<ILoadersContext::ILock> lock(context_.Lock());
+      lock->Schedule(GetSharedObserver(), priority, command.release());
+    }
+  }
+  
+
+  void DicomResourcesLoader::ScheduleQido(boost::shared_ptr<LoadedDicomResources> target,
+                                          int priority,
+                                          const DicomSource& source,
+                                          Orthanc::ResourceType level,
+                                          const Orthanc::DicomMap& filter,
+                                          const std::set<Orthanc::DicomTag>& includeTags,
+                                          Orthanc::IDynamicObject* userPayload)
+  {
+    boost::shared_ptr<Orthanc::IDynamicObject> 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<Orthanc::DicomTag> tags;
+    filter.GetTags(tags);
+
+    std::map<std::string, std::string> arguments, headers;
+
+    for (std::set<Orthanc::DicomTag>::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::unique_ptr<IOracleCommand> command(
+      source.CreateDicomWebCommand(uri, arguments, headers, 
+                                   new DicomWebHandler(shared_from_this(), target, priority, source, protection)));
+
+
+    {
+      std::unique_ptr<ILoadersContext::ILock> lock(context_.Lock());
+      lock->Schedule(GetSharedObserver(), priority, command.release());
+    }
+  }
+
+    
+  void DicomResourcesLoader::ScheduleLoadOrthancResources(boost::shared_ptr<LoadedDicomResources> target,
+                                                          int priority,
+                                                          const DicomSource& source,
+                                                          Orthanc::ResourceType topLevel,
+                                                          const std::string& topId,
+                                                          Orthanc::ResourceType bottomLevel,
+                                                          Orthanc::IDynamicObject* userPayload)
+  {
+    boost::shared_ptr<Orthanc::IDynamicObject> 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<unsigned int> 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::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+      command->SetUri("/" + GetUri(topLevel) + "/" + topId + "/" + GetUri(bottomLevel));
+      command->AcquirePayload(new OrthancAllChildrenInstancesHandler
+                              (shared_from_this(), target, priority, source,
+                               remainingCommands, bottomLevel, protection));
+
+      {
+        std::unique_ptr<ILoadersContext::ILock> lock(context_.Lock());
+
+        // GetSharedObserver() means "this" (for use as an IObserver), as a 
+        // shared_ptr
+        // The oracle will thus call "this" 
+        lock->Schedule(GetSharedObserver(), priority, command.release());
+      }
+    }
+  }
+
+
+  void DicomResourcesLoader::ScheduleLoadDicomDir(boost::shared_ptr<LoadedDicomResources> target,
+                                                  int priority,
+                                                  const DicomSource& source,
+                                                  const std::string& path,
+                                                  Orthanc::IDynamicObject* userPayload)
+  {
+    boost::shared_ptr<Orthanc::IDynamicObject> 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::unique_ptr<ReadFileCommand> command(new ReadFileCommand(path));
+    command->AcquirePayload(new DicomDirHandler(shared_from_this(), target, priority, source, protection));
+
+    {
+      std::unique_ptr<ILoadersContext::ILock> 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<LoadedDicomResources> target,
+                                                   int priority,
+                                                   const DicomSource& source,
+                                                   const std::string& path,
+                                                   bool includePixelData,
+                                                   Orthanc::IDynamicObject* userPayload)
+  {
+    boost::shared_ptr<Orthanc::IDynamicObject> protection(userPayload);
+    
+#if ORTHANC_ENABLE_DCMTK == 1
+    std::unique_ptr<ParseDicomFromFileCommand> command(new ParseDicomFromFileCommand(path));
+    command->SetPixelDataIncluded(includePixelData);
+    command->AcquirePayload(new Handler(shared_from_this(), target, priority, source, protection));
+
+    {
+      std::unique_ptr<ILoadersContext::ILock> 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<LoadedDicomResources> target,
+                                                   int priority,
+                                                   const DicomSource& source,
+                                                   const std::string& dicomDirPath,
+                                                   const Orthanc::DicomMap& dicomDirEntry,
+                                                   bool includePixelData,
+                                                   Orthanc::IDynamicObject* userPayload)
+  {
+    std::unique_ptr<Orthanc::IDynamicObject> 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
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/DicomResourcesLoader.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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 <Core/DicomParsing/ParsedDicomDir.h>
+#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<DicomResourcesLoader>,
+    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<LoadedDicomResources> target,
+                                         int priority,
+                                         const DicomSource& source,
+                                         const std::string& instanceId,
+                                         boost::shared_ptr<unsigned int> remainingCommands,
+                                         boost::shared_ptr<Orthanc::IDynamicObject> userPayload);
+
+    void ScheduleLoadOrthancOneChildInstance(boost::shared_ptr<LoadedDicomResources> target,
+                                             int priority,
+                                             const DicomSource& source,
+                                             Orthanc::ResourceType level,
+                                             const std::string& id,
+                                             boost::shared_ptr<unsigned int> remainingCommands,
+                                             boost::shared_ptr<Orthanc::IDynamicObject> userPayload);
+    
+    DicomResourcesLoader(ILoadersContext& context) :
+      context_(context)
+    {
+    }
+
+    ILoadersContext&  context_;
+
+
+  public:
+    class SuccessMessage : public OrthancStone::OriginMessage<DicomResourcesLoader>
+    {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+      
+    private:
+      boost::shared_ptr<LoadedDicomResources>  resources_;
+      int                                      priority_;
+      const DicomSource&                       source_;
+      const Orthanc::IDynamicObject*           userPayload_;
+      
+    public:
+      SuccessMessage(const DicomResourcesLoader& origin,
+                     boost::shared_ptr<LoadedDicomResources> 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<LoadedDicomResources> 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<IObserver> Create(ILoadersContext::ILock& stone);
+    };
+
+    void ScheduleGetDicomWeb(boost::shared_ptr<LoadedDicomResources> target,
+                             int priority,
+                             const DicomSource& source,
+                             const std::string& uri,
+                             const std::set<Orthanc::DicomTag>& includeTags,
+                             Orthanc::IDynamicObject* userPayload);
+
+    void ScheduleGetDicomWeb(boost::shared_ptr<LoadedDicomResources> target,
+                             int priority,
+                             const DicomSource& source,
+                             const std::string& uri,
+                             Orthanc::IDynamicObject* userPayload)
+    {
+      std::set<Orthanc::DicomTag> includeTags;
+      ScheduleGetDicomWeb(target, priority, source, uri, includeTags, userPayload);
+    }        
+
+    void ScheduleQido(boost::shared_ptr<LoadedDicomResources> target,
+                      int priority,
+                      const DicomSource& source,
+                      Orthanc::ResourceType level,
+                      const Orthanc::DicomMap& filter,
+                      const std::set<Orthanc::DicomTag>& includeTags,
+                      Orthanc::IDynamicObject* userPayload);
+
+    void ScheduleLoadOrthancResources(boost::shared_ptr<LoadedDicomResources> target,
+                                      int priority,
+                                      const DicomSource& source,
+                                      Orthanc::ResourceType topLevel,
+                                      const std::string& topId,
+                                      Orthanc::ResourceType bottomLevel,
+                                      Orthanc::IDynamicObject* userPayload);
+
+    void ScheduleLoadOrthancResource(boost::shared_ptr<LoadedDicomResources> 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<LoadedDicomResources> target,
+                              int priority,
+                              const DicomSource& source,
+                              const std::string& path,
+                              Orthanc::IDynamicObject* userPayload);
+    
+    void ScheduleLoadDicomFile(boost::shared_ptr<LoadedDicomResources> target,
+                               int priority,
+                               const DicomSource& source,
+                               const std::string& path,
+                               bool includePixelData,
+                               Orthanc::IDynamicObject* userPayload);
+
+    bool ScheduleLoadDicomFile(boost::shared_ptr<LoadedDicomResources> target,
+                               int priority,
+                               const DicomSource& source,
+                               const std::string& dicomDirPath,
+                               const Orthanc::DicomMap& dicomDirEntry,
+                               bool includePixelData,
+                               Orthanc::IDynamicObject* userPayload);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/DicomSource.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "DicomSource.h"
+
+#include "../Oracle/HttpCommand.h"
+#include "../Oracle/OrthancRestApiCommand.h"
+
+#include <Core/OrthancException.h>
+
+#include <boost/algorithm/string/predicate.hpp>
+
+namespace OrthancStone
+{
+  static std::string EncodeGetArguments(const std::string& uri,
+                                        const std::map<std::string, std::string>& arguments)
+  {
+    std::string s = uri;
+    bool first = true;
+
+    for (std::map<std::string, std::string>::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<std::string, std::string>& arguments,
+                                                     const std::map<std::string, std::string>& headers,
+                                                     Orthanc::IDynamicObject* payload) const
+  {
+    std::unique_ptr<Orthanc::IDynamicObject> protection(payload);
+
+    switch (type_)
+    {
+      case DicomSourceType_DicomWeb:
+      {
+        std::unique_ptr<HttpCommand> command(new HttpCommand);
+        
+        command->SetMethod(Orthanc::HttpMethod_Get);
+        command->SetUrl(webService_.GetUrl() + "/" + EncodeGetArguments(uri, arguments));
+        command->SetHttpHeaders(webService_.GetHttpHeaders());
+
+        for (std::map<std::string, std::string>::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<std::string, std::string>::const_iterator
+               it = arguments.begin(); it != arguments.end(); ++it)
+        {
+          args[it->first] = it->second;
+        }
+          
+        Json::Value h = Json::objectValue;
+        for (std::map<std::string, std::string>::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::unique_ptr<OrthancRestApiCommand> 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);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/DicomSource.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Oracle/IOracleCommand.h"
+
+#include <Core/WebServiceParameters.h>
+
+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<std::string, std::string>& arguments,
+                                          const std::map<std::string, std::string>& 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;
+  };
+}
--- a/Framework/Loaders/DicomStructureSetLoader.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ /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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "DicomStructureSetLoader.h"
-
-#include "../Scene2D/PolylineSceneLayer.h"
-#include "../StoneException.h"
-#include "../Toolbox/GeometryToolbox.h"
-
-#include <Core/Toolbox.h>
-
-#include <algorithm>
-
-#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<DicomStructureSetLoader>();
-
-      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<DicomStructureSetLoader>();
-
-      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::unique_ptr<OrthancRestApiCommand> 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<DicomStructureSetLoader>();
-        
-      {
-        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<std::string>::iterator foundIt =
-              std::find(
-                loader.initiallyVisibleStructures_.begin(),
-                loader.initiallyVisibleStructures_.end(),
-                structureName);
-            std::vector<std::string>::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<std::string> instances;
-      std::set<std::string> nonEmptyInstances;
-      loader.content_->GetReferencedInstances(instances);
-      for (std::set<std::string>::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<unsigned int>(nonEmptyInstances.size());
-
-      for (std::set<std::string>::const_iterator
-        it = nonEmptyInstances.begin(); it != nonEmptyInstances.end(); ++it)
-      {
-        std::unique_ptr<OrthancRestApiCommand> 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<bool>         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<bool> visibility = std::vector<bool>()) 
-      : 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::unique_ptr<PolylineSceneLayer> 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<Point2D> > 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<Point2D, Point2D> > 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<std::string>& initiallyVisibleStructures)
-  {
-    Start();
-      
-    instanceId_ = instanceId;
-    initiallyVisibleStructures_ = initiallyVisibleStructures;
-
-    {
-      std::unique_ptr<OrthancRestApiCommand> 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_;
-  }
-
-}
--- a/Framework/Loaders/DicomStructureSetLoader.h	Tue Mar 31 15:47:29 2020 +0200
+++ /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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "../Toolbox/DicomStructureSet.h"
-#include "../Volumes/IVolumeSlicer.h"
-#include "LoaderStateMachine.h"
-
-#include <vector>
-
-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::unique_ptr<DicomStructureSet>  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<std::string> 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<bool>                  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<std::string>& initiallyVisibleStructures = std::vector<std::string>());
-
-    virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE;
-
-    void SetStructuresReady();
-
-    bool AreStructuresReady() const;
-  };
-}
--- a/Framework/Loaders/DicomStructureSetLoader2.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ /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 <http://www.gnu.org/licenses/>.
- **/
-
-#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<DicomStructureSetLoader2, OrthancRestApiCommand::SuccessMessage>
-      (*this, &DicomStructureSetLoader2::HandleSuccessMessage));
-
-    oracleObservable.RegisterObserverCallback(
-      new Callable<DicomStructureSetLoader2, OracleCommandExceptionMessage>
-      (*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::unique_ptr<OrthancRestApiCommand> 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
-
--- a/Framework/Loaders/DicomStructureSetLoader2.h	Tue Mar 31 15:47:29 2020 +0200
+++ /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 <http://www.gnu.org/licenses/>.
- **/
-
-#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 <boost/noncopyable.hpp>
-
-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
-
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/DicomVolumeLoader.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -0,0 +1,188 @@
+/**
+ * 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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "DicomVolumeLoader.h"
+
+#include <Core/Images/ImageProcessing.h>
+
+namespace OrthancStone
+{
+  DicomVolumeLoader::DicomVolumeLoader(boost::shared_ptr<SeriesFramesLoader>& 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<unsigned int>(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<const Orthanc::SingleValueObject<size_t>&>
+    (message.GetUserPayload()).GetValue();
+
+    {
+      ImageBuffer3D::SliceWriter writer(volume_->GetPixelData(), 
+                                        VolumeProjection_Axial, 
+                                        static_cast<unsigned int>(frameIndex));
+      
+      Orthanc::ImageProcessing::Copy(writer.GetAccessor(), message.GetImage());
+    }
+
+    volume_->IncrementRevision();
+
+    {
+      VolumeUpdatedMessage updated(*this, 
+                                   static_cast<unsigned int>(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<IObserver> DicomVolumeLoader::Factory::Create(ILoadersContext::ILock& context)
+  { 
+    boost::shared_ptr<SeriesFramesLoader> frames =
+      boost::dynamic_pointer_cast<SeriesFramesLoader>(framesFactory_.Create(context));
+
+    boost::shared_ptr<DicomVolumeLoader> volume(new DicomVolumeLoader(frames, computeRange_));
+    volume->Register<SeriesFramesLoader::FrameLoadedMessage>(*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<size_t>(i));
+      }
+    }
+    else
+    {
+      VolumeReadyMessage ready(*this);
+      BroadcastMessage(ready);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/DicomVolumeLoader.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Volumes/DicomVolumeImage.h"
+#include "SeriesFramesLoader.h"
+#include "SeriesMetadataLoader.h"
+
+namespace OrthancStone
+{
+  class DicomVolumeLoader : 
+    public ObserverBase<DicomVolumeLoader>,
+    public IObservable
+  {
+  private:
+    boost::shared_ptr<SeriesFramesLoader>  framesLoader_;
+    boost::shared_ptr<DicomVolumeImage>    volume_;
+    bool                                   isValid_;
+    bool                                   started_;
+    size_t                                 remaining_;
+
+    DicomVolumeLoader(boost::shared_ptr<SeriesFramesLoader>& framesLoader,
+                      bool computeRange);
+
+    void Handle(const OrthancStone::SeriesFramesLoader::FrameLoadedMessage& message);
+
+  public:
+    class VolumeReadyMessage : public OriginMessage<DicomVolumeLoader>
+    {
+      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<DicomVolumeLoader>
+    {
+      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<LoadedDicomResources> dicomDir)
+      {
+        framesFactory_.SetDicomDir(dicomDirPath, dicomDir);
+      }
+
+      virtual boost::shared_ptr<IObserver> Create(ILoadersContext::ILock& context) ORTHANC_OVERRIDE;
+    };
+
+    bool IsValid() const
+    {
+      return isValid_;
+    }
+
+    bool IsFullyLoaded() const
+    {
+      return remaining_ == 0;
+    }
+
+    boost::shared_ptr<DicomVolumeImage> GetVolume() const
+    {
+      return volume_;
+    }
+
+    const SeriesOrderedFrames& GetOrderedFrames() const
+    {
+      return framesLoader_->GetOrderedFrames();
+    }
+
+    void Start(int priority,
+               const DicomSource& source);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/GenericLoadersContext.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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<IObserver> loader) ORTHANC_OVERRIDE
+    {
+      that_.loaders_.push_back(loader);
+    }
+
+    virtual IObservable& GetOracleObservable() const ORTHANC_OVERRIDE
+    {
+      return that_.oracleObservable_;
+    }
+
+    virtual void Schedule(boost::shared_ptr<IObserver> receiver,
+                          int priority,
+                          IOracleCommand* command /* Takes ownership */) ORTHANC_OVERRIDE
+    {
+      that_.scheduler_->Schedule(receiver, priority, command);
+    };
+
+    virtual void CancelRequests(boost::shared_ptr<IObserver> 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<IObserver> 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);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/GenericLoadersContext.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Messages/IMessageEmitter.h"
+#include "../Oracle/ThreadedOracle.h"
+#include "ILoadersContext.h"
+#include "DicomSource.h"
+#include "OracleScheduler.h"
+
+#include <boost/thread/recursive_mutex.hpp>
+
+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::unique_ptr<ThreadedOracle>     oracle_;
+    boost::shared_ptr<OracleScheduler>  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<IObserver> >  loaders_; 
+
+    virtual void EmitMessage(boost::weak_ptr<IObserver> 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();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/ILoaderFactory.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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<IObserver> Create(ILoadersContext::ILock& context) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/ILoadersContext.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Messages/IObserver.h"
+#include "../Messages/IObservable.h"
+#include "../Oracle/IOracleCommand.h"
+
+#include <boost/shared_ptr.hpp>
+
+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<IObserver> 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<IObserver> 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<IObserver> 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;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/LoadedDicomResources.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "LoadedDicomResources.h"
+
+#include <Core/OrthancException.h>
+
+#include <cassert>
+
+
+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<std::string, unsigned int>  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;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/LoadedDicomResources.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <Core/DicomFormat/DicomMap.h>
+
+
+namespace OrthancStone
+{
+  /**
+    Stores an indexed collection of DicomMap objects. The index is a 
+    user-specified DicomTag.
+  */
+  class LoadedDicomResources : public boost::noncopyable
+  {
+  private:
+    typedef std::map<std::string, Orthanc::DicomMap*>  Resources;
+
+    Orthanc::DicomTag                indexedTag_;
+    Resources                        resources_;
+    std::vector<Orthanc::DicomMap*>  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;
+  };
+}
--- a/Framework/Loaders/LoaderCache.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ /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 <http://www.gnu.org/licenses/>.
- **/
-
-#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 <unistd.h>
-# 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 <Core/OrthancException.h>
-#include <Core/Toolbox.h>
-
-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<OrthancStone::OrthancSeriesVolumeProgressiveLoader> 
-    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<DicomVolumeImage> volumeImage(new DicomVolumeImage);
-        boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> 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<OrthancMultiframeVolumeLoader> 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<DicomVolumeImageMPRSlicer> 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<DicomVolumeImage> volumeImage(new DicomVolumeImage);
-        boost::shared_ptr<OrthancMultiframeVolumeLoader> 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<DicomVolumeImageMPRSlicer> 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<DicomStructureSetSlicer2> 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<std::string>& stringList)
-  {
-    if (stringList.size() == 0)
-    {
-      return "";
-    } 
-    else
-    {
-      std::vector<std::string> 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<DicomStructureSetLoader> 
-    LoaderCache::GetDicomStructureSetLoader(
-      std::string inInstanceUuid, 
-      const std::vector<std::string>& 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<DicomStructureSetLoader> 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<DicomStructureSetLoader2> 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<DicomStructureSetLoader2> loader;
-        boost::shared_ptr<DicomStructureSet2> structureSet(new DicomStructureSet2());
-        boost::shared_ptr<DicomStructureSetSlicer2> 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<typename T> void DebugDisplayObjRefCountsInMap(
-    const std::string& name, const std::map<std::string, boost::shared_ptr<T> >& myMap)
-  {
-    LOG(TRACE) << "Map \"" << name << "\" ref counts:";
-    size_t i = 0;
-    for (typename std::map<std::string, boost::shared_ptr<T> >::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
-  }
-}
--- a/Framework/Loaders/LoaderCache.h	Tue Mar 31 15:47:29 2020 +0200
+++ /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 <http://www.gnu.org/licenses/>.
- **/
-
-#pragma once
-
-#include <boost/shared_ptr.hpp>
-
-#include <map>
-#include <string>
-#include <vector>
-
-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<OrthancSeriesVolumeProgressiveLoader>
-      GetSeriesVolumeProgressiveLoader      (std::string seriesUuid);
-    
-    boost::shared_ptr<DicomVolumeImageMPRSlicer>
-      GetMultiframeDicomVolumeImageMPRSlicer(std::string instanceUuid);
-
-    boost::shared_ptr<OrthancMultiframeVolumeLoader>
-      GetMultiframeVolumeLoader(std::string instanceUuid);
-
-    boost::shared_ptr<DicomStructureSetLoader>
-      GetDicomStructureSetLoader(
-        std::string instanceUuid,
-        const std::vector<std::string>& initiallyVisibleStructures);
-
-#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2
-    boost::shared_ptr<DicomStructureSetLoader2>
-      GetDicomStructureSetLoader2(std::string instanceUuid);
-
-    boost::shared_ptr<DicomStructureSetSlicer2>
-      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<std::string, boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> >
-      seriesVolumeProgressiveLoaders_;
-    std::map<std::string, boost::shared_ptr<OrthancMultiframeVolumeLoader> >
-      multiframeVolumeLoaders_;
-    std::map<std::string, boost::shared_ptr<DicomVolumeImageMPRSlicer> >
-      dicomVolumeImageMPRSlicers_;
-    std::map<std::string, boost::shared_ptr<DicomStructureSetLoader> >
-      dicomStructureSetLoaders_;
-#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2
-    std::map<std::string, boost::shared_ptr<DicomStructureSetLoader2> >
-      dicomStructureSetLoaders2_;
-    std::map<std::string, boost::shared_ptr<DicomStructureSet2> >
-      dicomStructureSets2_;
-    std::map<std::string, boost::shared_ptr<DicomStructureSetSlicer2> >
-      dicomStructureSetSlicers2_;
-#endif 
-    //BGO_ENABLE_DICOMSTRUCTURESETLOADER2
-  };
-}
-
--- a/Framework/Loaders/LoaderStateMachine.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ /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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "LoaderStateMachine.h"
-
-#include <Core/OrthancException.h>
-
-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::unique_ptr<OracleCommandWithPayload> 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 <typename T>
-  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<State&>(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<LoaderStateMachine, OrthancRestApiCommand::SuccessMessage>
-      (*this, &LoaderStateMachine::HandleSuccessMessage));
-
-    oracleObservable.RegisterObserverCallback(
-      new Callable<LoaderStateMachine, GetOrthancImageCommand::SuccessMessage>
-      (*this, &LoaderStateMachine::HandleSuccessMessage));
-
-    oracleObservable.RegisterObserverCallback(
-      new Callable<LoaderStateMachine, GetOrthancWebViewerJpegCommand::SuccessMessage>
-      (*this, &LoaderStateMachine::HandleSuccessMessage));
-
-    oracleObservable.RegisterObserverCallback(
-      new Callable<LoaderStateMachine, OracleCommandExceptionMessage>
-      (*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;
-    }
-  }
-}
--- a/Framework/Loaders/LoaderStateMachine.h	Tue Mar 31 15:47:29 2020 +0200
+++ /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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#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 <Core/IDynamicObject.h>
-
-#include <list>
-
-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 <typename T>
-      T& GetLoader() const
-      {
-        return dynamic_cast<T&>(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 <typename T>
-    void HandleSuccessMessage(const T& message);
-
-    typedef std::list<IOracleCommand*>  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);  
-  };
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/OracleScheduler.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OracleScheduler.h"
+
+#include "../Oracle/ParseDicomFromFileCommand.h"
+
+namespace OrthancStone
+{
+  class OracleScheduler::ReceiverPayload : public Orthanc::IDynamicObject
+  {
+  private:
+    Priority   priority_;
+    boost::weak_ptr<IObserver>  receiver_;
+    std::unique_ptr<IOracleCommand>  command_;
+
+  public:
+    ReceiverPayload(Priority priority,
+                    boost::weak_ptr<IObserver> 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<IObserver> GetOriginalReceiver() const
+    {
+      return receiver_;
+    }
+
+    const IOracleCommand& GetOriginalCommand() const
+    {
+      assert(command_.get() != NULL);
+      return *command_;
+    }
+  }; 
+
+
+  class OracleScheduler::ScheduledCommand : public boost::noncopyable
+  {
+  private:
+    boost::weak_ptr<IObserver>     receiver_;
+    std::unique_ptr<IOracleCommand>  command_;
+
+  public:
+    ScheduledCommand(boost::shared_ptr<IObserver> receiver,
+                     IOracleCommand* command) :
+      receiver_(receiver),
+      command_(command)
+    {
+      if (command == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+    }
+
+    boost::weak_ptr<IObserver> GetReceiver() 
+    {
+      return receiver_;
+    }
+  
+    bool IsSameReceiver(boost::shared_ptr<OrthancStone::IObserver> receiver) const
+    {
+      boost::shared_ptr<IObserver> 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::unique_ptr<IOracleCommand> wrapped(command_->Clone());
+        dynamic_cast<OracleCommandBase&>(*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<IObserver> 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::unique_ptr<ScheduledCommand> command(dynamic_cast<ScheduledCommand*>(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<IObserver> 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<OracleScheduler>" 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<const ReceiverPayload&>(message.GetOrigin().GetPayload());
+    
+    RemoveActiveCommand(payload);
+
+    GetOrthancImageCommand::SuccessMessage bis(
+      dynamic_cast<const GetOrthancImageCommand&>(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<const ReceiverPayload&>(message.GetOrigin().GetPayload());
+    
+    RemoveActiveCommand(payload);
+
+    GetOrthancWebViewerJpegCommand::SuccessMessage bis(
+      dynamic_cast<const GetOrthancWebViewerJpegCommand&>(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<const ReceiverPayload&>(message.GetOrigin().GetPayload());
+    
+    RemoveActiveCommand(payload);
+
+    HttpCommand::SuccessMessage bis(
+      dynamic_cast<const HttpCommand&>(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<const ReceiverPayload&>(message.GetOrigin().GetPayload());
+    
+    RemoveActiveCommand(payload);
+
+    OrthancRestApiCommand::SuccessMessage bis(
+      dynamic_cast<const OrthancRestApiCommand&>(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<const ReceiverPayload&>(message.GetOrigin().GetPayload());
+    
+    RemoveActiveCommand(payload);
+
+    ParseDicomSuccessMessage bis(
+      dynamic_cast<const OracleCommandBase&>(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<const ReceiverPayload&>(message.GetOrigin().GetPayload());
+    
+    RemoveActiveCommand(payload);
+
+    ReadFileCommand::SuccessMessage bis(
+      dynamic_cast<const ReadFileCommand&>(payload.GetOriginalCommand()),
+      message.GetContent());
+    emitter_.EmitMessage(payload.GetOriginalReceiver(), bis);
+  }
+  
+
+  void OracleScheduler::Handle(const OracleCommandExceptionMessage& message)
+  {
+    const OracleCommandBase& command = dynamic_cast<const OracleCommandBase&>(message.GetOrigin());
+    
+    assert(command.HasPayload());
+    const ReceiverPayload& payload = dynamic_cast<const ReceiverPayload&>(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> OracleScheduler::Create(IOracle& oracle,
+                                                             IObservable& oracleObservable,
+                                                             IMessageEmitter& emitter,
+                                                             unsigned int maxHighPriority,
+                                                             unsigned int maxStandardPriority,
+                                                             unsigned int maxLowPriority)
+  {
+    boost::shared_ptr<OracleScheduler> scheduler
+      (new OracleScheduler(oracle, emitter, maxHighPriority, maxStandardPriority, maxLowPriority));
+    scheduler->Register<GetOrthancImageCommand::SuccessMessage>(oracleObservable, &OracleScheduler::Handle);
+    scheduler->Register<GetOrthancWebViewerJpegCommand::SuccessMessage>(oracleObservable, &OracleScheduler::Handle);
+    scheduler->Register<HttpCommand::SuccessMessage>(oracleObservable, &OracleScheduler::Handle);
+    scheduler->Register<OrthancRestApiCommand::SuccessMessage>(oracleObservable, &OracleScheduler::Handle);
+    scheduler->Register<ReadFileCommand::SuccessMessage>(oracleObservable, &OracleScheduler::Handle);
+    scheduler->Register<OracleCommandExceptionMessage>(oracleObservable, &OracleScheduler::Handle);
+
+#if ORTHANC_ENABLE_DCMTK == 1
+    scheduler->Register<ParseDicomSuccessMessage>(oracleObservable, &OracleScheduler::Handle);
+#endif
+
+    return scheduler;
+  }
+    
+
+  OracleScheduler::~OracleScheduler()
+  {      
+    CancelAllRequests();
+  }
+
+
+  void OracleScheduler::CancelRequests(boost::shared_ptr<IObserver> 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<IObserver> receiver,
+                                 int priority,
+                                 IOracleCommand* command /* Takes ownership */)
+  {
+    std::unique_ptr<ScheduledCommand> pending(new ScheduledCommand(receiver, dynamic_cast<IOracleCommand*>(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();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/OracleScheduler.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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<OracleScheduler>
+  {
+  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<int, ScheduledCommand*>  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<IObserver> 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<OracleScheduler> Create(IOracle& oracle,
+                                                     IObservable& oracleObservable,
+                                                     IMessageEmitter& emitter)
+    {
+      return Create(oracle, oracleObservable, emitter, 1, 4, 1);
+    }
+
+    static boost::shared_ptr<OracleScheduler> 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<IObserver> receiver);
+
+    void CancelAllRequests();
+
+    void Schedule(boost::shared_ptr<IObserver> receiver,
+                  int priority,
+                  IOracleCommand* command /* Takes ownership */);
+  };
+}
--- a/Framework/Loaders/OrthancMultiframeVolumeLoader.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,580 +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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "OrthancMultiframeVolumeLoader.h"
-
-#include <Core/Endianness.h>
-#include <Core/Toolbox.h>
-
-namespace OrthancStone
-{
-  class OrthancMultiframeVolumeLoader::LoadRTDoseGeometry : public LoaderStateMachine::State
-  {
-  private:
-    std::unique_ptr<Orthanc::DicomMap>  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<OrthancMultiframeVolumeLoader>().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<OrthancMultiframeVolumeLoader>();
-        
-      Json::Value body;
-      message.ParseJsonBody(body);
-        
-      if (body.type() != Json::objectValue)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
-      }
-
-      std::unique_ptr<Orthanc::DicomMap> 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::unique_ptr<OrthancRestApiCommand> 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<OrthancMultiframeVolumeLoader>().SetTransferSyntax(message.GetAnswer());
-    }
-  };
-
-  class OrthancMultiframeVolumeLoader::LoadUncompressedPixelData : public State
-  {
-  public:
-    LoadUncompressedPixelData(OrthancMultiframeVolumeLoader& that) :
-    State(that)
-    {
-    }
-      
-    virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
-    {
-      GetLoader<OrthancMultiframeVolumeLoader>().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::unique_ptr<OrthancRestApiCommand> 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<const uint32_t*>(source));
-  }
-
-  ORTHANC_FORCE_INLINE
-    static void CopyPixel(uint16_t& target, const void* source)
-  {
-    // TODO - check alignement?
-    target = le16toh(*reinterpret_cast<const uint16_t*>(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<uint16_t*>(&target);
-    CopyPixel(*targetUp, source);
-  }
-
-  template <typename T>
-  void OrthancMultiframeVolumeLoader::CopyPixelDataAndComputeDistribution(
-    const std::string& pixelData, std::map<T,uint64_t>& 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<const uint8_t*>(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<const uint8_t*>(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<T*>(writer.GetAccessor().GetRow(y));
-
-          for (unsigned int x = 0; x < width; x++)
-          {
-            CopyPixel(*target, source);
-
-            distribution[*target] += 1;
-
-            target++;
-            source += bpp;
-          }
-        }
-      }
-    }
-  }
-
-  template <typename T>
-  void OrthancMultiframeVolumeLoader::ComputeMinMaxWithOutlierRejection(
-    const std::map<T, uint64_t>& distribution)
-  {
-    if (distribution.size() == 0)
-    {
-      LOG(ERROR) << "ComputeMinMaxWithOutlierRejection -- Volume image empty.";
-    }
-    else
-    {
-      ImageBuffer3D& target = volume_->GetPixelData();
-
-      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<T, uint64_t>::const_iterator it = distribution.begin();
-        uint64_t totalCount = 0;
-        distributionRawMin_ = static_cast<float>(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<float>(pixelValue);
-        }
-        LOG(INFO) << "Volume image. First distribution value = " 
-          << static_cast<float>(distributionRawMin_) 
-          << " | Last distribution value = " 
-          << static_cast<float>(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<uint64_t>(
-        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<T, uint64_t>::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<T, uint64_t>::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<float>(resultMin);
-      computedDistributionMax_ = static_cast<float>(resultMax);
-    }
-  }
-
-  template <typename T>
-  void OrthancMultiframeVolumeLoader::CopyPixelDataAndComputeMinMax(
-    const std::string& pixelData)
-  {
-    std::map<T, uint64_t> distribution;
-    CopyPixelDataAndComputeDistribution(pixelData, distribution);
-    ComputeMinMaxWithOutlierRejection(distribution);
-  }
-
-  void OrthancMultiframeVolumeLoader::SetUncompressedPixelData(const std::string& pixelData)
-  {
-    switch (volume_->GetPixelData().GetFormat())
-    {
-      case Orthanc::PixelFormat_Grayscale32:
-        CopyPixelDataAndComputeMinMax<uint32_t>(pixelData);
-        break;
-      case Orthanc::PixelFormat_Grayscale16:
-        CopyPixelDataAndComputeMinMax<uint16_t>(pixelData);
-        break;
-      case Orthanc::PixelFormat_SignedGrayscale16:
-        CopyPixelDataAndComputeMinMax<int16_t>(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<DicomVolumeImage> 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::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
-      command->SetHttpHeader("Accept-Encoding", "gzip");
-      command->SetUri("/instances/" + instanceId + "/tags");
-      command->SetPayload(new LoadGeometry(*this));
-      Schedule(command.release());
-    }
-
-    {
-      std::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
-      command->SetUri("/instances/" + instanceId + "/metadata/TransferSyntax");
-      command->SetPayload(new LoadTransferSyntax(*this));
-      Schedule(command.release());
-    }
-  }
-}
--- a/Framework/Loaders/OrthancMultiframeVolumeLoader.h	Tue Mar 31 15:47:29 2020 +0200
+++ /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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "LoaderStateMachine.h"
-#include "../Volumes/DicomVolumeImage.h"
-
-#include <boost/shared_ptr.hpp>
-
-namespace OrthancStone
-{
-  class OrthancMultiframeVolumeLoader :
-    public LoaderStateMachine,
-    public IObservable,
-    public IGeometryProvider
-  {
-  private:
-    class LoadRTDoseGeometry;
-    class LoadGeometry;
-    class LoadTransferSyntax;    
-    class LoadUncompressedPixelData;
-
-    boost::shared_ptr<DicomVolumeImage>  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 <typename T>
-    void CopyPixelDataAndComputeMinMax(const std::string& pixelData);
-      
-    /** Service method for CopyPixelDataAndComputeMinMax*/
-    template <typename T>
-    void CopyPixelDataAndComputeDistribution(
-      const std::string& pixelData, 
-      std::map<T, uint64_t>& distribution);
-
-    /** Service method for CopyPixelDataAndComputeMinMax*/
-    template <typename T>
-    void ComputeMinMaxWithOutlierRejection(
-      const std::map<T, uint64_t>& 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<DicomVolumeImage> 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);
-  };
-}
--- a/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ /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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "OrthancSeriesVolumeProgressiveLoader.h"
-
-#include "../Toolbox/GeometryToolbox.h"
-#include "../Volumes/DicomVolumeImageMPRSlicer.h"
-#include "BasicFetchingItemsSorter.h"
-#include "BasicFetchingStrategy.h"
-
-#include <Core/Images/ImageProcessing.h>
-#include <Core/OrthancException.h>
-
-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<const DicomInstanceParameters&>(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<unsigned int>(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<unsigned int>& >(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::unique_ptr<OracleCommandWithPayload> command;
-        
-      if (quality == BEST_QUALITY)
-      {
-        std::unique_ptr<GetOrthancImageCommand> 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::unique_ptr<GetOrthancWebViewerJpegCommand> 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<unsigned int>(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::unique_ptr<DicomInstanceParameters> 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<unsigned int>(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<DicomVolumeImage>& 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<OrthancSeriesVolumeProgressiveLoader, OrthancRestApiCommand::SuccessMessage>
-      (*this, &OrthancSeriesVolumeProgressiveLoader::LoadGeometry));
-
-    oracleObservable.RegisterObserverCallback(
-      new Callable<OrthancSeriesVolumeProgressiveLoader, GetOrthancImageCommand::SuccessMessage>
-      (*this, &OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent));
-
-    oracleObservable.RegisterObserverCallback(
-      new Callable<OrthancSeriesVolumeProgressiveLoader, GetOrthancWebViewerJpegCommand::SuccessMessage>
-      (*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::unique_ptr<OrthancRestApiCommand> 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;
-    }
-  }
-}
--- a/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h	Tue Mar 31 15:47:29 2020 +0200
+++ /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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#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 <boost/shared_ptr.hpp>
-
-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::unique_ptr<VolumeImageGeometry>     geometry_;
-      std::vector<DicomInstanceParameters*>  slices_;
-      std::vector<uint64_t>                  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<DicomVolumeImage>           volume_;
-    std::unique_ptr<IFetchingItemsSorter::IFactory> sorter_;
-    std::unique_ptr<IFetchingStrategy>              strategy_;
-    std::vector<unsigned int>                     slicesQuality_;
-    bool                                          volumeImageReadyInHighQuality_;
-
-
-  public:
-    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, VolumeImageReadyInHighQuality, OrthancSeriesVolumeProgressiveLoader);
-
-
-    OrthancSeriesVolumeProgressiveLoader(const boost::shared_ptr<DicomVolumeImage>& 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;
-  };
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/SeriesFramesLoader.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -0,0 +1,550 @@
+/**
+ * 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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "SeriesFramesLoader.h"
+
+#include "../Oracle/ParseDicomFromFileCommand.h"
+#include "../Oracle/ParseDicomFromWadoCommand.h"
+
+#if ORTHANC_ENABLE_DCMTK == 1
+#  include <Core/DicomParsing/Internals/DicomImageDecoder.h>
+#endif
+
+#include <Core/DicomFormat/DicomInstanceHasher.h>
+#include <Core/Images/Image.h>
+#include <Core/Images/ImageProcessing.h>
+#include <Core/Images/JpegReader.h>
+
+#include <boost/algorithm/string/predicate.hpp>
+
+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::unique_ptr<Orthanc::IDynamicObject>  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<LoadedDicomResources> 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::unique_ptr<Orthanc::ImageAccessor> decoded;
+    decoded.reset(Orthanc::DicomImageDecoder::Decode(
+      dicom, 
+      static_cast<unsigned int>(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<std::string, std::string>& headers)
+  {
+    assert(payload.GetSource().IsDicomWeb() &&
+           payload.HasWindowing());
+
+    bool ok = false;
+    for (std::map<std::string, std::string>::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.01f;  // 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<const Payload&>(message.GetOrigin().GetPayload());
+    if ((payload.GetSource().IsDicomDir() ||
+         payload.GetSource().IsDicomWeb()) &&
+        message.HasPixelData())
+    {
+      HandleDicom(dynamic_cast<const Payload&>(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<const Payload&>(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<const Payload&>(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<const Payload&>(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<const Payload&>(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<LoadedDicomResources> dicomDir)
+  {
+    dicomDirPath_ = dicomDirPath;
+    dicomDir_ = dicomDir;
+  }
+
+
+  boost::shared_ptr<IObserver> SeriesFramesLoader::Factory::Create(ILoadersContext::ILock& stone)
+  {
+    boost::shared_ptr<SeriesFramesLoader> loader(
+      new SeriesFramesLoader(stone.GetContext(), instances_, dicomDirPath_, dicomDir_));
+    loader->Register<GetOrthancImageCommand::SuccessMessage>(stone.GetOracleObservable(), &SeriesFramesLoader::Handle);
+    loader->Register<GetOrthancWebViewerJpegCommand::SuccessMessage>(stone.GetOracleObservable(), &SeriesFramesLoader::Handle);
+    loader->Register<HttpCommand::SuccessMessage>(stone.GetOracleObservable(), &SeriesFramesLoader::Handle);
+    loader->Register<OrthancRestApiCommand::SuccessMessage>(stone.GetOracleObservable(), &SeriesFramesLoader::Handle);
+
+#if ORTHANC_ENABLE_DCMTK == 1
+    loader->Register<ParseDicomSuccessMessage>(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::unique_ptr<Orthanc::IDynamicObject> 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::unique_ptr<ParseDicomFromFileCommand> command(new ParseDicomFromFileCommand(dicomDirPath_, file));
+        command->SetPixelDataIncluded(true);
+        command->AcquirePayload(new Payload(source, index, sopInstanceUid, quality, protection.release()));
+
+        {
+          std::unique_ptr<ILoadersContext::ILock> 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<std::string, std::string> arguments, headers;
+        arguments["window"] = (boost::lexical_cast<std::string>(c) + "," +
+                               boost::lexical_cast<std::string>(w) + ",linear");
+        headers["Accept"] = "image/jpeg";
+
+        std::unique_ptr<Payload> payload(new Payload(source, index, sopInstanceUid, quality, protection.release()));
+        payload->SetWindowing(c, w);
+
+        {
+          std::unique_ptr<ILoadersContext::ILock> 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::unique_ptr<Payload> payload(new Payload(source, index, sopInstanceUid, quality, protection.release()));
+
+        const std::map<std::string, std::string> empty;
+
+        std::unique_ptr<ParseDicomFromWadoCommand> command(
+          new ParseDicomFromWadoCommand(sopInstanceUid, source.CreateDicomWebCommand(uri, empty, empty, NULL)));
+        command->AcquirePayload(payload.release());
+
+        {
+          std::unique_ptr<ILoadersContext::ILock> 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::unique_ptr<GetOrthancWebViewerJpegCommand> command(new GetOrthancWebViewerJpegCommand);
+        command->SetInstance(orthancId);
+        command->SetExpectedPixelFormat(parameters.GetExpectedPixelFormat());
+        command->AcquirePayload(new Payload(source, index, sopInstanceUid, quality, protection.release()));
+
+        {
+          std::unique_ptr<ILoadersContext::ILock> 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::unique_ptr<GetOrthancImageCommand> 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::unique_ptr<ILoadersContext::ILock> lock(context_.Lock());
+          lock->Schedule(GetSharedObserver(), priority, command.release());
+        }
+      }
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/SeriesFramesLoader.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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<SeriesFramesLoader>,
+    public IObservable
+  {
+  private:
+    class Payload;
+
+    ILoadersContext&                         context_;
+    SeriesOrderedFrames                      frames_;
+    std::string                              dicomDirPath_;
+    boost::shared_ptr<LoadedDicomResources>  dicomDir_;
+
+    SeriesFramesLoader(ILoadersContext& context,
+                       LoadedDicomResources& instances,
+                       const std::string& dicomDirPath,
+                       boost::shared_ptr<LoadedDicomResources> 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<std::string, std::string>& 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<SeriesFramesLoader>
+    {
+      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<LoadedDicomResources>  dicomDir_;
+
+    public:
+      // No "const" because "LoadedDicomResources::GetResource()" will call "Flatten()"
+      Factory(LoadedDicomResources& instances) :
+      instances_(instances)
+      {
+      }
+
+      void SetDicomDir(const std::string& dicomDirPath,
+                       boost::shared_ptr<LoadedDicomResources> dicomDir);
+
+      virtual boost::shared_ptr<IObserver> 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 */);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/SeriesMetadataLoader.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "SeriesMetadataLoader.h"
+
+#include <Core/DicomFormat/DicomInstanceHasher.h>
+
+namespace OrthancStone
+{
+  SeriesMetadataLoader::SeriesMetadataLoader(boost::shared_ptr<DicomResourcesLoader>& 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<std::string, int>::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<OrthancStone::LoadedDicomResources> 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<std::string, unsigned int>::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<std::string, unsigned int>::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<IObserver> SeriesMetadataLoader::Factory::Create(ILoadersContext::ILock& context)
+  {
+    DicomResourcesLoader::Factory factory;
+    boost::shared_ptr<DicomResourcesLoader> loader
+      (boost::dynamic_pointer_cast<DicomResourcesLoader>(factory.Create(context)));
+      
+    boost::shared_ptr<SeriesMetadataLoader> obj(new SeriesMetadataLoader(loader));
+    obj->Register<DicomResourcesLoader::SuccessMessage>(*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<LoadedDicomResources> 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<LoadedDicomResources> 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<LoadedDicomResources> dicomDir
+      (new LoadedDicomResources(Orthanc::DICOM_TAG_REFERENCED_SOP_INSTANCE_UID_IN_FILE));
+    loader_->ScheduleLoadDicomDir(dicomDir, priority, source, path,
+                                  NULL /* TODO PAYLOAD */);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/SeriesMetadataLoader.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "DicomResourcesLoader.h"
+
+namespace OrthancStone
+{
+  class SeriesMetadataLoader :
+    public ObserverBase<SeriesMetadataLoader>,
+    public IObservable
+  {
+  private:
+    enum State
+    {
+      State_Setup,
+      State_Default,
+      State_DicomDir,
+      State_DicomFile
+    };
+
+    typedef std::map<std::string, boost::shared_ptr<LoadedDicomResources> >  Series;
+
+    boost::shared_ptr<DicomResourcesLoader>  loader_;
+    State                                    state_;
+    std::map<std::string, int>               scheduled_;   // Maps a "SeriesInstanceUID" to a priority
+    Series                                   series_;
+    boost::shared_ptr<LoadedDicomResources>  dicomDir_;
+    std::string                              dicomDirPath_;
+    std::map<std::string, unsigned int>      seriesSize_;
+
+    SeriesMetadataLoader(boost::shared_ptr<DicomResourcesLoader>& loader);
+
+    bool IsScheduledWithHigherPriority(const std::string& seriesInstanceUid,
+                                       int priority) const;
+
+    void Handle(const DicomResourcesLoader::SuccessMessage& message);
+
+  public:
+    class SeriesLoadedMessage : public OriginMessage<SeriesMetadataLoader>
+    {
+      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<LoadedDicomResources>  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<LoadedDicomResources> dicomDir)
+      {
+        dicomDirPath_ = dicomDirPath;
+        dicomDir_ = dicomDir;
+      }
+
+      const std::string& GetDicomDirPath() const
+      {
+        return dicomDirPath_;
+      }
+
+      // Will be NULL on non-DICOMDIR sources
+      boost::shared_ptr<LoadedDicomResources> GetDicomDir() const
+      {
+        return dicomDir_;
+      }
+    };
+
+  
+    class Factory : public ILoaderFactory
+    {
+    public:
+      virtual boost::shared_ptr<IObserver> Create(ILoadersContext::ILock& context);
+    };
+
+  
+    class Accessor : public boost::noncopyable
+    {
+    private:
+      boost::shared_ptr<LoadedDicomResources>  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);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/SeriesOrderedFrames.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -0,0 +1,345 @@
+/**
+ * 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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../Toolbox/SlicesSorter.h"
+#include "SeriesOrderedFrames.h"
+
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  class SeriesOrderedFrames::Instance : public boost::noncopyable
+  {
+  private:
+    std::unique_ptr<Orthanc::DicomMap>  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<int>(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<int>::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<Instance*>(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<Instance*>& payload =
+          dynamic_cast<const Orthanc::SingleValueObject<Instance*>&>(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(), 
+                                             static_cast<unsigned int>(j)));
+        }
+      }
+
+      isRegular_ = sorter.ComputeSpacingBetweenSlices(spacingBetweenSlices_);
+      return true;
+    }
+  }
+
+
+  void SeriesOrderedFrames::SortIndexInSeries()
+  {
+    std::vector<InstanceWithIndexInSeries> 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(), 
+                                           static_cast<unsigned int>(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::unique_ptr<Instance> 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);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/SeriesOrderedFrames.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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<Instance*>  instances_;
+    std::vector<Frame*>     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;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/SeriesThumbnailsLoader.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "SeriesThumbnailsLoader.h"
+
+#include <Core/DicomFormat/DicomMap.h>
+#include <Core/DicomFormat/DicomInstanceHasher.h>
+#include <Core/Images/ImageProcessing.h>
+#include <Core/Images/JpegWriter.h>
+#include <Core/OrthancException.h>
+
+#include <boost/algorithm/string/predicate.hpp>
+
+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::unique_ptr<Thumbnail> 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<SeriesThumbnailsLoader>  loader_;
+    DicomSource                                source_;
+    std::string                                studyInstanceUid_;
+    std::string                                seriesInstanceUid_;
+
+  public:
+    Handler(boost::shared_ptr<SeriesThumbnailsLoader> 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<SeriesThumbnailsLoader> 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<std::string, std::string>& 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<SeriesThumbnailsLoader> 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<std::string, std::string>& 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<SeriesThumbnailsLoader> 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<std::string, std::string>& headers)
+    {
+      std::string mime = Orthanc::MIME_JPEG;
+      for (std::map<std::string, std::string>::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<std::string, std::string> arguments, headers;
+      arguments["0020000D"] = GetStudyInstanceUid();
+      arguments["0020000E"] = GetSeriesInstanceUid();
+      arguments["includefield"] = "00080016";
+
+      std::unique_ptr<IOracleCommand> 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<SeriesThumbnailsLoader> 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<std::string, std::string>& headers)
+    {
+      SeriesThumbnailType type = ExtractSopClassUid(body);
+
+      if (type == SeriesThumbnailType_Pdf ||
+          type == SeriesThumbnailType_Video)
+      {
+        GetLoader()->AcquireThumbnail(GetSource(), GetStudyInstanceUid(),
+                                      GetSeriesInstanceUid(), new Thumbnail(type));
+      }
+      else
+      {
+        std::unique_ptr<GetOrthancImageCommand> 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<SeriesThumbnailsLoader> 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<std::string, std::string>& 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<std::string, std::string> arguments, headers;
+          arguments["quality"] = boost::lexical_cast<std::string>(JPEG_QUALITY);
+          headers["Accept"] = Orthanc::MIME_JPEG;
+
+          const std::string instance = json[INSTANCES][index].asString();
+
+          std::unique_ptr<OrthancRestApiCommand> 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::unique_ptr<ILoadersContext::ILock> lock(context_.Lock());
+    lock->Schedule(GetSharedObserver(), priority_, command);
+  }    
+
+  
+  void SeriesThumbnailsLoader::Handle(const HttpCommand::SuccessMessage& message)
+  {
+    assert(message.GetOrigin().HasPayload());
+    dynamic_cast<Handler&>(message.GetOrigin().GetPayload()).HandleSuccess(message.GetAnswer(), message.GetAnswerHeaders());
+  }
+
+
+  void SeriesThumbnailsLoader::Handle(const OrthancRestApiCommand::SuccessMessage& message)
+  {
+    assert(message.GetOrigin().HasPayload());
+    dynamic_cast<Handler&>(message.GetOrigin().GetPayload()).HandleSuccess(message.GetAnswer(), message.GetAnswerHeaders());
+  }
+
+
+  void SeriesThumbnailsLoader::Handle(const GetOrthancImageCommand::SuccessMessage& message)
+  {
+    assert(message.GetOrigin().HasPayload());
+
+    std::unique_ptr<Orthanc::ImageAccessor> 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<ThumbnailInformation&>(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<const OracleCommandBase&>(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<const ThumbnailInformation&>(command.GetPayload());
+      AcquireThumbnail(info.GetDicomSource(), info.GetStudyInstanceUid(),
+                       info.GetSeriesInstanceUid(), new Thumbnail(SeriesThumbnailType_Unsupported));
+    }
+    else
+    {
+      dynamic_cast<Handler&>(command.GetPayload()).HandleError();
+    }
+  }
+
+
+  SeriesThumbnailsLoader::SeriesThumbnailsLoader(ILoadersContext& context,
+                                                 int priority) :
+    context_(context),
+    priority_(priority),
+    width_(128),
+    height_(128)
+  {
+  }
+    
+  
+  boost::shared_ptr<IObserver> SeriesThumbnailsLoader::Factory::Create(ILoadersContext::ILock& stone)
+  {
+    boost::shared_ptr<SeriesThumbnailsLoader> result(new SeriesThumbnailsLoader(stone.GetContext(), priority_));
+    result->Register<GetOrthancImageCommand::SuccessMessage>(stone.GetOracleObservable(), &SeriesThumbnailsLoader::Handle);
+    result->Register<HttpCommand::SuccessMessage>(stone.GetOracleObservable(), &SeriesThumbnailsLoader::Handle);
+    result->Register<OracleCommandExceptionMessage>(stone.GetOracleObservable(), &SeriesThumbnailsLoader::Handle);
+    result->Register<OrthancRestApiCommand::SuccessMessage>(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<std::string, std::string> arguments, headers;
+      arguments["viewport"] = (boost::lexical_cast<std::string>(width_) + "," +
+                               boost::lexical_cast<std::string>(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::unique_ptr<IOracleCommand> 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::unique_ptr<OrthancRestApiCommand> 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");
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/SeriesThumbnailsLoader.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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<SeriesThumbnailsLoader>
+  {
+  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<SeriesThumbnailsLoader>
+    {
+      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<std::string, Thumbnail*>  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<IObserver> 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);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/WebAssemblyLoadersContext.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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<IObserver> receiver,
+                          int priority,
+                          IOracleCommand* command /* Takes ownership */) ORTHANC_OVERRIDE
+    {
+      that_.scheduler_->Schedule(receiver, priority, command);
+    }
+
+    virtual void CancelRequests(boost::shared_ptr<IObserver> receiver) ORTHANC_OVERRIDE
+    {
+      that_.scheduler_->CancelRequests(receiver);
+    }
+
+    virtual void CancelAllRequests() ORTHANC_OVERRIDE
+    {
+      that_.scheduler_->CancelAllRequests();
+    }
+
+    virtual void AddLoader(boost::shared_ptr<IObserver> 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);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/WebAssemblyLoadersContext.h	Tue Mar 31 15:51:02 2020 +0200
@@ -0,0 +1,63 @@
+/**
+ * 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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ILoadersContext.h"
+#include "../Oracle/WebAssemblyOracle.h"
+#include "OracleScheduler.h"
+
+#include <list>
+
+namespace OrthancStone
+{
+  class WebAssemblyLoadersContext : public ILoadersContext
+  {
+  private:
+    class Locker;
+    
+    WebAssemblyOracle                          oracle_;
+    boost::shared_ptr<OracleScheduler>         scheduler_;
+    std::list< boost::shared_ptr<IObserver> >  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;
+  };
+}
--- a/Framework/Messages/ICallable.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Messages/ICallable.h	Tue Mar 31 15:51:02 2020 +0200
@@ -22,23 +22,21 @@
 #pragma once
 
 #include "IMessage.h"
+#include "IObserver.h"
 
 #include <Core/Logging.h>
 
 #include <boost/noncopyable.hpp>
+#include <boost/weak_ptr.hpp>
 
 #include <string>
 #include <stdint.h>
 
-#include <stdint.h>
-
-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:
@@ -50,61 +48,38 @@
 
     virtual const MessageIdentifier& GetMessageIdentifier() = 0;
 
-    virtual IObserver* GetObserver() const = 0;
-  };
-
-  template <typename TMessage>
-  class MessageHandler: public ICallable
-  {
+    // TODO - Is this needed?
+    virtual boost::weak_ptr<IObserver> GetObserver() const = 0;
   };
 
 
   template <typename TObserver,
             typename TMessage>
-  class Callable : public MessageHandler<TMessage>
+  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<IObserver>  observer_;
+    MemberMethod                function_;
 
   public:
-    Callable(TObserver& observer,
-             MemberFunction function) :
+    Callable(boost::shared_ptr<TObserver> 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<const TMessage&>(message));
+      boost::shared_ptr<IObserver> lock(observer_);
+      if (lock)
+      {
+        TObserver& observer = dynamic_cast<TObserver&>(*lock);
+        const TMessage& typedMessage = dynamic_cast<const TMessage&>(message);
+        (observer.*function_) (typedMessage);
+      }
     }
 
     virtual const MessageIdentifier& GetMessageIdentifier()
@@ -112,41 +87,9 @@
       return TMessage::GetStaticIdentifier();
     }
 
-    virtual IObserver* GetObserver() const
+    virtual boost::weak_ptr<IObserver> GetObserver() const
     {
-      return &observer_;
+      return observer_;
     }
   };
-
-#if 0 /* __cplusplus >= 201103L*/
-
-#include <functional>
-
-  template <typename TMessage>
-  class LambdaCallable : public MessageHandler<TMessage>
-  {
-  private:
-
-    IObserver&      observer_;
-    std::function<void (const TMessage&)> lambda_;
-
-  public:
-    LambdaCallable(IObserver& observer,
-                    std::function<void (const TMessage&)> lambdaFunction) :
-             observer_(observer),
-             lambda_(lambdaFunction)
-    {
-    }
-
-    virtual void Apply(const IMessage& message)
-    {
-      lambda_(dynamic_cast<const TMessage&>(message));
-    }
-
-    virtual IObserver* GetObserver() const
-    {
-      return &observer_;
-    }
-  };
-#endif //__cplusplus >= 201103L
 }
--- a/Framework/Messages/IMessage.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Messages/IMessage.h	Tue Mar 31 15:51:02 2020 +0200
@@ -21,6 +21,7 @@
 
 #pragma once
 
+#include <boost/lexical_cast.hpp>
 #include <boost/noncopyable.hpp>
 
 #include <string.h>
@@ -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<std::string>(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);
+    }
   };
 
     
--- a/Framework/Messages/IMessageEmitter.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Messages/IMessageEmitter.h	Tue Mar 31 15:51:02 2020 +0200
@@ -24,6 +24,8 @@
 #include "IObserver.h"
 #include "IMessage.h"
 
+#include <boost/weak_ptr.hpp>
+
 namespace OrthancStone
 {
   /**
@@ -39,7 +41,7 @@
     {
     }
 
-    virtual void EmitMessage(const IObserver& observer,
+    virtual void EmitMessage(boost::weak_ptr<IObserver> observer,
                              const IMessage& message) = 0;
   };
 }
--- a/Framework/Messages/IObservable.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Messages/IObservable.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -21,8 +21,9 @@
 
 #include "IObservable.h"
 
+#include "../StoneException.h"
+
 #include <Core/Logging.h>
-#include <Core/OrthancException.h>
 
 #include <cassert>
 
@@ -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<IObserver&>(*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<ICallable*>::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<IObserver> 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<IObserver> 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<IObserver> lock(observer.lock());
+    if (lock)
     {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      EmitMessageInternal(lock.get(), message);
     }
-    
-    forwarders_.insert(forwarder);
   }
 }
--- a/Framework/Messages/IObservable.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Messages/IObservable.h	Tue Mar 31 15:51:02 2020 +0200
@@ -24,8 +24,6 @@
 #include "../StoneEnumerations.h"
 #include "ICallable.h"
 #include "IObserver.h"
-#include "MessageBroker.h"
-#include "MessageForwarder.h"
 
 #include <set>
 #include <map>
@@ -37,39 +35,20 @@
   private:
     typedef std::map<MessageIdentifier, std::set<ICallable*> >  Callables;
 
-    typedef std::set<IMessageForwarder*>     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 ownership of the callable
+    void RegisterCallable(ICallable* callable);
 
     void BroadcastMessage(const IMessage& message);
 
-    void EmitMessage(const IObserver& observer,
+    void EmitMessage(boost::weak_ptr<IObserver> observer,
                      const IMessage& message);
-
-    // Takes ownsership
-    void RegisterForwarder(IMessageForwarder* forwarder);
   };
 }
--- a/Framework/Messages/IObserver.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,94 +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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "IObserver.h"
-
-#include "IMessage.h"
-#include "../StoneException.h"
-
-#include <Core/Logging.h>
-#include <Core/Toolbox.h>
-
-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
-  {
-    bool ok = (fingerprint_[0] >= IObserver_FIRST_UNIQUE_ID) &&
-      (fingerprint_[1] == fingerprint_[0] / 2) &&
-      (fingerprint_[2] == fingerprint_[1] + IObserver_UNIQUE_ID_MAGIC_NUMBER);
-    if(!ok) 
-    {
-      LOG(INFO) << "Fingerprint not valid: " << " fingerprint_[0] = " << fingerprint_[0] << " fingerprint_[1] = " << fingerprint_[1]<< " fingerprint_[2] = " << fingerprint_[2];
-    }
-    return ok;
-  }
-}
--- a/Framework/Messages/IObserver.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Messages/IObserver.h	Tue Mar 31 15:51:02 2020 +0200
@@ -21,7 +21,7 @@
 
 #pragma once
 
-#include "MessageBroker.h"
+#include <boost/noncopyable.hpp>
 
 #include <stdint.h>
 
@@ -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_;
     }
   };
 }
--- a/Framework/Messages/LockingEmitter.h	Tue Mar 31 15:47:29 2020 +0200
+++ /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 <http://www.gnu.org/licenses/>.
- **/
-
-#pragma once
-
-#include <Core/Enumerations.h>
-#include <Core/OrthancException.h>
-
-#include "IMessageEmitter.h"
-#include "IObservable.h"
-
-#include <boost/thread.hpp>
-
-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<boost::shared_mutex>  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<boost::shared_mutex>  lock_;
-
-    public:
-      ReaderLock(LockingEmitter& that) :
-        that_(that),
-        lock_(that.mutex_)
-      {
-      }
-    };
-
-
-    class WriterLock : public boost::noncopyable
-    {
-    private:
-      LockingEmitter& that_;
-      boost::unique_lock<boost::shared_mutex>  lock_;
-
-    public:
-      WriterLock(LockingEmitter& that) :
-        that_(that),
-        lock_(that.mutex_)
-      {
-      }
-
-      MessageBroker& GetBroker()
-      {
-        return that_.broker_;
-      }
-
-      IObservable& GetOracleObservable()
-      {
-        return that_.oracleObservable_;
-      }
-    };
-  };
-}
--- a/Framework/Messages/MessageBroker.h	Tue Mar 31 15:47:29 2020 +0200
+++ /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 <http://www.gnu.org/licenses/>.
- **/
-
-#pragma once
-
-#include "boost/noncopyable.hpp"
-
-#include <set>
-
-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<const IObserver*> 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();
-    }
-  };
-}
--- a/Framework/Messages/MessageForwarder.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ /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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "MessageForwarder.h"
-
-#include "IObservable.h"
-
-namespace OrthancStone
-{
-
-  void IMessageForwarder::ForwardMessageInternal(const IMessage& message)
-  {
-    emitter_.BroadcastMessage(message);
-  }
-
-  void IMessageForwarder::RegisterForwarderInEmitter()
-  {
-    emitter_.RegisterForwarder(this);
-  }
-}
--- a/Framework/Messages/MessageForwarder.h	Tue Mar 31 15:47:29 2020 +0200
+++ /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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "ICallable.h"
-#include "IObserver.h"
-
-#include <boost/noncopyable.hpp>
-
-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<A::MessageType>(broker, *this)  // where "this" is B
-   *
-   * in C:
-   * B.RegisterObserverCallback(new Callable<C, A:MessageTyper>(*this, &B::MyCallback))   // where "this" is C
-   */
-  template<typename TMessage>
-  class MessageForwarder : public IMessageForwarder, public Callable<MessageForwarder<TMessage>, TMessage>
-  {
-  public:
-    MessageForwarder(MessageBroker& broker,
-                     IObservable& emitter // the object that will emit the messages to forward
-                     )
-      : IMessageForwarder(broker, emitter),
-        Callable<MessageForwarder<TMessage>, TMessage>(*this, &MessageForwarder::ForwardMessage)
-    {
-      RegisterForwarderInEmitter();
-    }
-
-protected:
-    void ForwardMessage(const TMessage& message)
-    {
-      ForwardMessageInternal(message);
-    }
-
-  };
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Messages/ObserverBase.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ICallable.h"
+#include "IObserver.h"
+#include "IObservable.h"
+
+#include <Core/OrthancException.h>
+
+#include <boost/enable_shared_from_this.hpp>
+
+namespace OrthancStone 
+{
+  template <typename TObserver>
+  class ObserverBase : 
+    public IObserver,
+    public boost::enable_shared_from_this<TObserver>
+  {
+  public:
+    boost::shared_ptr<TObserver> 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 <typename TMessage>
+    ICallable* CreateCallable(void (TObserver::* MemberMethod) (const TMessage&))
+    {
+      return new Callable<TObserver, TMessage>(GetSharedObserver(), MemberMethod);
+    }
+
+    template <typename TMessage>
+    void Register(IObservable& observable,
+                  void (TObserver::* MemberMethod) (const TMessage&))
+    {
+      observable.RegisterCallable(CreateCallable(MemberMethod));
+    }
+  };
+}
--- a/Framework/OpenGL/OpenGLIncludes.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/OpenGL/OpenGLIncludes.h	Tue Mar 31 15:51:02 2020 +0200
@@ -32,6 +32,8 @@
 #if defined(__APPLE__)
 #  include <OpenGL/gl.h>
 #  include <OpenGL/glext.h>
+#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 <GL/glew.h>
--- a/Framework/OpenGL/SdlOpenGLContext.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/OpenGL/SdlOpenGLContext.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -68,6 +68,7 @@
         GLenum err = glewInit();
         if (GLEW_OK != err)
         {
+          LOG(ERROR) << glewGetErrorString(err);
           throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
                                           "Cannot initialize glew");
         }
--- a/Framework/OpenGL/SdlOpenGLContext.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/OpenGL/SdlOpenGLContext.h	Tue Mar 31 15:51:02 2020 +0200
@@ -26,6 +26,8 @@
 #include "IOpenGLContext.h"
 #include "../Viewport/SdlWindow.h"
 
+#include <SDL_render.h>
+
 #include <Core/Enumerations.h>
 
 namespace OrthancStone
@@ -62,6 +64,11 @@
     virtual unsigned int GetCanvasWidth() const ORTHANC_OVERRIDE;
 
     virtual unsigned int GetCanvasHeight() const ORTHANC_OVERRIDE;
+
+    void ToggleMaximize()
+    {
+      window_.ToggleMaximize();
+    }
   };
 }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Oracle/GenericOracleRunner.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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 <dcmtk/dcmdata/dcdeftag.h>
+#  include <dcmtk/dcmdata/dcfilefo.h>
+static unsigned int BUCKET_DICOMDIR = 0;
+static unsigned int BUCKET_SOP = 1;
+#endif
+
+#include <Core/Compression/GzipCompressor.h>
+#include <Core/HttpClient.h>
+#include <Core/OrthancException.h>
+#include <Core/Toolbox.h>
+#include <Core/SystemToolbox.h>
+
+#include <boost/filesystem.hpp>
+
+
+
+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<IObserver> 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<IObserver> 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<IObserver> 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<IObserver> 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<IObserver> 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<uint64_t>(static_cast<size_t>(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::unique_ptr<Orthanc::ParsedDicomFile> 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<IObserver> receiver,
+                          IMessageEmitter& emitter,
+                          boost::shared_ptr<ParsedDicomCache> 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::unique_ptr<Orthanc::ParsedDicomFile> parsed(ParseDicom(fileSize, path, command.IsPixelDataIncluded()));
+
+    if (fileSize != static_cast<size_t>(fileSize))
+    {
+      // Cannot load such a large file on 32-bit architecture
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotEnoughMemory);
+    }
+    
+    {
+      ParseDicomSuccessMessage message
+        (command, *parsed, static_cast<size_t>(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<size_t>(fileSize), command.IsPixelDataIncluded());
+    }
+  }
+
+  
+  static void RunInternal(boost::weak_ptr<IObserver> receiver,
+                          IMessageEmitter& emitter,
+                          boost::shared_ptr<ParsedDicomCache> 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<const HttpCommand&>(command.GetRestCommand()));
+        break;
+        
+      case IOracleCommand::Type_OrthancRestApi:
+        RunOrthancRestApiCommand(answer, answerHeaders, orthanc,
+                                 dynamic_cast<const OrthancRestApiCommand&>(command.GetRestCommand()));
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+
+    size_t fileSize;
+    std::unique_ptr<Orthanc::ParsedDicomFile> 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<IObserver> 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<const HttpCommand&>(command));
+          break;
+
+        case IOracleCommand::Type_OrthancRestApi:
+          RunInternal(receiver, emitter, orthanc_,
+                      dynamic_cast<const OrthancRestApiCommand&>(command));
+          break;
+
+        case IOracleCommand::Type_GetOrthancImage:
+          RunInternal(receiver, emitter, orthanc_,
+                      dynamic_cast<const GetOrthancImageCommand&>(command));
+          break;
+
+        case IOracleCommand::Type_GetOrthancWebViewerJpeg:
+          RunInternal(receiver, emitter, orthanc_,
+                      dynamic_cast<const GetOrthancWebViewerJpegCommand&>(command));
+          break;
+
+        case IOracleCommand::Type_ReadFile:
+          RunInternal(receiver, emitter, rootDirectory_,
+                      dynamic_cast<const ReadFileCommand&>(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<const ParseDicomFromFileCommand&>(command));
+              break;
+
+            case IOracleCommand::Type_ParseDicomFromWado:
+              RunInternal(receiver, emitter, dicomCache_, orthanc_,
+                          dynamic_cast<const ParseDicomFromWadoCommand&>(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);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Oracle/GenericOracleRunner.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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 <Core/Enumerations.h>  // For ORTHANC_OVERRIDE
+#include <Core/WebServiceParameters.h>
+
+namespace OrthancStone
+{
+  class GenericOracleRunner : public boost::noncopyable
+  {
+  private:
+    Orthanc::WebServiceParameters  orthanc_;
+    std::string                    rootDirectory_;
+
+#if ORTHANC_ENABLE_DCMTK == 1
+    boost::shared_ptr<ParsedDicomCache>  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<ParsedDicomCache> cache)
+    {
+      dicomCache_ = cache;
+    }
+#endif
+
+    void Run(boost::weak_ptr<IObserver> receiver,
+             IMessageEmitter& emitter,
+             const IOracleCommand& command);
+  };
+}
--- a/Framework/Oracle/GetOrthancImageCommand.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Oracle/GetOrthancImageCommand.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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<std::string>(frame) + "/" + GetFormatSuffix(pixelFormat));
+  }
+
+
+  void GetOrthancImageCommand::ProcessHttpAnswer(boost::weak_ptr<IObserver> 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);
   }
 }
--- a/Framework/Oracle/GetOrthancImageCommand.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Oracle/GetOrthancImageCommand.h	Tue Mar 31 15:51:02 2020 +0200
@@ -22,7 +22,7 @@
 #pragma once
 
 #include "../Messages/IMessageEmitter.h"
-#include "OracleCommandWithPayload.h"
+#include "OracleCommandBase.h"
 
 #include <Core/Images/ImageAccessor.h>
 
@@ -30,7 +30,7 @@
 
 namespace OrthancStone
 {
-  class GetOrthancImageCommand : public OracleCommandWithPayload
+  class GetOrthancImageCommand : public OracleCommandBase
   {
   public:
     typedef std::map<std::string, std::string>  HttpHeaders;
@@ -40,17 +40,22 @@
       ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
       
     private:
-      std::unique_ptr<Orthanc::ImageAccessor>  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<IObserver> receiver,
+                           IMessageEmitter& emitter,
                            const std::string& answer,
                            const HttpHeaders& answerHeaders) const;
   };
--- a/Framework/Oracle/GetOrthancWebViewerJpegCommand.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Oracle/GetOrthancWebViewerJpegCommand.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -29,23 +29,16 @@
 #include <Core/OrthancException.h>
 #include <Core/Toolbox.h>
 
+#ifdef _MSC_VER
+// 'Json::Reader': Use CharReader and CharReaderBuilder instead
+#pragma warning(disable:4996)
+#endif
+
 #include <json/reader.h>
 #include <json/value.h>
 
 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 +69,8 @@
   }
 
 
-  void GetOrthancWebViewerJpegCommand::ProcessHttpAnswer(IMessageEmitter& emitter,
-                                                         const IObserver& receiver,
+  void GetOrthancWebViewerJpegCommand::ProcessHttpAnswer(boost::weak_ptr<IObserver> receiver,
+                                                         IMessageEmitter& emitter,
                                                          const std::string& answer) const
   {
     // This code comes from older "OrthancSlicesLoader::ParseSliceImageJpeg()"
@@ -149,7 +142,7 @@
       }
       else
       {
-        SuccessMessage message(*this, reader.release());
+        SuccessMessage message(*this, *reader);
         emitter.EmitMessage(receiver, message);
         return;
       }
@@ -168,7 +161,7 @@
       }
       else
       {
-        SuccessMessage message(*this, reader.release());
+        SuccessMessage message(*this, *reader);
         emitter.EmitMessage(receiver, message);
         return;
       }
@@ -210,8 +203,8 @@
       float offset = static_cast<float>(stretchLow) / scaling;
       Orthanc::ImageProcessing::ShiftScale(*image, offset, scaling, true);
     }
-    
-    SuccessMessage message(*this, image.release());
+
+    SuccessMessage message(*this, *image);
     emitter.EmitMessage(receiver, message);
   }
 }
--- a/Framework/Oracle/GetOrthancWebViewerJpegCommand.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Oracle/GetOrthancWebViewerJpegCommand.h	Tue Mar 31 15:51:02 2020 +0200
@@ -22,7 +22,7 @@
 #pragma once
 
 #include "../Messages/IMessageEmitter.h"
-#include "OracleCommandWithPayload.h"
+#include "OracleCommandBase.h"
 
 #include <Core/Images/ImageAccessor.h>
 
@@ -30,7 +30,7 @@
 
 namespace OrthancStone
 {
-  class GetOrthancWebViewerJpegCommand : public OracleCommandWithPayload
+  class GetOrthancWebViewerJpegCommand : public OracleCommandBase
   {
   public:
     typedef std::map<std::string, std::string>  HttpHeaders;
@@ -40,15 +40,19 @@
       ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
       
     private:
-      std::unique_ptr<Orthanc::ImageAccessor>  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<IObserver> receiver,
+                           IMessageEmitter& emitter,
                            const std::string& answer) const;
   };
 }
--- a/Framework/Oracle/HttpCommand.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Oracle/HttpCommand.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -23,21 +23,16 @@
 
 #include <Core/OrthancException.h>
 
+#ifdef _MSC_VER
+// 'Json::Reader': Use CharReader and CharReaderBuilder instead
+#pragma warning(disable:4996)
+#endif
+
 #include <json/reader.h>
 #include <json/writer.h>
 
 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 +71,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);
+    }
+  }
 }
--- a/Framework/Oracle/HttpCommand.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Oracle/HttpCommand.h	Tue Mar 31 15:51:02 2020 +0200
@@ -22,7 +22,7 @@
 #pragma once
 
 #include "../Messages/IMessage.h"
-#include "OracleCommandWithPayload.h"
+#include "OracleCommandBase.h"
 
 #include <Core/Enumerations.h>
 
@@ -31,7 +31,7 @@
 
 namespace OrthancStone
 {
-  class HttpCommand : public OracleCommandWithPayload
+  class HttpCommand : public OracleCommandBase
   {
   public:
     typedef std::map<std::string, std::string>  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;
   };
 }
--- a/Framework/Oracle/IOracle.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Oracle/IOracle.h	Tue Mar 31 15:51:02 2020 +0200
@@ -24,6 +24,8 @@
 #include "../Messages/IObserver.h"
 #include "IOracleCommand.h"
 
+#include <boost/shared_ptr.hpp>
+
 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<IObserver> receiver,
                           IOracleCommand* command) = 0;  // Takes ownership
   };
 }
--- a/Framework/Oracle/IOracleCommand.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Oracle/IOracleCommand.h	Tue Mar 31 15:51:02 2020 +0200
@@ -21,7 +21,7 @@
 
 #pragma once
 
-#include <boost/noncopyable.hpp>
+#include <Core/IDynamicObject.h>
 
 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;
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Oracle/OracleCommandBase.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OracleCommandBase.h"
+
+#include <Core/OrthancException.h>
+
+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);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Oracle/OracleCommandBase.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IOracleCommand.h"
+
+#include <Core/Compatibility.h>
+#include <Core/IDynamicObject.h>
+
+#include <memory>
+
+namespace OrthancStone
+{
+  class OracleCommandBase : public IOracleCommand
+  {
+  private:
+    std::unique_ptr<Orthanc::IDynamicObject>  payload_;
+
+  public:
+    void AcquirePayload(Orthanc::IDynamicObject* payload);
+
+    virtual bool HasPayload() const
+    {
+      return (payload_.get() != NULL);
+    }
+
+    virtual Orthanc::IDynamicObject& GetPayload() const;
+
+    Orthanc::IDynamicObject* ReleasePayload();
+  };
+}
--- a/Framework/Oracle/OracleCommandExceptionMessage.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Oracle/OracleCommandExceptionMessage.h	Tue Mar 31 15:51:02 2020 +0200
@@ -28,34 +28,28 @@
 
 namespace OrthancStone
 {
-  class OracleCommandExceptionMessage : public IMessage
+  class OracleCommandExceptionMessage : public OriginMessage<IOracleCommand>
   {
     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_;
--- a/Framework/Oracle/OracleCommandWithPayload.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ /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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "OracleCommandWithPayload.h"
-
-#include <Core/OrthancException.h>
-
-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);
-    }
-  }
-}
--- a/Framework/Oracle/OracleCommandWithPayload.h	Tue Mar 31 15:47:29 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,50 +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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "IOracleCommand.h"
-
-#include <Core/Compatibility.h>
-#include <Core/IDynamicObject.h>
-
-#include <memory>
-
-namespace OrthancStone
-{
-  class OracleCommandWithPayload : public IOracleCommand
-  {
-  private:
-    std::unique_ptr<Orthanc::IDynamicObject>  payload_;
-
-  public:
-    void SetPayload(Orthanc::IDynamicObject* payload);
-
-    bool HasPayload() const
-    {
-      return (payload_.get() != NULL);
-    }
-
-    Orthanc::IDynamicObject& GetPayload() const;
-
-    Orthanc::IDynamicObject* ReleasePayload();
-  };
-}
--- a/Framework/Oracle/OrthancRestApiCommand.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Oracle/OrthancRestApiCommand.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -23,21 +23,16 @@
 
 #include <Core/OrthancException.h>
 
+#ifdef _MSC_VER
+// 'Json::Reader': Use CharReader and CharReaderBuilder instead
+#pragma warning(disable:4996)
+#endif
+
 #include <json/reader.h>
 #include <json/writer.h>
 
 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 +46,8 @@
   OrthancRestApiCommand::OrthancRestApiCommand() :
     method_(Orthanc::HttpMethod_Get),
     uri_("/"),
-    timeout_(600)
+    timeout_(600),
+    applyPlugins_(false)
   {
   }
 
--- a/Framework/Oracle/OrthancRestApiCommand.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Oracle/OrthancRestApiCommand.h	Tue Mar 31 15:51:02 2020 +0200
@@ -22,7 +22,7 @@
 #pragma once
 
 #include "../Messages/IMessage.h"
-#include "OracleCommandWithPayload.h"
+#include "OracleCommandBase.h"
 
 #include <Core/Enumerations.h>
 
@@ -31,7 +31,7 @@
 
 namespace OrthancStone
 {
-  class OrthancRestApiCommand : public OracleCommandWithPayload
+  class OrthancRestApiCommand : public OracleCommandBase
   {
   public:
     typedef std::map<std::string, std::string>  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_;
+    }
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Oracle/ParseDicomFromFileCommand.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "ParseDicomFromFileCommand.h"
+
+#include <Core/OrthancException.h>
+
+#include <boost/filesystem/path.hpp>
+
+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();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Oracle/ParseDicomFromFileCommand.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "OracleCommandBase.h"
+
+#include <string>
+
+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;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Oracle/ParseDicomFromWadoCommand.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "ParseDicomFromWadoCommand.h"
+
+#include <Core/OrthancException.h>
+
+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_;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Oracle/ParseDicomFromWadoCommand.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "OracleCommandBase.h"
+
+#include <string>
+
+namespace OrthancStone
+{
+  class ParseDicomFromWadoCommand : public OracleCommandBase
+  {
+  private:
+    std::string                    sopInstanceUid_;
+    std::unique_ptr<IOracleCommand>  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;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Oracle/ParseDicomSuccessMessage.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "ParseDicomSuccessMessage.h"
+
+#include <Core/DicomParsing/ParsedDicomFile.h>
+#include <Core/HttpServer/MultipartStreamReader.h>
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  class MultipartHandler : public Orthanc::MultipartStreamReader::IHandler
+  {
+  private:
+    std::unique_ptr<Orthanc::ParsedDicomFile>  dicom_;
+    size_t                                   size_;
+
+  public:
+    MultipartHandler() :
+      size_(0)
+    {
+    }
+      
+    virtual void HandlePart(const std::map<std::string, std::string>& 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<std::string, std::string>& 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");
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Oracle/ParseDicomSuccessMessage.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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 <map>
+
+namespace Orthanc
+{
+  class ParsedDicomFile;
+}
+
+namespace OrthancStone
+{
+  class ParseDicomSuccessMessage : public OriginMessage<OracleCommandBase>
+  {
+    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<std::string, std::string>& headers);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Oracle/ReadFileCommand.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Messages/IMessage.h"
+#include "OracleCommandBase.h"
+
+namespace OrthancStone
+{
+  class ReadFileCommand : public OracleCommandBase
+  {
+  public:
+    class SuccessMessage : public OriginMessage<ReadFileCommand>
+    {
+      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_;
+    }
+  };
+}
--- a/Framework/Oracle/SleepOracleCommand.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Oracle/SleepOracleCommand.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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_;
--- a/Framework/Oracle/ThreadedOracle.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Oracle/ThreadedOracle.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <Core/Compression/GzipCompressor.h>
-#include <Core/HttpClient.h>
+#include <Core/Logging.h>
 #include <Core/OrthancException.h>
-#include <Core/Toolbox.h>
-
 
 namespace OrthancStone
 {
   class ThreadedOracle::Item : public Orthanc::IDynamicObject
   {
   private:
-    const IObserver&                receiver_;
+    boost::weak_ptr<IObserver>      receiver_;
     std::unique_ptr<IOracleCommand>   command_;
 
   public:
-    Item(const IObserver& receiver,
+    Item(boost::weak_ptr<IObserver> receiver,
          IOracleCommand* command) :
       receiver_(receiver),
       command_(command)
@@ -54,7 +46,7 @@
       }
     }
 
-    const IObserver& GetReceiver() const
+    boost::weak_ptr<IObserver> GetReceiver()
     {
       return receiver_;
     }
@@ -73,12 +65,12 @@
     class Item
     {
     private:
-      const IObserver&                   receiver_;
+      boost::weak_ptr<IObserver>         receiver_;
       std::unique_ptr<SleepOracleCommand>  command_;
       boost::posix_time::ptime           expiration_;
 
     public:
-      Item(const IObserver& receiver,
+      Item(boost::weak_ptr<IObserver> receiver,
            SleepOracleCommand* command) :
         receiver_(receiver),
         command_(command)
@@ -123,7 +115,7 @@
       }
     }
 
-    void Add(const IObserver& receiver,
+    void Add(boost::weak_ptr<IObserver> 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::unique_ptr<Orthanc::IDynamicObject>  object(queue_.Dequeue(100));
@@ -316,60 +160,37 @@
     {
       Item& item = dynamic_cast<Item&>(*object);
 
-      try
+      if (item.GetCommand().GetType() == IOracleCommand::Type_Sleep)
       {
-        switch (item.GetCommand().GetType())
+        SleepOracleCommand& command = dynamic_cast<SleepOracleCommand&>(item.GetCommand());
+          
+        std::unique_ptr<SleepOracleCommand> copy(new SleepOracleCommand(command.GetDelay()));
+          
+        if (command.HasPayload())
         {
-          case IOracleCommand::Type_Sleep:
-          {
-            SleepOracleCommand& command = dynamic_cast<SleepOracleCommand&>(item.GetCommand());
-
-            std::unique_ptr<SleepOracleCommand> 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<const HttpCommand&>(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<const OrthancRestApiCommand&>(item.GetCommand()));
-            break;
-
-          case IOracleCommand::Type_GetOrthancImage:
-            Execute(emitter_, orthanc_, item.GetReceiver(), 
-                    dynamic_cast<const GetOrthancImageCommand&>(item.GetCommand()));
-            break;
-
-          case IOracleCommand::Type_GetOrthancWebViewerJpeg:
-            Execute(emitter_, orthanc_, item.GetReceiver(), 
-                    dynamic_cast<const GetOrthancWebViewerJpegCommand&>(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<IObserver> receiver,
                                 IOracleCommand* command)
   {
-    queue_.Enqueue(new Item(receiver, command));
+    std::unique_ptr<Item> 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;
+      }
+    }
   }
 }
--- a/Framework/Oracle/ThreadedOracle.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Oracle/ThreadedOracle.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <Core/WebServiceParameters.h>
 #include <Core/MultiThreading/SharedMessageQueue.h>
 
 
@@ -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<ParsedDicomCache>  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<IObserver> receiver,
+                          IOracleCommand* command) ORTHANC_OVERRIDE;
   };
 }
--- a/Framework/Oracle/WebAssemblyOracle.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Oracle/WebAssemblyOracle.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <Core/OrthancException.h>
 #include <Core/Toolbox.h>
@@ -31,23 +36,18 @@
 #include <emscripten/html5.h>
 #include <emscripten/fetch.h>
 
-#if 0
-extern bool logbgo233;
-extern bool logbgo115;
-#endif
-
 namespace OrthancStone
 {
   class WebAssemblyOracle::TimeoutContext
   {
   private:
     WebAssemblyOracle&                 oracle_;
-    const IObserver&                   receiver_;
+    boost::weak_ptr<IObserver>         receiver_;
     std::unique_ptr<SleepOracleCommand>  command_;
 
   public:
     TimeoutContext(WebAssemblyOracle& oracle,
-                   const IObserver& receiver,
+                   boost::weak_ptr<IObserver> 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::unique_ptr<IOracleCommand> command_;
-    std::string                   expectedContentType_;
-    int64_t                       receiverFingerprint_;
+    WebAssemblyOracle&             oracle_;
+    boost::weak_ptr<IObserver>     receiver_;
+    std::unique_ptr<IOracleCommand>  command_;
+    std::string                    expectedContentType_;
 
   public:
     FetchContext(WebAssemblyOracle& oracle,
-                 const IObserver& receiver,
+                 boost::weak_ptr<IObserver> 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<IObserver> 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<T&>(*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::unique_ptr<Orthanc::ParsedDicomFile>& dicom,
+                      size_t fileSize)
     {
-      FetchContext* context = reinterpret_cast<FetchContext*>(fetch->userData);
-
-      const auto& command = context->GetTypedCommand<OrthancRestApiCommand>();
-      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::unique_ptr<FetchContext> context(reinterpret_cast<FetchContext*>(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::unique_ptr<FetchContext> context(reinterpret_cast<FetchContext*>(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<std::string> 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<HttpCommand>(), 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<OrthancRestApiCommand>(), headers, answer);
+              context->EmitMessage(message);
+              break;
+            }
+
+            case IOracleCommand::Type_GetOrthancImage:
             {
-              case IOracleCommand::Type_Http:
+              context->GetTypedCommand<GetOrthancImageCommand>().ProcessHttpAnswer
+                (context->GetReceiver(), context->GetEmitter(), answer, headers);
+              break;
+            }
+
+            case IOracleCommand::Type_GetOrthancWebViewerJpeg:
+            {
+              context->GetTypedCommand<GetOrthancWebViewerJpegCommand>().ProcessHttpAnswer
+                (context->GetReceiver(), context->GetEmitter(), answer);
+              break;
+            }
+
+            case IOracleCommand::Type_ParseDicomFromWado:
+            {
+#if ORTHANC_ENABLE_DCMTK == 1
+              const ParseDicomFromWadoCommand& command =
+                context->GetTypedCommand<ParseDicomFromWadoCommand>();
+              
+              size_t fileSize;
+              std::unique_ptr<Orthanc::ParsedDicomFile> dicom
+                (ParseDicomSuccessMessage::ParseWadoAnswer(fileSize, answer, headers));
+
               {
-                HttpCommand::SuccessMessage message(context->GetTypedCommand<HttpCommand>(), 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<OrthancRestApiCommand>(), 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<GetOrthancImageCommand>().ProcessHttpAnswer
-                  (context->GetEmitter(), context->GetReceiver(), answer, headers);
-                break;
-              }
-
-              case IOracleCommand::Type_GetOrthancWebViewerJpeg:
-              {
-                context->GetTypedCommand<GetOrthancWebViewerJpegCommand>().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<IObserver>     receiver_;
     std::unique_ptr<IOracleCommand>  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<IObserver> 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<const char*> 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<IObserver> 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<IObserver> 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<IObserver> 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<IObserver> 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<IObserver> receiver,
+                                  ParseDicomFromWadoCommand* command)
+  {
+    std::unique_ptr<ParseDicomFromWadoCommand> 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<const HttpCommand&>(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<const OrthancRestApiCommand&>(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<IObserver> receiver,
                                    IOracleCommand* command)
   {
     LOG(TRACE) << "WebAssemblyOracle::Schedule : receiver = "
-      << std::hex << &receiver << std::dec
-      << " | Current fingerprint is " << receiver.GetFingerprint();
+               << std::hex << &receiver;
     
     std::unique_ptr<IOracleCommand> 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<OrthancRestApiCommand*>(protection.get());
-        //  LOG(TRACE) << "WebAssemblyOracle::Schedule | typed command = " << rac;
-        //  LOG(TRACE) << "WebAssemblyOracle::Schedule" << rac->GetUri();
-        //}
-        //// END OF BLOCK TO REMOVE
         Execute(receiver, dynamic_cast<OrthancRestApiCommand*>(protection.release()));
         break;
         
       case IOracleCommand::Type_GetOrthancImage:
-        //// DIAGNOSTIC. PLEASE REMOVE IF IT HAS BEEN COMMITTED BY MISTAKE
-        //{
-        //  GetOrthancImageCommand* rac = dynamic_cast<GetOrthancImageCommand*>(protection.get());
-        //  LOG(TRACE) << "WebAssemblyOracle::Schedule" << rac->GetUri();
-        //}
-        //// END OF BLOCK TO REMOVE
         Execute(receiver, dynamic_cast<GetOrthancImageCommand*>(protection.release()));
         break;
 
       case IOracleCommand::Type_GetOrthancWebViewerJpeg:
-        //// DIAGNOSTIC. PLEASE REMOVE IF IT HAS BEEN COMMITTED BY MISTAKE
-        //{
-        //  GetOrthancWebViewerJpegCommand* rac = dynamic_cast<GetOrthancWebViewerJpegCommand*>(protection.get());
-        //  LOG(TRACE) << "WebAssemblyOracle::Schedule" << rac->GetUri();
-        //}
-        //// END OF BLOCK TO REMOVE
-        Execute(receiver, dynamic_cast<GetOrthancWebViewerJpegCommand*>(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<ParseDicomFromWadoCommand*>(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
   }
 }
--- a/Framework/Oracle/WebAssemblyOracle.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Oracle/WebAssemblyOracle.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <Core/WebServiceParameters.h>
 
 
 namespace OrthancStone
 {
   class WebAssemblyOracle :
     public IOracle,
-    public IObservable
+    public IMessageEmitter
   {
   private:
     typedef std::map<std::string, std::string>  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<IObserver> receiver,
                  HttpCommand* command);    
     
-    void Execute(const IObserver& receiver,
+    void Execute(boost::weak_ptr<IObserver> receiver,
                  OrthancRestApiCommand* command);    
     
-    void Execute(const IObserver& receiver,
+    void Execute(boost::weak_ptr<IObserver> receiver,
                  GetOrthancImageCommand* command);    
     
-    void Execute(const IObserver& receiver,
+    void Execute(boost::weak_ptr<IObserver> receiver,
                  GetOrthancWebViewerJpegCommand* command);
+    
+    void Execute(boost::weak_ptr<IObserver> receiver,
+                 ParseDicomFromWadoCommand* command);
 
-    std::string orthancRoot_;
+    IObservable                    oracleObservable_;
+    bool                           isLocalOrthanc_;
+    std::string                    localOrthancRoot_;
+    Orthanc::WebServiceParameters  remoteOrthanc_;
+
+#if ORTHANC_ENABLE_DCMTK == 1
+    std::unique_ptr<ParsedDicomCache>  dicomCache_;
+#endif
 
   public:
-    WebAssemblyOracle(MessageBroker& broker) :
-      IObservable(broker)
+    WebAssemblyOracle() :
+      isLocalOrthanc_(false)
     {
     }
-
-    void SetOrthancRoot(const std::string& root)
+    
+    virtual void EmitMessage(boost::weak_ptr<IObserver> 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<IObserver> 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);
   };
 }
--- a/Framework/Radiography/RadiographyAlphaLayer.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Radiography/RadiographyAlphaLayer.h	Tue Mar 31 15:51:02 2020 +0200
@@ -39,8 +39,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)
     {
     }
--- a/Framework/Radiography/RadiographyDicomLayer.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Radiography/RadiographyDicomLayer.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -48,7 +48,8 @@
   }
 
 
-  RadiographyDicomLayer::RadiographyDicomLayer(MessageBroker& broker, const RadiographyScene& scene) : RadiographyLayer(broker, scene)
+  RadiographyDicomLayer::RadiographyDicomLayer(const RadiographyScene& scene) :
+    RadiographyLayer(scene)
   {
 
   }
--- a/Framework/Radiography/RadiographyDicomLayer.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Radiography/RadiographyDicomLayer.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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)
     {
--- a/Framework/Radiography/RadiographyLayer.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Radiography/RadiographyLayer.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -119,8 +119,7 @@
   }
 
 
-  RadiographyLayer::RadiographyLayer(MessageBroker& broker, const RadiographyScene& scene) :
-    IObservable(broker),
+  RadiographyLayer::RadiographyLayer(const RadiographyScene& scene) :
     index_(0),
     hasSize_(false),
     width_(0),
--- a/Framework/Radiography/RadiographyLayer.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Radiography/RadiographyLayer.h	Tue Mar 31 15:51:02 2020 +0200
@@ -240,7 +240,7 @@
                      double zoom);
 
   public:
-    RadiographyLayer(MessageBroker& broker, const RadiographyScene& scene);
+    RadiographyLayer(const RadiographyScene& scene);
 
     virtual ~RadiographyLayer()
     {
--- a/Framework/Radiography/RadiographyMaskLayer.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Radiography/RadiographyMaskLayer.h	Tue Mar 31 15:51:02 2020 +0200
@@ -42,9 +42,9 @@
 
     mutable std::unique_ptr<Orthanc::ImageAccessor>  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)
--- a/Framework/Radiography/RadiographyScene.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Radiography/RadiographyScene.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -147,7 +147,7 @@
 
     BroadcastMessage(GeometryChangedMessage(*this, *layer));
     BroadcastMessage(ContentChangedMessage(*this, *layer));
-    layer->RegisterObserverCallback(new Callable<RadiographyScene, RadiographyLayer::LayerEditedMessage>(*this, &RadiographyScene::OnLayerEdited));
+    Register<RadiographyLayer::LayerEditedMessage>(*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::unique_ptr<RadiographyTextLayer>  alpha(new RadiographyTextLayer(IObservable::GetBroker(), *this));
+    std::unique_ptr<RadiographyTextLayer>  alpha(new RadiographyTextLayer(*this));
     alpha->SetText(utf8, font, fontSize, foreground);
     if (centerGeometry != NULL)
     {
@@ -382,7 +381,7 @@
                                                float foreground,
                                                RadiographyLayer::Geometry* geometry)
   {
-    std::unique_ptr<RadiographyMaskLayer>  mask(new RadiographyMaskLayer(IObservable::GetBroker(), *this, dicomLayer, foreground));
+    std::unique_ptr<RadiographyMaskLayer>  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::unique_ptr<RadiographyAlphaLayer>  alpha(new RadiographyAlphaLayer(IObservable::GetBroker(), *this));
+    std::unique_ptr<RadiographyAlphaLayer>  alpha(new RadiographyAlphaLayer(*this));
     alpha->SetAlpha(bitmap);
     if (geometry != NULL)
     {
@@ -412,7 +411,7 @@
                                                      RadiographyPhotometricDisplayMode preferredPhotometricDisplayMode,
                                                      RadiographyLayer::Geometry* geometry)
   {
-    RadiographyDicomLayer& layer = dynamic_cast<RadiographyDicomLayer&>(RegisterLayer(new RadiographyDicomLayer(IObservable::GetBroker(), *this)));
+    RadiographyDicomLayer& layer = dynamic_cast<RadiographyDicomLayer&>(RegisterLayer(new RadiographyDicomLayer(*this)));
 
     layer.SetInstance(instance, frame);
 
@@ -434,7 +433,7 @@
                                                      bool httpCompression,
                                                      RadiographyLayer::Geometry* geometry)
   {
-    RadiographyDicomLayer& layer = dynamic_cast<RadiographyDicomLayer&>(RegisterLayer(new RadiographyDicomLayer(IObservable::GetBroker(), *this)));
+    RadiographyDicomLayer& layer = dynamic_cast<RadiographyDicomLayer&>(RegisterLayer(new RadiographyDicomLayer( *this)));
     layer.SetInstance(instance, frame);
 
     if (geometry != NULL)
@@ -448,8 +447,8 @@
 
       orthanc.GetBinaryAsync(
             uri, headers,
-            new Callable<RadiographyScene, Deprecated::OrthancApiClient::BinaryResponseReadyMessage>
-            (*this, &RadiographyScene::OnTagsReceived), NULL,
+            new Deprecated::DeprecatedCallable<RadiographyScene, Deprecated::OrthancApiClient::BinaryResponseReadyMessage>
+            (GetSharedObserver(), &RadiographyScene::OnTagsReceived), NULL,
             new Orthanc::SingleValueObject<size_t>(layer.GetIndex()));
     }
 
@@ -467,8 +466,8 @@
 
       orthanc.GetBinaryAsync(
             uri, headers,
-            new Callable<RadiographyScene, Deprecated::OrthancApiClient::BinaryResponseReadyMessage>
-            (*this, &RadiographyScene::OnFrameReceived), NULL,
+            new Deprecated::DeprecatedCallable<RadiographyScene, Deprecated::OrthancApiClient::BinaryResponseReadyMessage>
+            (GetSharedObserver(), &RadiographyScene::OnFrameReceived), NULL,
             new Orthanc::SingleValueObject<size_t>(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;
@@ -854,8 +853,8 @@
 
     orthanc.PostJsonAsyncExpectJson(
           "/tools/create-dicom", createDicomRequestContent,
-          new Callable<RadiographyScene, Deprecated::OrthancApiClient::JsonResponseReadyMessage>
-          (*this, &RadiographyScene::OnDicomExported),
+          new Deprecated::DeprecatedCallable<RadiographyScene, Deprecated::OrthancApiClient::JsonResponseReadyMessage>
+          (GetSharedObserver(), &RadiographyScene::OnDicomExported),
           NULL, NULL);
 
   }
--- a/Framework/Radiography/RadiographyScene.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Radiography/RadiographyScene.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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<RadiographyScene>,
+    public IObservable
   {
     friend class RadiographySceneGeometryReader;
   public:
@@ -191,7 +192,7 @@
     virtual void OnLayerEdited(const RadiographyLayer::LayerEditedMessage& message);
 
   public:
-    RadiographyScene(MessageBroker& broker);
+    RadiographyScene();
     
     virtual ~RadiographyScene();
 
--- a/Framework/Radiography/RadiographySceneReader.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Radiography/RadiographySceneReader.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -55,7 +55,7 @@
 
   RadiographyDicomLayer* RadiographySceneGeometryReader::LoadDicom(const std::string& instanceId, unsigned int frame, RadiographyLayer::Geometry* geometry)
   {
-    std::unique_ptr<RadiographyPlaceholderLayer>  layer(new RadiographyPlaceholderLayer(dynamic_cast<IObservable&>(scene_).GetBroker(), scene_));
+    std::unique_ptr<RadiographyPlaceholderLayer>  layer(new RadiographyPlaceholderLayer(scene_));
     layer->SetGeometry(*geometry);
     layer->SetSize(dicomImageWidth_, dicomImageHeight_);
     scene_.RegisterLayer(layer.get());
--- a/Framework/Radiography/RadiographySceneReader.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Radiography/RadiographySceneReader.h	Tue Mar 31 15:51:02 2020 +0200
@@ -37,8 +37,8 @@
   class RadiographyPlaceholderLayer : public RadiographyDicomLayer
   {
   public:
-    RadiographyPlaceholderLayer(MessageBroker& broker, const RadiographyScene& scene) :
-      RadiographyDicomLayer(broker, scene)
+    RadiographyPlaceholderLayer(const RadiographyScene& scene) :
+      RadiographyDicomLayer(scene)
     {
     }
 
--- a/Framework/Radiography/RadiographyTextLayer.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Radiography/RadiographyTextLayer.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -22,7 +22,7 @@
 
 #include "Core/OrthancException.h"
 #include "RadiographyScene.h"
-#include "Framework/Toolbox/TextRenderer.h"
+#include "../Toolbox/TextRenderer.h"
 
 namespace OrthancStone
 {
--- a/Framework/Radiography/RadiographyTextLayer.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Radiography/RadiographyTextLayer.h	Tue Mar 31 15:51:02 2020 +0200
@@ -37,8 +37,8 @@
 
     static std::map<std::string, Orthanc::EmbeddedResources::FileResourceId>  fonts_;
   public:
-    RadiographyTextLayer(MessageBroker& broker, const RadiographyScene& scene) :
-      RadiographyAlphaLayer(broker, scene)
+    RadiographyTextLayer(const RadiographyScene& scene) :
+      RadiographyAlphaLayer(scene)
     {
     }
 
--- a/Framework/Radiography/RadiographyWidget.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Radiography/RadiographyWidget.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -174,12 +174,9 @@
   }
 
 
-  RadiographyWidget::RadiographyWidget(MessageBroker& broker,
-                                       boost::shared_ptr<RadiographyScene> scene,
+  RadiographyWidget::RadiographyWidget(boost::shared_ptr<RadiographyScene> 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<RadiographyScene> scene)
   {
-    if (scene_ != NULL)
-    {
-      scene_->Unregister(this);
-    }
-
     scene_ = scene;
 
-    scene_->RegisterObserverCallback(
-          new Callable<RadiographyWidget, RadiographyScene::GeometryChangedMessage>
-          (*this, &RadiographyWidget::OnGeometryChanged));
-
-    scene_->RegisterObserverCallback(
-          new Callable<RadiographyWidget, RadiographyScene::ContentChangedMessage>
-          (*this, &RadiographyWidget::OnContentChanged));
-
-    scene_->RegisterObserverCallback(
-          new Callable<RadiographyWidget, RadiographyScene::LayerRemovedMessage>
-          (*this, &RadiographyWidget::OnLayerRemoved));
+    Register<RadiographyScene::GeometryChangedMessage>(*scene_, &RadiographyWidget::OnGeometryChanged);
+    Register<RadiographyScene::ContentChangedMessage>(*scene_, &RadiographyWidget::OnContentChanged);
+    Register<RadiographyScene::LayerRemovedMessage>(*scene_, &RadiographyWidget::OnLayerRemoved);
 
     Unselect();
 
--- a/Framework/Radiography/RadiographyWidget.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Radiography/RadiographyWidget.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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<RadiographyWidget>,
     public IObservable
   {
   public:
@@ -64,8 +65,7 @@
     bool IsInvertedInternal() const;
 
   public:
-    RadiographyWidget(MessageBroker& broker,
-                      boost::shared_ptr<RadiographyScene> 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<RadiographyScene> 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
--- a/Framework/Scene2D/CairoCompositor.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/CairoCompositor.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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();
   }
 
--- a/Framework/Scene2D/CairoCompositor.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/CairoCompositor.h	Tue Mar 31 15:51:02 2020 +0200
@@ -37,7 +37,7 @@
   private:
     typedef std::map<size_t, GlyphBitmapAlphabet*>   Fonts;
 
-    Internals::CompositorHelper  helper_;
+    std::unique_ptr<Internals::CompositorHelper>  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);
--- a/Framework/Scene2D/FloatTextureSceneLayer.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/FloatTextureSceneLayer.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -30,7 +30,8 @@
 namespace OrthancStone
 {
   FloatTextureSceneLayer::FloatTextureSceneLayer(const Orthanc::ImageAccessor& texture) :
-    inverted_(false)
+    inverted_(false),
+    applyLog_(false)
   {
     {
       std::unique_ptr<Orthanc::ImageAccessor> t(
@@ -95,6 +96,14 @@
     IncrementRevision();
   }
 
+
+  void FloatTextureSceneLayer::SetApplyLog(bool apply)
+  {
+    applyLog_ = apply;
+    IncrementRevision();
+  }
+
+
   void FloatTextureSceneLayer::FitRange()
   {
     float minValue, maxValue;
@@ -126,6 +135,7 @@
     cloned->customCenter_ = customCenter_;
     cloned->customWidth_ = customWidth_;
     cloned->inverted_ = inverted_;
+    cloned->applyLog_ = applyLog_;
 
     return cloned.release();
   }
--- a/Framework/Scene2D/FloatTextureSceneLayer.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/FloatTextureSceneLayer.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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
--- a/Framework/Scene2D/GrayscaleStyleConfigurator.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/GrayscaleStyleConfigurator.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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_);
   }
 }
--- a/Framework/Scene2D/GrayscaleStyleConfigurator.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/GrayscaleStyleConfigurator.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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_;
--- a/Framework/Scene2D/ICompositor.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/ICompositor.h	Tue Mar 31 15:51:02 2020 +0200
@@ -1,8 +1,9 @@
 #pragma once
 
-#include <boost/noncopyable.hpp>
+#include "Scene2D.h"
+#include "ScenePoint2D.h"
+
 #include <EmbeddedResources.h>
-#include <Core/Enumerations.h>
 
 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<double>(x) + 0.5 - static_cast<double>(GetCanvasWidth()) / 2.0,
+        static_cast<double>(y) + 0.5 - static_cast<double>(GetCanvasHeight()) / 2.0);
+    }
+
+    void FitContent(Scene2D& scene) const
+    {
+      scene.FitContent(GetCanvasWidth(), GetCanvasHeight());
+    }
   };
 }
--- a/Framework/Scene2D/ISceneLayer.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/ISceneLayer.h	Tue Mar 31 15:51:02 2020 +0200
@@ -33,6 +33,7 @@
   public:
     enum Type
     {
+      Type_NullLayer,
       Type_InfoPanel,
       Type_ColorTexture,
       Type_Polyline,
--- a/Framework/Scene2D/InfoPanelSceneLayer.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/InfoPanelSceneLayer.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -28,10 +28,12 @@
 {
   InfoPanelSceneLayer::InfoPanelSceneLayer(const Orthanc::ImageAccessor& texture,
                                            BitmapAnchor anchor,
-                                           bool isLinearInterpolation) :
+                                           bool isLinearInterpolation,
+                                           bool applySceneRotation) :
     texture_(Orthanc::Image::Clone(texture)),
     anchor_(anchor),
-    isLinearInterpolation_(isLinearInterpolation)
+    isLinearInterpolation_(isLinearInterpolation),
+    applySceneRotation_(applySceneRotation)
   {
     if (texture_->GetFormat() != Orthanc::PixelFormat_RGBA32 &&
         texture_->GetFormat() != Orthanc::PixelFormat_RGB24)
--- a/Framework/Scene2D/InfoPanelSceneLayer.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/InfoPanelSceneLayer.h	Tue Mar 31 15:51:02 2020 +0200
@@ -37,15 +37,26 @@
     std::unique_ptr<Orthanc::ImageAccessor>  texture_;
     BitmapAnchor                           anchor_;
     bool                                   isLinearInterpolation_;
+    bool                                   applySceneRotation_;
 
   public:
+  /**
+   * If you supply `true` for `applySceneRotation`, then, in addition to 
+   * a translation to bring it at the desired anchoring location, the image
+   * will be rotated around its center by the same rotation as the scene
+   * transformation.
+   */
     InfoPanelSceneLayer(const Orthanc::ImageAccessor& texture,
                         BitmapAnchor anchor,
-                        bool isLinearInterpolation);
+                        bool isLinearInterpolation,
+                        bool applySceneRotation = false);
 
     virtual ISceneLayer* Clone() const
     {
-      return new InfoPanelSceneLayer(*texture_, anchor_, isLinearInterpolation_);
+      return new InfoPanelSceneLayer(*texture_, 
+                                     anchor_, 
+                                     isLinearInterpolation_, 
+                                     applySceneRotation_);
     }
 
     const Orthanc::ImageAccessor& GetTexture() const
@@ -58,6 +69,11 @@
       return anchor_;
     }
 
+    bool ShouldApplySceneRotation() const
+    {
+      return applySceneRotation_;
+    }
+
     bool IsLinearInterpolation() const
     {
       return isLinearInterpolation_;
--- a/Framework/Scene2D/Internals/CairoFloatTextureRenderer.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/Internals/CairoFloatTextureRenderer.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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<const float*>(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<float>(v));
+          }
+
+          assert(v >= 0.0f && v <= 255.0f);
+
           uint8_t vv = static_cast<uint8_t>(v);
 
           if (l.IsInverted())
--- a/Framework/Scene2D/Internals/CairoInfoPanelRenderer.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/Internals/CairoInfoPanelRenderer.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -34,9 +34,9 @@
       texture_.Copy(l.GetTexture(), true);
       anchor_ = l.GetAnchor();
       isLinearInterpolation_ = l.IsLinearInterpolation();
+      applySceneRotation_ = l.ShouldApplySceneRotation();
     }
 
-    
     void CairoInfoPanelRenderer::Render(const AffineTransform2D& transform,
                                         unsigned int canvasWidth,
                                         unsigned int canvasHeight)
@@ -47,14 +47,71 @@
         canvasWidth, canvasHeight);
 
       cairo_t* cr = target_.GetCairoContext();
-
       cairo_save(cr);
 
-      cairo_matrix_t t;
-      cairo_matrix_init_identity(&t);
-      cairo_matrix_translate(&t, dx, dy);
-      cairo_transform(cr, &t);
+      if (applySceneRotation_)
+      {
+        // the transformation is as follows:
+        // - originally, the image is aligned so that its top left corner
+        // is at 0,0
+        // - first, we translate the image by -w/2,-h/2 
+        // - then we rotate it, so that the next rotation will make the  
+        //   image rotate around its center.
+        // - then, we translate the image by +w/2,+h/2 to put it
+        //   back in place
+        // - the fourth and last transform is the one that brings the 
+        //   image to its desired anchored location.
+
+        int32_t halfWidth =
+          static_cast<int32_t>(0.5 * texture_.GetWidth());
+
+        int32_t halfHeight =
+          static_cast<int32_t>(0.5 * texture_.GetHeight());
+
+        AffineTransform2D translation1 =
+          AffineTransform2D::CreateOffset(-halfWidth, -halfHeight);
+
+        const Matrix& sceneTransformM = transform.GetHomogeneousMatrix();
+        Matrix r;
+        Matrix q;
+        LinearAlgebra::RQDecomposition3x3(r, q, sceneTransformM);
+
+        // first, put the scene rotation in a cairo matrix
+        cairo_matrix_t m;
+        cairo_matrix_init(
+          &m, q(0, 0), q(1, 0), q(0, 1), q(1, 1), q(0, 2), q(1, 2));
 
+        // now let's build the transform piece by piece
+        // first translation (directly written in `transform`)
+        cairo_matrix_t transform;
+        cairo_matrix_init_identity(&transform);
+        cairo_matrix_translate(&transform, -halfWidth, -halfHeight);
+
+        // then the rotation
+        cairo_matrix_multiply(&transform, &transform, &m);
+
+        // then the second translation
+        {
+          cairo_matrix_t translation2;
+          cairo_matrix_init_translate(&translation2, halfWidth, halfHeight);
+          cairo_matrix_multiply(&transform, &transform, &m);
+        }
+
+        // then the last translation
+        {
+          cairo_matrix_t translation3;
+          cairo_matrix_init_translate(&translation3, dx, dy);
+          cairo_matrix_multiply(&transform, &transform, &translation3);
+        }
+        cairo_transform(cr, &transform);
+      } 
+      else
+      {
+        cairo_matrix_t t;
+        cairo_matrix_init_identity(&t);
+        cairo_matrix_translate(&t, dx, dy);
+        cairo_transform(cr, &t);
+      }
       cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
       cairo_set_source_surface(cr, texture_.GetObject(), 0, 0);
 
--- a/Framework/Scene2D/Internals/CairoInfoPanelRenderer.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/Internals/CairoInfoPanelRenderer.h	Tue Mar 31 15:51:02 2020 +0200
@@ -36,11 +36,15 @@
       CairoSurface           texture_;
       BitmapAnchor           anchor_;
       bool                   isLinearInterpolation_;
+      bool                   applySceneRotation_;
 
     public:
       CairoInfoPanelRenderer(ICairoContextProvider& target,
                              const ISceneLayer& layer) :
-        target_(target)
+        target_(target),
+        anchor_(BitmapAnchor_TopLeft),
+        applySceneRotation_(false)
+
       {
         Update(layer);
       }
--- a/Framework/Scene2D/Internals/CairoLookupTableTextureRenderer.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/Internals/CairoLookupTableTextureRenderer.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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<uint8_t>& 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<const float*>(source.GetConstRow(y));
-        uint8_t* q = reinterpret_cast<uint8_t*>(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<uint8_t>(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());
     }
 
--- a/Framework/Scene2D/Internals/CompositorHelper.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/Internals/CompositorHelper.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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);
 
@@ -103,6 +104,8 @@
           content_.erase(found);
         }
 
+        // the returned renderer can be NULL in case of an unknown layer
+        // or a NullLayer
         std::unique_ptr<ILayerRenderer> renderer(factory_.Create(layer));
 
         if (renderer.get() != NULL)
@@ -115,7 +118,7 @@
       {
         // This layer has already been rendered
         assert(found->second->GetLastRevision() <= layer.GetRevision());
-        
+
         if (found->second->GetLastRevision() < layer.GetRevision())
         {
           found->second->UpdateRenderer();
@@ -141,18 +144,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<double>(canvasWidth) / 2.0,
         static_cast<double>(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);
     }
   }
 }
--- a/Framework/Scene2D/Internals/CompositorHelper.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/Internals/CompositorHelper.h	Tue Mar 31 15:51:02 2020 +0200
@@ -64,9 +64,9 @@
 
       typedef std::map<int, Item*>  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);
     };
   }
--- a/Framework/Scene2D/Internals/FixedPointAligner.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/Internals/FixedPointAligner.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -25,26 +25,28 @@
 {
   namespace Internals
   {
-    FixedPointAligner::FixedPointAligner(boost::weak_ptr<ViewportController> controllerW,
+    FixedPointAligner::FixedPointAligner(boost::shared_ptr<IViewport> viewport,
                                          const ScenePoint2D& p) 
-      : controllerW_(controllerW)
+      : viewport_(viewport)
       , canvas_(p)
     {
-      boost::shared_ptr<ViewportController> controller = controllerW_.lock();
-      pivot_ = canvas_.Apply(controller->GetCanvasToSceneTransform());
+      std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+      pivot_ = canvas_.Apply(lock->GetController().GetCanvasToSceneTransform());
     }
 
     
     void FixedPointAligner::Apply()
     {
-      boost::shared_ptr<ViewportController> controller = controllerW_.lock();
-      ScenePoint2D p = canvas_.Apply(controller->GetCanvasToSceneTransform());
+      std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+      ScenePoint2D p = canvas_.Apply(
+        lock->GetController().GetCanvasToSceneTransform());
 
-      controller->SetSceneToCanvasTransform(
+      lock->GetController().SetSceneToCanvasTransform(
         AffineTransform2D::Combine(
-          controller->GetSceneToCanvasTransform(),
+          lock->GetController().GetSceneToCanvasTransform(),
           AffineTransform2D::CreateOffset(p.GetX() - pivot_.GetX(),
                                           p.GetY() - pivot_.GetY())));
+      lock->Invalidate();
     }
   }
 }
--- a/Framework/Scene2D/Internals/FixedPointAligner.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/Internals/FixedPointAligner.h	Tue Mar 31 15:51:02 2020 +0200
@@ -22,6 +22,9 @@
 
 #include "../../Scene2DViewport/PredeclaredTypes.h"
 #include "../../Scene2D/ScenePoint2D.h"
+#include "../../Viewport/IViewport.h"
+
+#include <boost/weak_ptr.hpp>
 
 namespace OrthancStone
 {
@@ -32,12 +35,12 @@
     class FixedPointAligner : public boost::noncopyable
     {
     private:
-      boost::weak_ptr<ViewportController> controllerW_;
+      boost::shared_ptr<IViewport> viewport_;
       ScenePoint2D           pivot_;
       ScenePoint2D           canvas_;
 
     public:
-      FixedPointAligner(boost::weak_ptr<ViewportController> controllerW,
+      FixedPointAligner(boost::shared_ptr<IViewport> viewport,
                         const ScenePoint2D& p);
 
       void Apply();
--- a/Framework/Scene2D/Internals/OpenGLFloatTextureRenderer.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/Internals/OpenGLFloatTextureRenderer.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -21,6 +21,8 @@
 
 #include "OpenGLFloatTextureRenderer.h"
 
+#include <Core/OrthancException.h>
+
 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()));
--- a/Framework/Scene2D/Internals/OpenGLInfoPanelRenderer.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/Internals/OpenGLInfoPanelRenderer.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -32,6 +32,7 @@
         context_.MakeCurrent();
         texture_.reset(new OpenGL::OpenGLTexture(context_));
         texture_->Load(layer.GetTexture(), layer.IsLinearInterpolation());
+        applySceneRotation_ = layer.ShouldApplySceneRotation();
         anchor_ = layer.GetAnchor();
       }
     }
@@ -41,11 +42,11 @@
                                                      const InfoPanelSceneLayer& layer) :
       context_(context),
       program_(program),
-      anchor_(BitmapAnchor_TopLeft)
+      anchor_(BitmapAnchor_TopLeft),
+      applySceneRotation_(false)
     {
       LoadTexture(layer);
     }
-
     
     void OpenGLInfoPanelRenderer::Render(const AffineTransform2D& transform,
                                          unsigned int canvasWidth,
@@ -60,7 +61,56 @@
 
         // The position of this type of layer is layer: Ignore the
         // "transform" coming from the scene
-        program_.Apply(*texture_, AffineTransform2D::CreateOffset(dx, dy), true);
+        AffineTransform2D actualTransform = 
+          AffineTransform2D::CreateOffset(dx, dy);
+
+        if (applySceneRotation_)
+        {
+          // the transformation is as follows:
+          // - originally, the image is aligned so that its top left corner
+          // is at 0,0
+          // - first, we translate the image by -w/2,-h/2 
+          // - then we rotate it, so that the next rotation will make the  
+          //   image rotate around its center.
+          // - then, we translate the image by +w/2,+h/2 to put it
+          //   back in place
+          // - the fourth and last transform is the one that brings the 
+          //   image to its desired anchored location.
+
+          int32_t halfWidth = 
+            static_cast<int32_t>(0.5 * texture_->GetWidth());
+
+          int32_t halfHeight=
+            static_cast<int32_t>(0.5 * texture_->GetHeight());
+
+          AffineTransform2D translation1 =
+            AffineTransform2D::CreateOffset(-halfWidth, -halfHeight);
+
+          const Matrix& sceneTransformM = transform.GetHomogeneousMatrix();
+          Matrix r;
+          Matrix q;
+          LinearAlgebra::RQDecomposition3x3(r, q, sceneTransformM);
+
+          // counterintuitively, q is the rotation and r is the upper
+          // triangular
+          AffineTransform2D rotation(q);
+
+          AffineTransform2D translation2 =
+            AffineTransform2D::CreateOffset(halfWidth, halfHeight);
+
+          // please note that the last argument is the 1st applied 
+          // transformation (rationale: if arguments are a, b and c, then
+          // the resulting matrix is a*b*c:
+          // x2 = (a*b*c)*x1 = (a*(b*(c*x1))) (you can see that the result
+          // of c*x1 is transformed by b, and the result of b*c*x1 is trans-
+          // formed by a)
+          actualTransform = AffineTransform2D::Combine(actualTransform,
+                                                       translation2,
+                                                       rotation,
+                                                       translation1);
+        }
+
+        program_.Apply(*texture_, actualTransform, true);
       }
     }
   }
--- a/Framework/Scene2D/Internals/OpenGLInfoPanelRenderer.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/Internals/OpenGLInfoPanelRenderer.h	Tue Mar 31 15:51:02 2020 +0200
@@ -36,6 +36,7 @@
       OpenGLColorTextureProgram&            program_;
       std::unique_ptr<OpenGL::OpenGLTexture>  texture_;
       BitmapAnchor                          anchor_;
+      bool                                  applySceneRotation_;
 
       void LoadTexture(const InfoPanelSceneLayer& layer);
 
--- a/Framework/Scene2D/Internals/OpenGLLookupTableTextureRenderer.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/Internals/OpenGLLookupTableTextureRenderer.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -39,76 +39,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<uint8_t>& 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<const float*>(source.GetConstRow(y));
-            uint8_t* q = reinterpret_cast<uint8_t*>(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<uint8_t>(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_));
--- a/Framework/Scene2D/LookupTableStyleConfigurator.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/LookupTableStyleConfigurator.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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_);
   }
 }
--- a/Framework/Scene2D/LookupTableStyleConfigurator.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/LookupTableStyleConfigurator.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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);
     }
--- a/Framework/Scene2D/LookupTableTextureSceneLayer.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/LookupTableTextureSceneLayer.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -27,7 +27,8 @@
 
 namespace OrthancStone
 {
-  LookupTableTextureSceneLayer::LookupTableTextureSceneLayer(const Orthanc::ImageAccessor& texture)
+  LookupTableTextureSceneLayer::LookupTableTextureSceneLayer(const Orthanc::ImageAccessor& texture) :
+    applyLog_(false)
   {
     {
       std::unique_ptr<Orthanc::ImageAccessor> 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 <bool IsApplyLog,
+            Orthanc::PixelFormat TargetFormat>
+  static void RenderInternal(Orthanc::ImageAccessor& target,
+                             const Orthanc::ImageAccessor& source,
+                             float minValue,
+                             float slope,
+                             const std::vector<uint8_t>& 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<const float*>(source.GetConstRow(y));
+      uint8_t* q = reinterpret_cast<uint8_t*>(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<float>(v));
+        }
+
+        assert(v >= 0.0f && v <= 255.0f);
+
+        uint8_t vv = static_cast<uint8_t>(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<uint8_t>& lut = GetLookupTable();
+    if (lut.size() != 4 * 256)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    switch (target.GetFormat())
+    {
+      case Orthanc::PixelFormat_RGBA32:
+        if (applyLog_)
+        {
+          RenderInternal<true, Orthanc::PixelFormat_RGBA32>(target, source, minValue, slope, lut);
+        }
+        else
+        {
+          RenderInternal<false, Orthanc::PixelFormat_RGBA32>(target, source, minValue, slope, lut);
+        }
+        break;
+
+      case Orthanc::PixelFormat_BGRA32:
+        if (applyLog_)
+        {
+          RenderInternal<true, Orthanc::PixelFormat_BGRA32>(target, source, minValue, slope, lut);
+        }
+        else
+        {
+          RenderInternal<false, Orthanc::PixelFormat_BGRA32>(target, source, minValue, slope, lut);
+        }
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+  }
 }
--- a/Framework/Scene2D/LookupTableTextureSceneLayer.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/LookupTableTextureSceneLayer.h	Tue Mar 31 15:51:02 2020 +0200
@@ -32,6 +32,7 @@
     float                 minValue_;
     float                 maxValue_;
     std::vector<uint8_t>  lut_;
+    bool                  applyLog_;
 
     void SetLookupTableRgb(const std::vector<uint8_t>& 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;
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2D/NullLayer.h	Tue Mar 31 15:51:02 2020 +0200
@@ -0,0 +1,62 @@
+/**
+ * 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 <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+#include "ISceneLayer.h"
+
+#include <Core/Enumerations.h>
+
+#include <stdint.h>
+
+/**
+ This layer can be used when a z-index needs to be booked inside a Scene2D.
+
+ It can later be replaced by the actual layer.
+*/
+namespace OrthancStone
+{
+  class NullLayer : public ISceneLayer
+  {
+  public:
+    NullLayer() {}
+
+    virtual ISceneLayer* Clone() const ORTHANC_OVERRIDE
+    {
+      return new NullLayer();
+    }
+
+    virtual Type GetType() const ORTHANC_OVERRIDE
+    {
+      return Type_NullLayer;
+    }
+
+    virtual bool GetBoundingBox(Extent2D& target) const ORTHANC_OVERRIDE
+    {
+      target = Extent2D();
+      return false; 
+    }
+
+    virtual uint64_t GetRevision() const ORTHANC_OVERRIDE
+    {
+      return 0;
+    }
+  };
+}
--- a/Framework/Scene2D/OpenGLCompositor.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/OpenGLCompositor.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
  **/
@@ -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,22 +137,52 @@
     canvasWidth_(0),
     canvasHeight_(0)
   {
+    if (!context_.IsContextLost())
+    {
+      canvasWidth_ = context_.GetCanvasWidth();
+      canvasHeight_ = context_.GetCanvasHeight();
+    }
+
+    ResetScene();
   }
 
   OpenGLCompositor::~OpenGLCompositor()
   {
     if (!context_.IsContextLost())
     {
-      context_.MakeCurrent(); // this can throw if context lost!
-      for (Fonts::iterator it = fonts_.begin(); it != fonts_.end(); ++it)
+      try
       {
-        assert(it->second != NULL);
-        delete it->second;
+        try
+        {
+          context_.MakeCurrent(); // this can throw if context lost!
+        }
+        catch (...)
+        {
+          LOG(ERROR) << "context_.MakeCurrent() failed in OpenGLCompositor::~OpenGLCompositor()!";
+        }
+
+        for (Fonts::iterator it = fonts_.begin(); it != fonts_.end(); ++it)
+        {
+          try
+          {
+
+            assert(it->second != NULL);
+            delete it->second;
+          }
+          catch (...)
+          {
+            LOG(ERROR) << "Exception thrown while deleting OpenGL-based font!";
+          }
+        }
+      }
+      catch (...)
+      {
+        // logging threw an exception!
       }
     }
   }
 
-  void OpenGLCompositor::Refresh()
+  void OpenGLCompositor::Refresh(const Scene2D& scene)
   {
     if (!context_.IsContextLost())
     {
@@ -167,11 +195,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,
--- a/Framework/Scene2D/OpenGLCompositor.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/OpenGLCompositor.h	Tue Mar 31 15:51:02 2020 +0200
@@ -37,27 +37,31 @@
 
     typedef std::map<size_t, Font*>  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::unique_ptr<Internals::CompositorHelper>  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);
 
--- a/Framework/Scene2D/PanSceneTracker.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/PanSceneTracker.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -20,16 +20,23 @@
 
 
 #include "PanSceneTracker.h"
+#include "../Viewport/IViewport.h"
 #include "../Scene2DViewport/ViewportController.h"
 
+#include <memory>
+
 namespace OrthancStone
 {
-  PanSceneTracker::PanSceneTracker(boost::weak_ptr<ViewportController> controllerW,
+  PanSceneTracker::PanSceneTracker(boost::shared_ptr<IViewport> viewport,
                                    const PointerEvent& event)
-    : OneGesturePointerTracker(controllerW)
-    , originalSceneToCanvas_(GetController()->GetSceneToCanvasTransform())
-    , originalCanvasToScene_(GetController()->GetCanvasToSceneTransform())
+    : OneGesturePointerTracker(viewport)
   {
+    
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+
+    originalSceneToCanvas_ = lock->GetController().GetSceneToCanvasTransform();
+    originalCanvasToScene_ = lock->GetController().GetCanvasToSceneTransform();
+
     pivot_ = event.GetMainPosition().Apply(originalCanvasToScene_);
   }
 
@@ -38,25 +45,19 @@
   {
     ScenePoint2D p = event.GetMainPosition().Apply(originalCanvasToScene_);
 
-      // The controller is a weak pointer. It could be deleted when the
-      // tracker is still alive (for instance, because of a lost WebGL
-      // context)
-      if(GetController().get() != NULL)
-      {
-      GetController()->SetSceneToCanvasTransform(
-        AffineTransform2D::Combine(
-          originalSceneToCanvas_,
-          AffineTransform2D::CreateOffset(p.GetX() - pivot_.GetX(),
-                                          p.GetY() - pivot_.GetY())));
-      }
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+
+    lock->GetController().SetSceneToCanvasTransform(
+      AffineTransform2D::Combine(
+        originalSceneToCanvas_,
+        AffineTransform2D::CreateOffset(p.GetX() - pivot_.GetX(),
+                                        p.GetY() - pivot_.GetY())));
+    lock->Invalidate();
   }
 
   void PanSceneTracker::Cancel()
   {
-      if(GetController().get() != NULL)
-      {
-        GetController()->SetSceneToCanvasTransform(originalSceneToCanvas_);
-      }
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    lock->GetController().SetSceneToCanvasTransform(originalSceneToCanvas_);
   }
-
 }
--- a/Framework/Scene2D/PanSceneTracker.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/PanSceneTracker.h	Tue Mar 31 15:51:02 2020 +0200
@@ -28,14 +28,13 @@
   class PanSceneTracker : public OneGesturePointerTracker
   {
   public:
-    PanSceneTracker(boost::weak_ptr<ViewportController> controllerW,
+    PanSceneTracker(boost::shared_ptr<IViewport> viewport,
                     const PointerEvent& event);
 
     virtual void PointerMove(const PointerEvent& event) ORTHANC_OVERRIDE;
     virtual void Cancel() ORTHANC_OVERRIDE;
 
   private:
-    boost::weak_ptr<ViewportController> controllerW_;
     ScenePoint2D           pivot_;
     AffineTransform2D      originalSceneToCanvas_;
     AffineTransform2D      originalCanvasToScene_;
--- a/Framework/Scene2D/PointerEvent.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/PointerEvent.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -26,6 +26,7 @@
 namespace OrthancStone
 {
   PointerEvent::PointerEvent() :
+    button_(MouseButton_None),
     hasAltModifier_(false),
     hasControlModifier_(false),
     hasShiftModifier_(false)
--- a/Framework/Scene2D/PointerEvent.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/PointerEvent.h	Tue Mar 31 15:51:02 2020 +0200
@@ -31,6 +31,7 @@
   class PointerEvent : public boost::noncopyable
   {
   private:
+    MouseButton                button_;
     std::vector<ScenePoint2D>  positions_;
     bool                       hasAltModifier_;
     bool                       hasControlModifier_;
@@ -88,5 +89,15 @@
     {
       return hasShiftModifier_;
     }
+
+    void SetMouseButton(MouseButton button)
+    {
+      button_ = button;
+    }
+
+    MouseButton GetMouseButton() const
+    {
+      return button_;
+    }
   };
 }
--- a/Framework/Scene2D/RotateSceneTracker.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/RotateSceneTracker.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
  **/
@@ -23,23 +23,25 @@
 
 namespace OrthancStone
 {
-  RotateSceneTracker::RotateSceneTracker(boost::weak_ptr<ViewportController> controllerW,
+  RotateSceneTracker::RotateSceneTracker(boost::shared_ptr<IViewport> viewport,
                                          const PointerEvent& event)
-    : OneGesturePointerTracker(controllerW)
+    : OneGesturePointerTracker(viewport)
     , click_(event.GetMainPosition())
-    , aligner_(controllerW, click_)
+    , aligner_(viewport, click_)
     , isFirst_(true)
-    , originalSceneToCanvas_(GetController()->GetSceneToCanvasTransform())
   {
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    originalSceneToCanvas_ = lock->GetController().GetSceneToCanvasTransform();
+
   }
-  
+
   void RotateSceneTracker::PointerMove(const PointerEvent& event)
   {
     ScenePoint2D p = event.GetMainPosition();
     double dx = p.GetX() - click_.GetX();
     double dy = p.GetY() - click_.GetY();
 
-    if (std::abs(dx) > 5.0 || 
+    if (std::abs(dx) > 5.0 ||
         std::abs(dy) > 5.0)
     {
       double a = atan2(dy, dx);
@@ -50,28 +52,22 @@
         isFirst_ = false;
       }
 
-      // The controller is a weak pointer. It could be deleted when the
-      // tracker is still alive (for instance, because of a lost WebGL
-      // context that triggers a recreation of the viewport)
-      if(GetController().get() != NULL)
-      {
-        GetController()->SetSceneToCanvasTransform(
-          AffineTransform2D::Combine(
-            AffineTransform2D::CreateRotation(a - referenceAngle_),
-            originalSceneToCanvas_));
-        
-        aligner_.Apply();
-      }
+      std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+
+      lock->GetController().SetSceneToCanvasTransform(
+        AffineTransform2D::Combine(
+          AffineTransform2D::CreateRotation(a - referenceAngle_),
+          originalSceneToCanvas_));
+      aligner_.Apply();
+      lock->Invalidate();
     }
   }
 
   void RotateSceneTracker::Cancel()
   {
-      // See remark above
-      if(GetController().get() != NULL)
-      {
-        GetController()->SetSceneToCanvasTransform(originalSceneToCanvas_);
-      }
+    // See remark above
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    lock->GetController().SetSceneToCanvasTransform(originalSceneToCanvas_);
+    lock->Invalidate();
   }
-
 }
--- a/Framework/Scene2D/RotateSceneTracker.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/RotateSceneTracker.h	Tue Mar 31 15:51:02 2020 +0200
@@ -30,7 +30,7 @@
   class RotateSceneTracker : public OneGesturePointerTracker
   {
   public:
-    RotateSceneTracker(boost::weak_ptr<ViewportController> controllerW,
+    RotateSceneTracker(boost::shared_ptr<IViewport> viewport,
                        const PointerEvent& event);
 
     virtual void PointerMove(const PointerEvent& event) ORTHANC_OVERRIDE;
--- a/Framework/Scene2D/Scene2D.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/Scene2D.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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);
     }
   }
 
--- a/Framework/Scene2D/Scene2D.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/Scene2D.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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;
     };
--- a/Framework/Scene2D/ScenePoint2D.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/ScenePoint2D.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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);
+      }
     }
   };
 }
--- a/Framework/Scene2D/ZoomSceneTracker.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/ZoomSceneTracker.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -22,19 +22,19 @@
 #include "ZoomSceneTracker.h"
 #include "../Scene2DViewport/ViewportController.h"
 
-using boost::weak_ptr;
-using boost::shared_ptr;
-
 namespace OrthancStone
 {
-  ZoomSceneTracker::ZoomSceneTracker(weak_ptr<ViewportController> controllerW,
+  ZoomSceneTracker::ZoomSceneTracker(boost::shared_ptr<IViewport> viewport,
                                      const PointerEvent& event,
                                      unsigned int canvasHeight)
-    : OneGesturePointerTracker(controllerW)
+    : OneGesturePointerTracker(viewport)
     , clickY_(event.GetMainPosition().GetY())
-    , aligner_(controllerW, event.GetMainPosition())
-    , originalSceneToCanvas_(GetController()->GetSceneToCanvasTransform())
+    , aligner_(viewport, event.GetMainPosition())
   {
+    
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    originalSceneToCanvas_ = lock->GetController().GetSceneToCanvasTransform();
+
     if (canvasHeight <= 3)
     {
       active_ = false;
@@ -76,26 +76,20 @@
 
       double zoom = pow(2.0, z);
 
-      // The controller is a weak pointer. It could be deleted when the
-      // tracker is still alive (for instance, because of a lost WebGL
-      // context)
-      if(GetController().get() != NULL)
-      {
-        GetController()->SetSceneToCanvasTransform(
-          AffineTransform2D::Combine(
-            AffineTransform2D::CreateScaling(zoom, zoom),
-            originalSceneToCanvas_));
-
-        aligner_.Apply();
-      }
+      std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+      lock->GetController().SetSceneToCanvasTransform(
+        AffineTransform2D::Combine(
+          AffineTransform2D::CreateScaling(zoom, zoom),
+          originalSceneToCanvas_));
+      aligner_.Apply();
+      lock->Invalidate();
     }
   }
 
   void ZoomSceneTracker::Cancel()
   {
-      if(GetController().get() != NULL)
-      {
-        GetController()->SetSceneToCanvasTransform(originalSceneToCanvas_);
-      }
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    lock->GetController().SetSceneToCanvasTransform(originalSceneToCanvas_);
+    lock->Invalidate();
   }
 }
--- a/Framework/Scene2D/ZoomSceneTracker.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2D/ZoomSceneTracker.h	Tue Mar 31 15:51:02 2020 +0200
@@ -23,14 +23,17 @@
 
 
 #include "../Scene2DViewport/OneGesturePointerTracker.h"
+#include "../Viewport/IViewport.h"
 #include "Internals/FixedPointAligner.h"
 
+#include <boost/weak_ptr.hpp>
+
 namespace OrthancStone
 {
   class ZoomSceneTracker : public OneGesturePointerTracker
   {
   public:
-    ZoomSceneTracker(boost::weak_ptr<ViewportController> controllerW,
+    ZoomSceneTracker(boost::shared_ptr<IViewport> viewport,
                      const PointerEvent& event,
                      unsigned int canvasHeight);
 
--- a/Framework/Scene2DViewport/AngleMeasureTool.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/AngleMeasureTool.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -42,16 +42,23 @@
   // the params in the LayerHolder ctor specify the number of polyline and text
   // layers
   AngleMeasureTool::AngleMeasureTool(
-    MessageBroker& broker, boost::weak_ptr<ViewportController> controllerW)
-    : MeasureTool(broker, controllerW)
+    boost::shared_ptr<IViewport> viewport)
+    : MeasureTool(viewport)
 #if ORTHANC_STONE_ENABLE_OUTLINED_TEXT == 1
-    , layerHolder_(boost::make_shared<LayerHolder>(controllerW,1,5))
+    , layerHolder_(boost::shared_ptr<LayerHolder>(new LayerHolder(viewport,1,5)))
 #else
-    , layerHolder_(boost::make_shared<LayerHolder>(controllerW, 1, 1))
+    , layerHolder_(boost::shared_ptr<LayerHolder>(new LayerHolder(viewport,1,1)))
 #endif
     , angleHighlightArea_(AngleHighlightArea_None)
   {
-    RefreshScene();
+  }
+
+  boost::shared_ptr<AngleMeasureTool> AngleMeasureTool::Create(boost::shared_ptr<IViewport> viewport)
+  {
+    boost::shared_ptr<AngleMeasureTool> obj(new AngleMeasureTool(viewport));
+    obj->MeasureTool::PostConstructor();
+    obj->RefreshScene();
+    return obj;
   }
 
   AngleMeasureTool::~AngleMeasureTool()
@@ -108,7 +115,9 @@
   
   void AngleMeasureTool::SetMemento(boost::shared_ptr<MeasureToolMemento> mementoBase)
   {
-    boost::shared_ptr<AngleMeasureToolMemento> memento = boost::dynamic_pointer_cast<AngleMeasureToolMemento>(mementoBase);
+    boost::shared_ptr<AngleMeasureToolMemento> memento = 
+      boost::dynamic_pointer_cast<AngleMeasureToolMemento>(mementoBase);
+
     ORTHANC_ASSERT(memento.get() != NULL, "Internal error: wrong (or bad) memento");
     center_   = memento->center_;
     side1End_ = memento->side1End_;
@@ -119,7 +128,8 @@
   std::string AngleMeasureTool::GetDescription()
   {
     std::stringstream ss;
-    ss << "AngleMeasureTool. Center = " << center_ << " Side1End = " << side1End_ << " Side2End = " << side2End_;
+    ss << "AngleMeasureTool. Center = " << center_ << " Side1End = " 
+       << side1End_ << " Side2End = " << side2End_;
     return ss.str();
   }
 
@@ -131,36 +141,51 @@
 
   AngleMeasureTool::AngleHighlightArea AngleMeasureTool::AngleHitTest(ScenePoint2D p) const
   {
-    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;
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    ViewportController& controller = lock->GetController();
+    Scene2D& scene = controller.GetScene();
+
+    const double pixelToScene = scene.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;
 
     {
-      const double sqDistanceFromSide1End = ScenePoint2D::SquaredDistancePtPt(p, side1End_);
+      const double sqDistanceFromSide1End = 
+        ScenePoint2D::SquaredDistancePtPt(p, side1End_);
+
       if (sqDistanceFromSide1End <= SQUARED_HIT_TEST_MAX_DISTANCE_SCENE_COORD)
         return AngleHighlightArea_Side1End;
     }
 
     {
-      const double sqDistanceFromSide2End = ScenePoint2D::SquaredDistancePtPt(p, side2End_);
+      const double sqDistanceFromSide2End = 
+        ScenePoint2D::SquaredDistancePtPt(p, side2End_);
+
       if (sqDistanceFromSide2End <= SQUARED_HIT_TEST_MAX_DISTANCE_SCENE_COORD)
         return AngleHighlightArea_Side2End;
     }
 
     {
-      const double sqDistanceFromCenter = ScenePoint2D::SquaredDistancePtPt(p, center_);
+      const double sqDistanceFromCenter = 
+        ScenePoint2D::SquaredDistancePtPt(p, center_);
       if (sqDistanceFromCenter <= SQUARED_HIT_TEST_MAX_DISTANCE_SCENE_COORD)
         return AngleHighlightArea_Center;
     }
 
     {
-      const double sqDistanceFromSide1 = ScenePoint2D::SquaredDistancePtSegment(center_, side1End_, p);
+      const double sqDistanceFromSide1 = 
+        ScenePoint2D::SquaredDistancePtSegment(center_, side1End_, p);
+
       if (sqDistanceFromSide1 <= SQUARED_HIT_TEST_MAX_DISTANCE_SCENE_COORD)
         return AngleHighlightArea_Side1;
     }
 
     {
-      const double sqDistanceFromSide2 = ScenePoint2D::SquaredDistancePtSegment(center_, side2End_, p);
+      const double sqDistanceFromSide2 = 
+        ScenePoint2D::SquaredDistancePtSegment(center_, side2End_, p);
+
       if (sqDistanceFromSide2 <= SQUARED_HIT_TEST_MAX_DISTANCE_SCENE_COORD)
         return AngleHighlightArea_Side2;
     }
@@ -168,7 +193,7 @@
     return AngleHighlightArea_None;
   }
 
-  bool AngleMeasureTool::HitTest(ScenePoint2D p) const
+  bool AngleMeasureTool::HitTest(ScenePoint2D p)
   {
     return AngleHitTest(p) != AngleHighlightArea_None;
   }
@@ -176,8 +201,12 @@
 
   boost::shared_ptr<IFlexiblePointerTracker> AngleMeasureTool::CreateEditionTracker(const PointerEvent& e)
   {
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    ViewportController& controller = lock->GetController();
+    Scene2D& scene = controller.GetScene();
+
     ScenePoint2D scenePos = e.GetMainPosition().Apply(
-      GetController()->GetScene().GetCanvasToSceneTransform());
+      scene.GetCanvasToSceneTransform());
 
     if (!HitTest(scenePos))
       return boost::shared_ptr<IFlexiblePointerTracker>();
@@ -186,11 +215,12 @@
       new EditLineMeasureTracker(
         boost::shared_ptr<LineMeasureTool> measureTool;
         MessageBroker & broker,
-        boost::weak_ptr<ViewportController>          controllerW,
+        boost::shared_ptr<IViewport>          viewport,
         const PointerEvent & e);
     */
+
     boost::shared_ptr<EditAngleMeasureTracker> editAngleMeasureTracker(
-      new EditAngleMeasureTracker(shared_from_this(), GetBroker(), GetController(), e));
+      new EditAngleMeasureTracker(shared_from_this(), viewport_, e));
     return editAngleMeasureTracker;
   }
 
@@ -204,7 +234,10 @@
   {
     if (IsSceneAlive())
     {
-      boost::shared_ptr<ViewportController> controller = GetController();
+      std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+      ViewportController& controller = lock->GetController();
+      Scene2D& scene = controller.GetScene();
+
       if (IsEnabled())
       {
         layerHolder_->CreateLayersIfNeeded();
@@ -212,72 +245,90 @@
         {
           // 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_, 
+                          controller.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_, 
+                          controller.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
@@ -293,8 +344,8 @@
           double delta = NormalizeAngle(p2cAngle - p1cAngle);
           double theta = p1cAngle + delta / 2;
 
-          double ox = controller->GetAngleTopTextLabelDistanceS() * cos(theta);
-          double oy = controller->GetAngleTopTextLabelDistanceS() * sin(theta);
+          double ox = controller.GetAngleTopTextLabelDistanceS() * cos(theta);
+          double oy = controller.GetAngleTopTextLabelDistanceS() * sin(theta);
 
           double pointX = center_.GetX() + ox;
           double pointY = center_.GetY() + oy;
@@ -307,10 +358,10 @@
 
 #if ORTHANC_STONE_ENABLE_OUTLINED_TEXT == 1
           SetTextLayerOutlineProperties(
-            GetController()->GetScene(), layerHolder_, buf, ScenePoint2D(pointX, pointY), 0);
+            scene, layerHolder_, buf, ScenePoint2D(pointX, pointY), 0);
 #else
           SetTextLayerProperties(
-            GetController()->GetScene(), layerHolder_, buf, ScenePoint2D(pointX, pointY) , 0);
+            scene, layerHolder_, buf, ScenePoint2D(pointX, pointY) , 0);
 #endif
 
 #if 0
@@ -370,6 +421,7 @@
       {
         RemoveFromScene();
       }
+      lock->Invalidate();
     }
   }
 }
--- a/Framework/Scene2DViewport/AngleMeasureTool.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/AngleMeasureTool.h	Tue Mar 31 15:51:02 2020 +0200
@@ -37,10 +37,10 @@
 
 namespace OrthancStone
 {
-  class AngleMeasureTool : public MeasureTool, public boost::enable_shared_from_this<AngleMeasureTool>
+  class AngleMeasureTool : public MeasureTool
   {
   public:
-    AngleMeasureTool(MessageBroker& broker, boost::weak_ptr<ViewportController> controllerW);
+    static boost::shared_ptr<AngleMeasureTool> Create(boost::shared_ptr<IViewport> viewport);
 
     ~AngleMeasureTool();
 
@@ -48,7 +48,7 @@
     void SetCenter(ScenePoint2D start);
     void SetSide2End(ScenePoint2D start);
 
-    virtual bool HitTest(ScenePoint2D p) const ORTHANC_OVERRIDE;
+    virtual bool HitTest(ScenePoint2D p) ORTHANC_OVERRIDE;
     virtual void Highlight(ScenePoint2D p) ORTHANC_OVERRIDE;
     virtual void ResetHighlightState() ORTHANC_OVERRIDE;
     virtual boost::shared_ptr<IFlexiblePointerTracker> CreateEditionTracker(const PointerEvent& e) ORTHANC_OVERRIDE;
@@ -70,6 +70,8 @@
     AngleHighlightArea AngleHitTest(ScenePoint2D p) const;
 
   private:
+    AngleMeasureTool(boost::shared_ptr<IViewport> viewport);
+
     virtual void        RefreshScene() ORTHANC_OVERRIDE;
     void                RemoveFromScene();
     void                SetAngleHighlightArea(AngleHighlightArea area);
--- a/Framework/Scene2DViewport/CreateAngleMeasureCommand.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/CreateAngleMeasureCommand.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -26,14 +26,16 @@
 namespace OrthancStone
 {
   CreateAngleMeasureCommand::CreateAngleMeasureCommand(
-    MessageBroker& broker,
-    boost::weak_ptr<ViewportController> controllerW,
+    boost::shared_ptr<IViewport> viewport,
     ScenePoint2D           point)
-    : CreateMeasureCommand(controllerW)
-    , measureTool_(
-      boost::make_shared<AngleMeasureTool>(boost::ref(broker), controllerW))
+    : CreateMeasureCommand(viewport)
+    , measureTool_(AngleMeasureTool::Create(viewport))
   {
-    GetController()->AddMeasureTool(measureTool_);
+    
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    ViewportController& controller = lock->GetController();
+
+    controller.AddMeasureTool(measureTool_);
     measureTool_->SetSide1End(point);
     measureTool_->SetCenter(point);
     measureTool_->SetSide2End(point);
--- a/Framework/Scene2DViewport/CreateAngleMeasureCommand.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/CreateAngleMeasureCommand.h	Tue Mar 31 15:51:02 2020 +0200
@@ -28,8 +28,7 @@
   public:
     /** Ctor sets end of side 1*/
     CreateAngleMeasureCommand(
-      MessageBroker& broker,
-      boost::weak_ptr<ViewportController> controllerW,
+      boost::shared_ptr<IViewport> viewport,
       ScenePoint2D           point);
 
     /** This method sets center*/
--- a/Framework/Scene2DViewport/CreateAngleMeasureTracker.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/CreateAngleMeasureTracker.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -26,17 +26,18 @@
 namespace OrthancStone
 {
   CreateAngleMeasureTracker::CreateAngleMeasureTracker(
-    MessageBroker&                  broker,
-    boost::weak_ptr<ViewportController>          controllerW,
-    const PointerEvent&             e)
-    : CreateMeasureTracker(controllerW)
+    boost::shared_ptr<IViewport> viewport,
+    const PointerEvent& e)
+    : CreateMeasureTracker(viewport)
     , state_(CreatingSide1)
   {
-    command_.reset(
-      new CreateAngleMeasureCommand(
-        broker,
-        controllerW,
-        e.GetMainPosition().Apply(GetScene().GetCanvasToSceneTransform())));
+    ScenePoint2D point = e.GetMainPosition();
+    {    
+      std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+      Scene2D& scene = lock->GetController().GetScene();
+      point = e.GetMainPosition().Apply(scene.GetCanvasToSceneTransform());
+    }
+    command_.reset(new CreateAngleMeasureCommand(viewport, point));
   }
 
   CreateAngleMeasureTracker::~CreateAngleMeasureTracker()
@@ -52,24 +53,31 @@
         "PointerMove: active_ == false");
     }
 
-    ScenePoint2D scenePos = event.GetMainPosition().Apply(
-      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");
+      std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+      ViewportController& controller = lock->GetController();
+
+      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();
+      lock->Invalidate();
     }
-    //LOG(TRACE) << "scenePos.GetX() = " << scenePos.GetX() << "     " <<
-    //  "scenePos.GetY() = " << scenePos.GetY();
   }
 
   void CreateAngleMeasureTracker::PointerUp(const PointerEvent& e)
--- a/Framework/Scene2DViewport/CreateAngleMeasureTracker.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/CreateAngleMeasureTracker.h	Tue Mar 31 15:51:02 2020 +0200
@@ -38,8 +38,7 @@
     must be supplied, too
     */
     CreateAngleMeasureTracker(
-      MessageBroker&                  broker,
-      boost::weak_ptr<ViewportController>          controllerW,
+      boost::shared_ptr<IViewport>          viewport,
       const PointerEvent&             e);
 
     ~CreateAngleMeasureTracker();
--- a/Framework/Scene2DViewport/CreateLineMeasureCommand.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/CreateLineMeasureCommand.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -26,15 +26,17 @@
 namespace OrthancStone
 {
   CreateLineMeasureCommand::CreateLineMeasureCommand(
-    MessageBroker& broker,
-    boost::weak_ptr<ViewportController> controllerW,
+    boost::shared_ptr<IViewport> viewport,
     ScenePoint2D           point)
-    : CreateMeasureCommand(controllerW)
-    , measureTool_(
-      boost::make_shared<LineMeasureTool>(boost::ref(broker), controllerW))
+    : CreateMeasureCommand(viewport)
+    , measureTool_(LineMeasureTool::Create(viewport))
   {
-    GetController()->AddMeasureTool(measureTool_);
+    
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    ViewportController& controller = lock->GetController();
+    controller.AddMeasureTool(measureTool_);
     measureTool_->Set(point, point);
+    lock->Invalidate();
   }
 
   void CreateLineMeasureCommand::SetEnd(ScenePoint2D scenePos)
--- a/Framework/Scene2DViewport/CreateLineMeasureCommand.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/CreateLineMeasureCommand.h	Tue Mar 31 15:51:02 2020 +0200
@@ -27,8 +27,7 @@
   {
   public:
     CreateLineMeasureCommand(
-      MessageBroker& broker,
-      boost::weak_ptr<ViewportController> controllerW,
+      boost::shared_ptr<IViewport> viewport,
       ScenePoint2D           point);
 
     // the starting position is set in the ctor
--- a/Framework/Scene2DViewport/CreateLineMeasureTracker.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/CreateLineMeasureTracker.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -26,16 +26,17 @@
 namespace OrthancStone
 {
   CreateLineMeasureTracker::CreateLineMeasureTracker(
-    MessageBroker&                  broker,
-    boost::weak_ptr<ViewportController>          controllerW,
+    boost::shared_ptr<IViewport>          viewport,
     const PointerEvent&             e)
-    : CreateMeasureTracker(controllerW)
+    : CreateMeasureTracker(viewport)
   {
-    command_.reset(
-      new CreateLineMeasureCommand(
-        broker,
-        controllerW,
-        e.GetMainPosition().Apply(GetScene().GetCanvasToSceneTransform())));
+    ScenePoint2D point = e.GetMainPosition();
+    {
+      std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+      ViewportController& controller = lock->GetController();
+      point = e.GetMainPosition().Apply(controller.GetScene().GetCanvasToSceneTransform());
+    }
+    command_.reset(new CreateLineMeasureCommand(viewport, point));
   }
 
   CreateLineMeasureTracker::~CreateLineMeasureTracker()
@@ -52,12 +53,15 @@
         "PointerMove: active_ == false");
     }
 
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    ViewportController& controller = lock->GetController();
+
     ScenePoint2D scenePos = event.GetMainPosition().Apply(
-      GetScene().GetCanvasToSceneTransform());
-
+      controller.GetScene().GetCanvasToSceneTransform());
+      
     //LOG(TRACE) << "scenePos.GetX() = " << scenePos.GetX() << "     " <<
     //  "scenePos.GetY() = " << scenePos.GetY();
-
+      
     CreateLineMeasureTracker* concreteThis =
       dynamic_cast<CreateLineMeasureTracker*>(this);
     assert(concreteThis != NULL);
@@ -84,5 +88,4 @@
   {
     return boost::dynamic_pointer_cast<CreateLineMeasureCommand>(command_);
   }
-
 }
--- a/Framework/Scene2DViewport/CreateLineMeasureTracker.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/CreateLineMeasureTracker.h	Tue Mar 31 15:51:02 2020 +0200
@@ -38,8 +38,7 @@
     must be supplied, too
     */
     CreateLineMeasureTracker(
-      MessageBroker&                  broker,
-      boost::weak_ptr<ViewportController>          controllerW,
+      boost::shared_ptr<IViewport>          viewport,
       const PointerEvent&             e);
 
     ~CreateLineMeasureTracker();
--- a/Framework/Scene2DViewport/EditAngleMeasureCommand.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/EditAngleMeasureCommand.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -23,31 +23,30 @@
 namespace OrthancStone
 {
   EditAngleMeasureCommand::EditAngleMeasureCommand(
-    boost::shared_ptr<AngleMeasureTool>  measureTool,
-    MessageBroker& broker,
-    boost::weak_ptr<ViewportController> controllerW)
-    : EditMeasureCommand(measureTool, controllerW)
+    boost::shared_ptr<MeasureTool>  measureTool,
+    boost::shared_ptr<IViewport> viewport)
+    : EditMeasureCommand(measureTool, viewport)
     , measureTool_(measureTool)
   {
   }
 
   void EditAngleMeasureCommand::SetCenter(ScenePoint2D scenePos)
   {
-    measureTool_->SetCenter(scenePos);
+    dynamic_cast<AngleMeasureTool&>(*measureTool_).SetCenter(scenePos);
     mementoModified_ = measureTool_->GetMemento();
   }
 
 
   void EditAngleMeasureCommand::SetSide1End(ScenePoint2D scenePos)
   {
-    measureTool_->SetSide1End(scenePos);
+    dynamic_cast<AngleMeasureTool&>(*measureTool_).SetSide1End(scenePos);
     mementoModified_ = measureTool_->GetMemento();
   }
 
 
   void EditAngleMeasureCommand::SetSide2End(ScenePoint2D scenePos)
   {
-    measureTool_->SetSide2End(scenePos);
+    dynamic_cast<AngleMeasureTool&>(*measureTool_).SetSide2End(scenePos);
     mementoModified_ = measureTool_->GetMemento();
   }
 }
--- a/Framework/Scene2DViewport/EditAngleMeasureCommand.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/EditAngleMeasureCommand.h	Tue Mar 31 15:51:02 2020 +0200
@@ -28,9 +28,8 @@
   public:
     /** Ctor sets end of side 1*/
     EditAngleMeasureCommand(
-      boost::shared_ptr<AngleMeasureTool>  measureTool,
-      MessageBroker& broker,
-      boost::weak_ptr<ViewportController> controllerW);
+      boost::shared_ptr<MeasureTool>  measureTool,
+      boost::shared_ptr<IViewport> viewport);
 
     /** This method sets center*/
     void SetCenter(ScenePoint2D scenePos);
@@ -46,6 +45,6 @@
     {
       return measureTool_;
     }
-    boost::shared_ptr<AngleMeasureTool> measureTool_;
+    boost::shared_ptr<MeasureTool> measureTool_;
   };
 }
--- a/Framework/Scene2DViewport/EditAngleMeasureTracker.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/EditAngleMeasureTracker.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -26,18 +26,19 @@
 namespace OrthancStone
 {
   EditAngleMeasureTracker::EditAngleMeasureTracker(
-    boost::shared_ptr<AngleMeasureTool>  measureTool,
-    MessageBroker& broker,
-    boost::weak_ptr<ViewportController> controllerW,
+    boost::shared_ptr<MeasureTool>  measureTool,
+    boost::shared_ptr<IViewport> viewport,
     const PointerEvent& e)
-    : EditMeasureTracker(controllerW, e)
+    : EditMeasureTracker(viewport, e)
   {
-    ScenePoint2D scenePos = e.GetMainPosition().Apply(
-      GetScene().GetCanvasToSceneTransform());
-
-    modifiedZone_ = measureTool->AngleHitTest(scenePos);
-
-    command_.reset(new EditAngleMeasureCommand(measureTool, broker, controllerW));
+    ScenePoint2D scenePos = e.GetMainPosition();
+    {
+      std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+      ViewportController& controller = lock->GetController();
+      scenePos = e.GetMainPosition().Apply(controller.GetScene().GetCanvasToSceneTransform());
+    }
+    modifiedZone_ = dynamic_cast<AngleMeasureTool&>(*measureTool).AngleHitTest(scenePos);
+    command_.reset(new EditAngleMeasureCommand(measureTool, viewport));
   }
 
   EditAngleMeasureTracker::~EditAngleMeasureTracker()
@@ -47,8 +48,12 @@
 
   void EditAngleMeasureTracker::PointerMove(const PointerEvent& e)
   {
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    ViewportController& controller = lock->GetController();
+    Scene2D& scene = controller.GetScene();
+
     ScenePoint2D scenePos = e.GetMainPosition().Apply(
-      GetScene().GetCanvasToSceneTransform());
+      scene.GetCanvasToSceneTransform());
 
     ScenePoint2D delta = scenePos - GetOriginalClickPosition();
 
@@ -59,38 +64,38 @@
 
     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!";
+      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;
     }
   }
 
@@ -111,5 +116,4 @@
     ORTHANC_ASSERT(ret.get() != NULL, "Internal error in EditAngleMeasureTracker::GetCommand()");
     return ret;
   }
-
 }
--- a/Framework/Scene2DViewport/EditAngleMeasureTracker.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/EditAngleMeasureTracker.h	Tue Mar 31 15:51:02 2020 +0200
@@ -37,9 +37,8 @@
     must be supplied, too
     */
     EditAngleMeasureTracker(
-      boost::shared_ptr<AngleMeasureTool>  measureTool,
-      MessageBroker& broker,
-      boost::weak_ptr<ViewportController> controllerW,
+      boost::shared_ptr<MeasureTool>  measureTool,
+      boost::shared_ptr<IViewport> viewport,
       const PointerEvent& e);
 
     ~EditAngleMeasureTracker();
--- a/Framework/Scene2DViewport/EditLineMeasureCommand.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/EditLineMeasureCommand.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -23,10 +23,9 @@
 namespace OrthancStone
 {
   EditLineMeasureCommand::EditLineMeasureCommand(
-    boost::shared_ptr<LineMeasureTool>  measureTool,
-    MessageBroker& broker,
-    boost::weak_ptr<ViewportController> controllerW)
-    : EditMeasureCommand(measureTool, controllerW)
+    boost::shared_ptr<MeasureTool>  measureTool,
+    boost::shared_ptr<IViewport> viewport)
+    : EditMeasureCommand(measureTool, viewport)
     , measureTool_(measureTool)
   {
   }
@@ -34,14 +33,14 @@
 
   void EditLineMeasureCommand::SetStart(ScenePoint2D scenePos)
   {
-    measureTool_->SetStart(scenePos);
+    dynamic_cast<LineMeasureTool&>(*measureTool_).SetStart(scenePos);
     mementoModified_ = measureTool_->GetMemento();
   }
 
 
   void EditLineMeasureCommand::SetEnd(ScenePoint2D scenePos)
   {
-    measureTool_->SetEnd(scenePos);
+    dynamic_cast<LineMeasureTool&>(*measureTool_).SetEnd(scenePos);
     mementoModified_ = measureTool_->GetMemento();
   }
 }
--- a/Framework/Scene2DViewport/EditLineMeasureCommand.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/EditLineMeasureCommand.h	Tue Mar 31 15:51:02 2020 +0200
@@ -27,9 +27,8 @@
   {
   public:
     EditLineMeasureCommand(
-      boost::shared_ptr<LineMeasureTool>  measureTool,
-      MessageBroker& broker,
-      boost::weak_ptr<ViewportController> controllerW);
+      boost::shared_ptr<MeasureTool>  measureTool,
+      boost::shared_ptr<IViewport> viewport);
 
     void SetStart(ScenePoint2D scenePos);
     void SetEnd(ScenePoint2D scenePos);
@@ -39,7 +38,6 @@
     {
       return measureTool_;
     }
-    boost::shared_ptr<LineMeasureTool> measureTool_;
+    boost::shared_ptr<MeasureTool> measureTool_;
   };
 }
-
--- a/Framework/Scene2DViewport/EditLineMeasureTracker.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/EditLineMeasureTracker.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -27,22 +27,19 @@
 namespace OrthancStone
 {
   EditLineMeasureTracker::EditLineMeasureTracker(
-    boost::shared_ptr<LineMeasureTool>  measureTool,
-    MessageBroker& broker,
-    boost::weak_ptr<ViewportController> controllerW,
-    const PointerEvent& e) 
-    : EditMeasureTracker(controllerW, e)
+    boost::shared_ptr<MeasureTool>  measureTool,
+    boost::shared_ptr<IViewport> viewport,
+    const PointerEvent& e)
+    : EditMeasureTracker(viewport, e)
   {
-    ScenePoint2D scenePos = e.GetMainPosition().Apply(
-      GetScene().GetCanvasToSceneTransform());
-
-    modifiedZone_ = measureTool->LineHitTest(scenePos);
-
-    command_.reset(
-      new EditLineMeasureCommand(
-        measureTool,
-        broker,
-        controllerW));
+    ScenePoint2D scenePos = e.GetMainPosition();
+    {
+      std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+      Scene2D& scene = lock->GetController().GetScene();
+      scenePos = e.GetMainPosition().Apply(scene.GetCanvasToSceneTransform());
+    }
+    modifiedZone_ = dynamic_cast<LineMeasureTool&>(*measureTool).LineHitTest(scenePos);
+    command_.reset(new EditLineMeasureCommand(measureTool, viewport));
   }
 
   EditLineMeasureTracker::~EditLineMeasureTracker()
@@ -52,8 +49,12 @@
 
   void EditLineMeasureTracker::PointerMove(const PointerEvent& e)
   {
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    ViewportController& controller = lock->GetController();
+    Scene2D& scene = controller.GetScene();
+
     ScenePoint2D scenePos = e.GetMainPosition().Apply(
-      GetScene().GetCanvasToSceneTransform());
+      scene.GetCanvasToSceneTransform());
 
     ScenePoint2D delta = scenePos - GetOriginalClickPosition();
 
@@ -86,7 +87,7 @@
     break;
     default:
       LOG(WARNING) << "Warning: please retry the measuring tool editing operation!";
-        break;
+      break;
     }
   }
 
--- a/Framework/Scene2DViewport/EditLineMeasureTracker.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/EditLineMeasureTracker.h	Tue Mar 31 15:51:02 2020 +0200
@@ -37,9 +37,8 @@
     must be supplied, too
     */
     EditLineMeasureTracker(
-      boost::shared_ptr<LineMeasureTool>  measureTool,
-      MessageBroker&                      broker,
-      boost::weak_ptr<ViewportController> controllerW,
+      boost::shared_ptr<MeasureTool>  measureTool,
+      boost::shared_ptr<IViewport> viewport,
       const PointerEvent&                 e);
 
     ~EditLineMeasureTracker();
--- a/Framework/Scene2DViewport/LayerHolder.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/LayerHolder.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -22,20 +22,20 @@
 #include "../Scene2D/TextSceneLayer.h"
 #include "../Scene2D/PolylineSceneLayer.h"
 #include "../Scene2D/Scene2D.h"
-#include "../Scene2DViewport/ViewportController.h"
+#include "../Viewport/IViewport.h"
 #include "../StoneException.h"
 
 namespace OrthancStone
 {
   LayerHolder::LayerHolder(
-    boost::weak_ptr<ViewportController> controllerW,
-    int                    polylineLayerCount,
-    int                    textLayerCount,
-    int                    infoTextCount)
+    boost::shared_ptr<IViewport> viewport,
+    int        polylineLayerCount,
+    int        textLayerCount,
+    int        infoTextCount)
     : textLayerCount_(textLayerCount)
     , polylineLayerCount_(polylineLayerCount)
     , infoTextCount_(infoTextCount)
-    , controllerW_(controllerW)
+    , viewport_(viewport)
     , baseLayerIndex_(-1)
   {
 
@@ -43,24 +43,26 @@
 
   void LayerHolder::CreateLayers()
   {
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    ViewportController& controller = lock->GetController();
+    Scene2D& scene = controller.GetScene();
+
     assert(baseLayerIndex_ == -1);
 
-    baseLayerIndex_ = GetScene().GetMaxDepth() + 100;
+    baseLayerIndex_ = scene.GetMaxDepth() + 100;
 
     for (int i = 0; i < polylineLayerCount_; ++i)
     {
       std::unique_ptr<PolylineSceneLayer> layer(new PolylineSceneLayer());
-      GetScene().SetLayer(baseLayerIndex_ + i, layer.release());
+      scene.SetLayer(baseLayerIndex_ + i, layer.release());
     }
 
     for (int i = 0; i < textLayerCount_; ++i)
     {
       std::unique_ptr<TextSceneLayer> layer(new TextSceneLayer());
-      GetScene().SetLayer(
-        baseLayerIndex_ + polylineLayerCount_ + i,
-        layer.release());
+      scene.SetLayer(baseLayerIndex_ + polylineLayerCount_ + i, layer.release());
     }
-
+    lock->Invalidate();
   }
 
   void LayerHolder::CreateLayersIfNeeded()
@@ -74,13 +76,6 @@
     return (baseLayerIndex_ != -1);
   }
 
-  Scene2D& LayerHolder::GetScene()
-  {
-    boost::shared_ptr<ViewportController> controller = controllerW_.lock();
-    ORTHANC_ASSERT(controller.get() != 0, "Zombie attack!");
-    return controller->GetScene();
-  }
-
   void LayerHolder::DeleteLayersIfNeeded()
   {
     if (baseLayerIndex_ != -1)
@@ -89,40 +84,48 @@
   
   void LayerHolder::DeleteLayers()
   {
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    Scene2D& scene = lock->GetController().GetScene();
+
     for (int i = 0; i < textLayerCount_ + polylineLayerCount_; ++i)
     {
-      ORTHANC_ASSERT(GetScene().HasLayer(baseLayerIndex_ + i), "No layer");
-      GetScene().DeleteLayer(baseLayerIndex_ + i);
+      ORTHANC_ASSERT(scene.HasLayer(baseLayerIndex_ + i), "No layer");
+      scene.DeleteLayer(baseLayerIndex_ + i);
     }
     baseLayerIndex_ = -1;
+    lock->Invalidate();
   }
-
+  
   PolylineSceneLayer* LayerHolder::GetPolylineLayer(int index /*= 0*/)
   {
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    Scene2D& scene = lock->GetController().GetScene();
+
     using namespace Orthanc;
     ORTHANC_ASSERT(baseLayerIndex_ != -1);
-    ORTHANC_ASSERT(GetScene().HasLayer(GetPolylineLayerIndex(index)));
-    ISceneLayer* layer =
-      &(GetScene().GetLayer(GetPolylineLayerIndex(index)));
-
+    ORTHANC_ASSERT(scene.HasLayer(GetPolylineLayerIndex(index)));
+    ISceneLayer* layer = &(scene.GetLayer(GetPolylineLayerIndex(index)));
+      
     PolylineSceneLayer* concreteLayer =
       dynamic_cast<PolylineSceneLayer*>(layer);
-
+      
     ORTHANC_ASSERT(concreteLayer != NULL);
     return concreteLayer;
   }
 
   TextSceneLayer* LayerHolder::GetTextLayer(int index /*= 0*/)
   {
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    Scene2D& scene = lock->GetController().GetScene();
+
     using namespace Orthanc;
     ORTHANC_ASSERT(baseLayerIndex_ != -1);
-    ORTHANC_ASSERT(GetScene().HasLayer(GetTextLayerIndex(index)));
-    ISceneLayer* layer =
-      &(GetScene().GetLayer(GetTextLayerIndex(index)));
-
+    ORTHANC_ASSERT(scene.HasLayer(GetTextLayerIndex(index)));
+    ISceneLayer* layer = &(scene.GetLayer(GetTextLayerIndex(index)));
+      
     TextSceneLayer* concreteLayer =
       dynamic_cast<TextSceneLayer*>(layer);
-
+      
     ORTHANC_ASSERT(concreteLayer != NULL);
     return concreteLayer;
   }
@@ -133,8 +136,7 @@
     ORTHANC_ASSERT(index < polylineLayerCount_);
     return baseLayerIndex_ + index;
   }
-
-
+  
   int LayerHolder::GetTextLayerIndex(int index /*= 0*/)
   {
     using namespace Orthanc;
--- a/Framework/Scene2DViewport/LayerHolder.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/LayerHolder.h	Tue Mar 31 15:51:02 2020 +0200
@@ -43,7 +43,7 @@
     performed at this time
     */
     LayerHolder(
-      boost::weak_ptr<ViewportController> controllerW,
+      boost::shared_ptr<IViewport> viewport,
       int polylineLayerCount, int textLayerCount, int infoTextCount = 0);
 
     /**
@@ -97,12 +97,11 @@
     int GetPolylineLayerIndex(int index = 0);
     int GetTextLayerIndex(int index = 0);
     int GetInfoTextLayerIndex(int index = 0);
-    Scene2D& GetScene();
 
     int textLayerCount_;
     int polylineLayerCount_;
     int infoTextCount_;
-    boost::weak_ptr<ViewportController> controllerW_;
+    boost::shared_ptr<IViewport> viewport_;
     int baseLayerIndex_;
   };
 }
--- a/Framework/Scene2DViewport/LineMeasureTool.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/LineMeasureTool.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -32,18 +32,26 @@
 {
 
   LineMeasureTool::LineMeasureTool(
-    MessageBroker& broker, boost::weak_ptr<ViewportController> controllerW)
-    : MeasureTool(broker, controllerW)
+    boost::shared_ptr<IViewport> viewport)
+    : MeasureTool(viewport)
 #if ORTHANC_STONE_ENABLE_OUTLINED_TEXT == 1
-    , layerHolder_(boost::make_shared<LayerHolder>(controllerW, 1, 5))
+    , layerHolder_(boost::shared_ptr<LayerHolder>(new LayerHolder(viewport,1,5)))
 #else
-    , layerHolder_(boost::make_shared<LayerHolder>(controllerW, 1, 1))
+    , layerHolder_(boost::shared_ptr<LayerHolder>(new LayerHolder(viewport,1,1)))
 #endif
     , lineHighlightArea_(LineHighlightArea_None)
   {
 
   }
 
+  boost::shared_ptr<LineMeasureTool> LineMeasureTool::Create(boost::shared_ptr<IViewport> viewport)
+  {
+    boost::shared_ptr<LineMeasureTool> obj(new LineMeasureTool(viewport));
+    obj->MeasureTool::PostConstructor();
+    obj->RefreshScene();
+    return obj;
+  }
+
   LineMeasureTool::~LineMeasureTool()
   {
     // this measuring tool is a RABI for the corresponding visual layers
@@ -106,53 +114,59 @@
     SetLineHighlightArea(lineHighlightArea);
   }
 
-  LineMeasureTool::LineHighlightArea LineMeasureTool::LineHitTest(ScenePoint2D p) const
+  LineMeasureTool::LineHighlightArea LineMeasureTool::LineHitTest(ScenePoint2D p)
   {
-    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;
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    ViewportController& controller = lock->GetController();
+    Scene2D& scene = controller.GetScene();
 
-    const double sqDistanceFromStart = ScenePoint2D::SquaredDistancePtPt(p, start_);
+    const double pixelToScene = scene.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;
+
+    const double sqDistanceFromStart = 
+      ScenePoint2D::SquaredDistancePtPt(p, start_);
+    
     if (sqDistanceFromStart <= SQUARED_HIT_TEST_MAX_DISTANCE_SCENE_COORD)
       return LineHighlightArea_Start;
     
     const double sqDistanceFromEnd = ScenePoint2D::SquaredDistancePtPt(p, end_);
+
     if (sqDistanceFromEnd <= SQUARED_HIT_TEST_MAX_DISTANCE_SCENE_COORD)
       return LineHighlightArea_End;
 
-    const double sqDistanceFromPtSegment = ScenePoint2D::SquaredDistancePtSegment(start_, end_, p);
+    const double sqDistanceFromPtSegment = 
+      ScenePoint2D::SquaredDistancePtSegment(start_, end_, p);
+    
     if (sqDistanceFromPtSegment <= SQUARED_HIT_TEST_MAX_DISTANCE_SCENE_COORD)
       return LineHighlightArea_Segment;
 
     return LineHighlightArea_None;
   }
 
-  bool LineMeasureTool::HitTest(ScenePoint2D p) const
+  bool LineMeasureTool::HitTest(ScenePoint2D p)
   {
     return LineHitTest(p) != LineHighlightArea_None;
   }
 
   boost::shared_ptr<IFlexiblePointerTracker> LineMeasureTool::CreateEditionTracker(const PointerEvent& e)
   {
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    ViewportController& controller = lock->GetController();
+    Scene2D& scene = controller.GetScene();
+
     ScenePoint2D scenePos = e.GetMainPosition().Apply(
-      GetController()->GetScene().GetCanvasToSceneTransform());
+      scene.GetCanvasToSceneTransform());
 
     if (!HitTest(scenePos))
       return boost::shared_ptr<IFlexiblePointerTracker>();
 
-    /**
-      new EditLineMeasureTracker(
-        boost::shared_ptr<LineMeasureTool> measureTool;
-        MessageBroker & broker,
-        boost::weak_ptr<ViewportController>          controllerW,
-        const PointerEvent & e);
-    */
     boost::shared_ptr<EditLineMeasureTracker> editLineMeasureTracker(
-      new EditLineMeasureTracker(shared_from_this(), GetBroker(), GetController(), e));
+      new EditLineMeasureTracker(shared_from_this(), viewport_, e));
     return editLineMeasureTracker;
   }
 
-
   boost::shared_ptr<MeasureToolMemento> LineMeasureTool::GetMemento() const
   {
     boost::shared_ptr<LineMeasureToolMemento> memento(new LineMeasureToolMemento());
@@ -161,10 +175,14 @@
     return memento;
   }
 
-  void LineMeasureTool::SetMemento(boost::shared_ptr<MeasureToolMemento> mementoBase)
+  void LineMeasureTool::SetMemento(
+    boost::shared_ptr<MeasureToolMemento> mementoBase)
   {
-    boost::shared_ptr<LineMeasureToolMemento> memento = boost::dynamic_pointer_cast<LineMeasureToolMemento>(mementoBase);
+    boost::shared_ptr<LineMeasureToolMemento> memento = 
+      boost::dynamic_pointer_cast<LineMeasureToolMemento>(mementoBase);
+    
     ORTHANC_ASSERT(memento.get() != NULL, "Internal error: wrong (or bad) memento");
+    
     start_ = memento->start_;
     end_ = memento->end_;
     RefreshScene();
@@ -176,61 +194,67 @@
     {
       if (IsEnabled())
       {
+        
+        std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+        ViewportController& controller = lock->GetController();
+        Scene2D& scene = controller.GetScene();
+
         layerHolder_->CreateLayersIfNeeded();
-
         {
           // 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, controller.GetScene(), start_, 
+                          controller.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, controller.GetScene(), end_, 
+                          controller.GetHandleSideLengthS());
+              
+                if (lineHighlightArea_ == LineHighlightArea_End)
+                  polylineLayer->AddChain(chain, true, highlightColor);
+                else
+                  polylineLayer->AddChain(chain, true, color);
+              }
             }
           }
-
         }
         {
           // Set the text layer propreties
@@ -246,14 +270,19 @@
           double midX = 0.5 * (end_.GetX() + start_.GetX());
           double midY = 0.5 * (end_.GetY() + start_.GetY());
 
+          {
+
 #if ORTHANC_STONE_ENABLE_OUTLINED_TEXT == 1
-          SetTextLayerOutlineProperties(
-            GetController()->GetScene(), layerHolder_, buf, ScenePoint2D(midX, midY), 0);
+            SetTextLayerOutlineProperties(
+              scene, layerHolder_, buf, ScenePoint2D(midX, midY), 0);
 #else
-          SetTextLayerProperties(
-            GetController()->GetScene(), layerHolder_, buf, ScenePoint2D(midX, midY), 0);
+            SetTextLayerProperties(
+              scene, layerHolder_, buf, ScenePoint2D(midX, midY), 0);
 #endif
+            lock->Invalidate();
+          }
         }
+        lock->Invalidate();
       }
       else
       {
--- a/Framework/Scene2DViewport/LineMeasureTool.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/LineMeasureTool.h	Tue Mar 31 15:51:02 2020 +0200
@@ -35,10 +35,10 @@
 
 namespace OrthancStone
 {
-  class LineMeasureTool : public MeasureTool, public boost::enable_shared_from_this<LineMeasureTool>
+  class LineMeasureTool : public MeasureTool
   {
   public:
-    LineMeasureTool(MessageBroker& broker, boost::weak_ptr<ViewportController> controllerW);
+    static boost::shared_ptr<LineMeasureTool> Create(boost::shared_ptr<IViewport> viewport);
 
     ~LineMeasureTool();
 
@@ -47,7 +47,7 @@
     void Set(ScenePoint2D start, ScenePoint2D end);
 
 
-    virtual bool HitTest(ScenePoint2D p) const ORTHANC_OVERRIDE;
+    virtual bool HitTest(ScenePoint2D p) ORTHANC_OVERRIDE;
     virtual void Highlight(ScenePoint2D p) ORTHANC_OVERRIDE;
     virtual void ResetHighlightState() ORTHANC_OVERRIDE;
     virtual boost::shared_ptr<IFlexiblePointerTracker> CreateEditionTracker(const PointerEvent& e) ORTHANC_OVERRIDE;
@@ -64,9 +64,11 @@
     };
 
 
-    LineHighlightArea LineHitTest(ScenePoint2D p) const;
+    LineHighlightArea LineHitTest(ScenePoint2D p);
 
   private:
+    LineMeasureTool(boost::shared_ptr<IViewport> viewport);
+
     virtual void        RefreshScene() ORTHANC_OVERRIDE;
     void                RemoveFromScene();
     void                SetLineHighlightArea(LineHighlightArea area);
--- a/Framework/Scene2DViewport/MeasureCommands.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/MeasureCommands.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -20,6 +20,8 @@
 
 #include "MeasureCommands.h"
 
+#include <memory>
+
 #include <boost/make_shared.hpp>
 #include <boost/ref.hpp>
 
@@ -27,19 +29,21 @@
 {
   void CreateMeasureCommand::Undo()
   {
+    std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
     // simply disable the measure tool upon undo
     GetMeasureTool()->Disable();
-    GetController()->RemoveMeasureTool(GetMeasureTool());
+    lock->GetController().RemoveMeasureTool(GetMeasureTool());
   }
 
   void CreateMeasureCommand::Redo()
   {
+    std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
     GetMeasureTool()->Enable();
-    GetController()->AddMeasureTool(GetMeasureTool());
+    lock->GetController().AddMeasureTool(GetMeasureTool());
   }
 
-  CreateMeasureCommand::CreateMeasureCommand(boost::weak_ptr<ViewportController> controllerW)
-    : MeasureCommand(controllerW)
+  CreateMeasureCommand::CreateMeasureCommand(boost::shared_ptr<IViewport> viewport)
+    : MeasureCommand(viewport)
   {
 
   }
@@ -52,15 +56,17 @@
 
   void DeleteMeasureCommand::Redo()
   {
+    std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
     // simply disable the measure tool upon undo
     GetMeasureTool()->Disable();
-    GetController()->RemoveMeasureTool(GetMeasureTool());
+    lock->GetController().RemoveMeasureTool(GetMeasureTool());
   }
 
   void DeleteMeasureCommand::Undo()
   {
+    std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
     GetMeasureTool()->Enable();
-    GetController()->AddMeasureTool(GetMeasureTool());
+    lock->GetController().AddMeasureTool(GetMeasureTool());
   }
 
   DeleteMeasureCommand::~DeleteMeasureCommand()
@@ -69,18 +75,19 @@
     // we thus leave it as is
   }
 
-  DeleteMeasureCommand::DeleteMeasureCommand(boost::shared_ptr<MeasureTool> measureTool, boost::weak_ptr<ViewportController> controllerW)
-    : MeasureCommand(controllerW)
+  DeleteMeasureCommand::DeleteMeasureCommand(boost::shared_ptr<MeasureTool> measureTool, boost::shared_ptr<IViewport> viewport)
+    : MeasureCommand(viewport)
     , mementoOriginal_(measureTool->GetMemento())
     , measureTool_(measureTool)
     , mementoModified_(measureTool->GetMemento())
   {
+    std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
     GetMeasureTool()->Disable();
-    GetController()->RemoveMeasureTool(GetMeasureTool());
+    lock->GetController().RemoveMeasureTool(GetMeasureTool());
   }
 
-  EditMeasureCommand::EditMeasureCommand(boost::shared_ptr<MeasureTool> measureTool, boost::weak_ptr<ViewportController> controllerW)
-    : MeasureCommand(controllerW)
+  EditMeasureCommand::EditMeasureCommand(boost::shared_ptr<MeasureTool> measureTool, boost::shared_ptr<IViewport> viewport)
+    : MeasureCommand(viewport)
     , mementoOriginal_(measureTool->GetMemento())
     , mementoModified_(measureTool->GetMemento())
   {
@@ -102,11 +109,4 @@
   {
     GetMeasureTool()->SetMemento(mementoModified_);
   }
-
-  boost::shared_ptr<ViewportController> MeasureCommand::GetController()
-  {
-    boost::shared_ptr<ViewportController> controller = controllerW_.lock();
-    assert(controller); // accessing dead object?
-    return controller;
-  }
 }
--- a/Framework/Scene2DViewport/MeasureCommands.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/MeasureCommands.h	Tue Mar 31 15:51:02 2020 +0200
@@ -19,7 +19,7 @@
  **/
 #pragma once
 
-#include "../Scene2D/Scene2D.h"
+#include "../Viewport/IViewport.h"
 
 // to be moved into Stone
 #include "PredeclaredTypes.h"
@@ -35,25 +35,21 @@
   class MeasureCommand : public boost::noncopyable
   {
   public:
-    MeasureCommand(boost::weak_ptr<ViewportController> controllerW) 
-      : controllerW_(controllerW)
-    {
-
-    }
+    MeasureCommand(boost::shared_ptr<IViewport> viewport) : viewport_(viewport)
+    {}
     virtual void Undo() = 0;
     virtual void Redo() = 0;
     
     virtual ~MeasureCommand() {};
 
   protected:
-    boost::shared_ptr<ViewportController>  GetController();
-    boost::weak_ptr<ViewportController> controllerW_;
+    boost::shared_ptr<IViewport> viewport_;
   };
 
   class CreateMeasureCommand : public MeasureCommand
   {
   public:
-    CreateMeasureCommand(boost::weak_ptr<ViewportController> controllerW);
+    CreateMeasureCommand(boost::shared_ptr<IViewport> viewport);
     virtual ~CreateMeasureCommand();
     virtual void Undo() ORTHANC_OVERRIDE;
     virtual void Redo() ORTHANC_OVERRIDE;
@@ -65,7 +61,7 @@
   class EditMeasureCommand : public MeasureCommand
   {
   public:
-    EditMeasureCommand(boost::shared_ptr<MeasureTool> measureTool, boost::weak_ptr<ViewportController> controllerW);
+    EditMeasureCommand(boost::shared_ptr<MeasureTool> measureTool, boost::shared_ptr<IViewport> viewport);
     virtual ~EditMeasureCommand();
     virtual void Undo() ORTHANC_OVERRIDE;
     virtual void Redo() ORTHANC_OVERRIDE;
@@ -86,7 +82,7 @@
   class DeleteMeasureCommand : public MeasureCommand
   {
   public:
-    DeleteMeasureCommand(boost::shared_ptr<MeasureTool> measureTool, boost::weak_ptr<ViewportController> controllerW);
+    DeleteMeasureCommand(boost::shared_ptr<MeasureTool> measureTool, boost::shared_ptr<IViewport> viewport);
     virtual ~DeleteMeasureCommand();
     virtual void Undo() ORTHANC_OVERRIDE;
     virtual void Redo() ORTHANC_OVERRIDE;
--- a/Framework/Scene2DViewport/MeasureTool.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/MeasureTool.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -26,16 +26,10 @@
 
 #include <boost/math/constants/constants.hpp>
 
+#include "../Viewport/IViewport.h"
+
 namespace OrthancStone
 {
-  MeasureTool::~MeasureTool()
-  {
-    // if the controller is dead, let's not bother.
-    boost::shared_ptr<ViewportController> controller = controllerW_.lock();
-    if (controller)
-      controller->Unregister(this);
-  }
-
   void MeasureTool::Enable()
   {
     enabled_ = true;
@@ -53,48 +47,30 @@
     return enabled_;
   }
 
-
-  boost::shared_ptr<const ViewportController> MeasureTool::GetController() const
+  MeasureTool::MeasureTool(
+    boost::shared_ptr<IViewport> viewport)
+    : viewport_(viewport)
+    , enabled_(true)
   {
-    boost::shared_ptr<const ViewportController> controller = controllerW_.lock();
-    if (!controller)
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
-        "Using dead ViewportController object!");
-    return controller;
+
   }
 
-  boost::shared_ptr<ViewportController> MeasureTool::GetController()
+  void MeasureTool::PostConstructor()
   {
-#if 1
-    return boost::const_pointer_cast<ViewportController>
-      (const_cast<const MeasureTool*>(this)->GetController());
-    //return boost::const_<boost::shared_ptr<ViewportController>>
-    //  (const_cast<const MeasureTool*>(this)->GetController());
-#else
-    boost::shared_ptr<ViewportController> controller = controllerW_.lock();
-    if (!controller)
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, 
-        "Using dead ViewportController object!");
-    return controller;
-#endif
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    ViewportController& controller = lock->GetController();
+
+    Register<ViewportController::SceneTransformChanged>(
+      controller, 
+      &MeasureTool::OnSceneTransformChanged);
   }
 
-  MeasureTool::MeasureTool(MessageBroker& broker,
-    boost::weak_ptr<ViewportController> controllerW)
-    : IObserver(broker)
-    , controllerW_(controllerW)
-    , enabled_(true)
-  {
-    GetController()->RegisterObserverCallback(
-      new Callable<MeasureTool, ViewportController::SceneTransformChanged>
-      (*this, &MeasureTool::OnSceneTransformChanged));
-  }
-
-
   bool MeasureTool::IsSceneAlive() const
   {
-    boost::shared_ptr<ViewportController> controller = controllerW_.lock();
-    return (controller.get() != NULL);
+    // since the lifetimes of the viewport, viewportcontroller (and the
+    // measuring tools inside it) are linked, the scene is always alive as 
+    // long as "this" is alive
+    return true;
   }
 
   void MeasureTool::OnSceneTransformChanged(
--- a/Framework/Scene2DViewport/MeasureTool.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/MeasureTool.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <boost/shared_ptr.hpp>
 #include <boost/weak_ptr.hpp>
 
 #include <vector>
@@ -38,10 +38,12 @@
   class IFlexiblePointerTracker;
   class MeasureToolMemento;
 
-  class MeasureTool : public IObserver
+  class MeasureTool : public ObserverBase<MeasureTool>
   {
   public:
-    virtual ~MeasureTool();
+    virtual ~MeasureTool()
+    {
+    }
 
     /**
     Enabled tools are rendered in the scene.
@@ -73,7 +75,7 @@
     true, then a click at that position will return a tracker to edit the 
     measuring tool
     */
-    virtual bool HitTest(ScenePoint2D p) const = 0;
+    virtual bool HitTest(ScenePoint2D p) = 0;
 
     /**
     This method must return a memento the captures the tool state (not including
@@ -111,7 +113,9 @@
     virtual std::string GetDescription() = 0;
 
   protected:
-    MeasureTool(MessageBroker& broker, boost::weak_ptr<ViewportController> controllerW);
+    MeasureTool(boost::shared_ptr<IViewport> viewport);
+
+    void PostConstructor();
 
     /**
     The measuring tool may exist in a standalone fashion, without any available
@@ -127,17 +131,20 @@
     */
     virtual void RefreshScene() = 0;
 
-    boost::shared_ptr<const ViewportController> GetController() const;
-    boost::shared_ptr<ViewportController>      GetController();
-
     /**
     enabled_ is not accessible by subclasses because there is a state machine
     that we do not wanna mess with
     */
     bool IsEnabled() const;
 
+    /**
+    Protected to allow sub-classes to use this weak pointer in factory methods
+    (pass them to created objects)
+    */
+    boost::shared_ptr<IViewport> viewport_;
+
+
   private:
-    boost::weak_ptr<ViewportController> controllerW_;
     bool     enabled_;
   };
 
--- a/Framework/Scene2DViewport/MeasureToolsToolbox.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/MeasureToolsToolbox.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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;
+  }
 
 }
--- a/Framework/Scene2DViewport/MeasureTrackers.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/MeasureTrackers.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -24,8 +24,8 @@
 namespace OrthancStone
 {
 
-  CreateMeasureTracker::CreateMeasureTracker(boost::weak_ptr<ViewportController> controllerW)
-    : controllerW_(controllerW)
+  CreateMeasureTracker::CreateMeasureTracker(boost::shared_ptr<IViewport> viewport)
+    : viewport_(viewport)
     , alive_(true)
     , commitResult_(true)
   {
@@ -47,29 +47,28 @@
     // if the tracker completes successfully, we add the command
     // to the undo stack
     // otherwise, we simply undo it
+
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    ViewportController& controller = lock->GetController();
+
     if (commitResult_)
-      controllerW_.lock()->PushCommand(command_);
+      lock->GetController().PushCommand(command_);
     else
       command_->Undo();
+
+    lock->Invalidate();
   }
 
-  Scene2D& CreateMeasureTracker::GetScene()
-  {
-    return controllerW_.lock()->GetScene();
-  }
-
-  EditMeasureTracker::EditMeasureTracker(boost::weak_ptr<ViewportController> controllerW, const PointerEvent& e)
-    : controllerW_(controllerW)
+  EditMeasureTracker::EditMeasureTracker(boost::shared_ptr<IViewport> viewport, const PointerEvent& e)
+    : viewport_(viewport)
     , alive_(true)
     , commitResult_(true)
   {
-    boost::shared_ptr<ViewportController> controller = controllerW.lock();
-    originalClickPosition_ = e.GetMainPosition().Apply(controller->GetScene().GetCanvasToSceneTransform());
-  }
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    ViewportController& controller = lock->GetController();
 
-  Scene2D& EditMeasureTracker::GetScene()
-  {
-    return controllerW_.lock()->GetScene();
+    originalClickPosition_ = e.GetMainPosition().Apply(
+      controller.GetScene().GetCanvasToSceneTransform());
   }
 
   void EditMeasureTracker::Cancel()
@@ -88,10 +87,16 @@
     // if the tracker completes successfully, we add the command
     // to the undo stack
     // otherwise, we simply undo it
+
+    std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+    ViewportController& controller = lock->GetController();
+
     if (commitResult_)
-      controllerW_.lock()->PushCommand(command_);
+      lock->GetController().PushCommand(command_);
     else
       command_->Undo();
+
+    lock->Invalidate();
   }
 }
 
--- a/Framework/Scene2DViewport/MeasureTrackers.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/MeasureTrackers.h	Tue Mar 31 15:51:02 2020 +0200
@@ -40,15 +40,14 @@
     virtual void Cancel() ORTHANC_OVERRIDE;
     virtual bool IsAlive() const ORTHANC_OVERRIDE;
   protected:
-    CreateMeasureTracker(boost::weak_ptr<ViewportController> controllerW);
+    CreateMeasureTracker(boost::shared_ptr<IViewport> viewport);
 
     ~CreateMeasureTracker();
   
   protected:
     boost::shared_ptr<CreateMeasureCommand>         command_;
-    boost::weak_ptr<ViewportController>          controllerW_;
+    boost::shared_ptr<IViewport>          viewport_;
     bool                            alive_;
-    Scene2D&                      GetScene();
 
   private:
     bool                            commitResult_;
@@ -60,15 +59,14 @@
     virtual void Cancel() ORTHANC_OVERRIDE;
     virtual bool IsAlive() const ORTHANC_OVERRIDE;
   protected:
-    EditMeasureTracker(boost::weak_ptr<ViewportController> controllerW, const PointerEvent& e);
+    EditMeasureTracker(boost::shared_ptr<IViewport> viewport, const PointerEvent& e);
 
     ~EditMeasureTracker();
 
   protected:
     boost::shared_ptr<EditMeasureCommand> command_;
-    boost::weak_ptr<ViewportController>   controllerW_;
+    boost::shared_ptr<IViewport>   viewport_;
     bool                                  alive_;
-    Scene2D&            GetScene();
     
     ScenePoint2D                          GetOriginalClickPosition() const
     {
--- a/Framework/Scene2DViewport/OneGesturePointerTracker.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/OneGesturePointerTracker.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -28,8 +28,8 @@
 namespace OrthancStone
 {
   OneGesturePointerTracker::OneGesturePointerTracker(
-    boost::weak_ptr<ViewportController> controllerW)
-    : controllerW_(controllerW)
+    boost::shared_ptr<IViewport> viewport)
+    : viewport_(viewport)
     , alive_(true)
     , currentTouchCount_(1)
   {
@@ -55,15 +55,19 @@
     // 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
+     * caught (at least in WebAssembly), so we delete the tracker on
+     * the next click inside the canvas.
+     **/
+    alive_ = false;
   }
 
   bool OneGesturePointerTracker::IsAlive() const
   {
     return alive_;
   }
-
-  boost::shared_ptr<ViewportController> OneGesturePointerTracker::GetController()
-  {
-    return controllerW_.lock();
-  }
 }
--- a/Framework/Scene2DViewport/OneGesturePointerTracker.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/OneGesturePointerTracker.h	Tue Mar 31 15:51:02 2020 +0200
@@ -22,6 +22,11 @@
 
 #include "IFlexiblePointerTracker.h"
 
+#include "../Viewport/IViewport.h"
+
+#include <boost/shared_ptr.hpp>
+#include <boost/weak_ptr.hpp>
+
 namespace OrthancStone
 {
   /**
@@ -39,16 +44,15 @@
   class OneGesturePointerTracker : public IFlexiblePointerTracker
   {
   public:
-    OneGesturePointerTracker(boost::weak_ptr<ViewportController> controllerW);
+    OneGesturePointerTracker(boost::shared_ptr<IViewport> viewport);
     virtual void PointerUp(const PointerEvent& event) ORTHANC_OVERRIDE;
     virtual void PointerDown(const PointerEvent& event) ORTHANC_OVERRIDE;
     virtual bool IsAlive() const ORTHANC_OVERRIDE;
   
   protected:
-    boost::shared_ptr<ViewportController>  GetController();
+    boost::shared_ptr<IViewport> viewport_;
 
   private:
-    boost::weak_ptr<ViewportController> controllerW_;
     bool                   alive_;
     int                    currentTouchCount_;
   };
--- a/Framework/Scene2DViewport/PredeclaredTypes.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/PredeclaredTypes.h	Tue Mar 31 15:51:02 2020 +0200
@@ -38,4 +38,5 @@
   class MeasureCommand;
   class ViewportController;
   class LayerHolder;
+  class IViewport;
 }
--- a/Framework/Scene2DViewport/ViewportController.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/ViewportController.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -24,44 +24,69 @@
 #include "MeasureCommands.h"
 
 #include "../StoneException.h"
+#include "../Scene2D/PanSceneTracker.h"
+#include "../Scene2D/RotateSceneTracker.h"
+#include "../Scene2D/ZoomSceneTracker.h"
 
 #include <boost/make_shared.hpp>
 
 namespace OrthancStone
 {
-  ViewportController::ViewportController(boost::weak_ptr<UndoStack> undoStackW,
-                                         MessageBroker& broker,
-                                         IViewport& viewport)
-    : IObservable(broker)
-    , undoStackW_(undoStackW)
-    , canvasToSceneFactor_(0.0)
-    , viewport_(viewport)
+  IFlexiblePointerTracker* DefaultViewportInteractor::CreateTracker(
+    boost::shared_ptr<IViewport>          viewport,
+    const PointerEvent& event,
+    unsigned int        viewportWidth,
+    unsigned int        viewportHeight)
   {
-  }
- 
-  ViewportController::~ViewportController()
-  {
+    switch (event.GetMouseButton())
+    {
+      case MouseButton_Left:
+        return new RotateSceneTracker(viewport, event);
 
+      case MouseButton_Middle:
+        return new PanSceneTracker(viewport, event);
+      
+      case MouseButton_Right:
+      {
+        if (viewportWidth != 0)
+        {
+          return new ZoomSceneTracker(viewport, event, viewportWidth);
+        }
+        else
+        {
+          return NULL;
+        }
+      }
+
+      default:
+        return NULL;
+    }
   }
 
-  boost::shared_ptr<UndoStack> ViewportController::GetUndoStack()
+  ViewportController::ViewportController(boost::shared_ptr<IViewport> viewport)
+    : viewport_(viewport)
+    , scene_(new Scene2D)
+    , canvasToSceneFactor_(1)
   {
-    return undoStackW_.lock();
+    // undoStack_ is not default-initialized, which basically means empty.
+    // The controller must be able to cope with this.
   }
 
-  boost::shared_ptr<const UndoStack> ViewportController::GetUndoStack() const
+  ViewportController::~ViewportController()
   {
-    return undoStackW_.lock();
   }
 
-  void ViewportController::PushCommand(boost::shared_ptr<MeasureCommand> command)
+  void ViewportController::PushCommand(
+    boost::shared_ptr<MeasureCommand> command)
   {
     boost::shared_ptr<UndoStack> 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!";
+      LOG(ERROR) << "Internal error: no undo stack!";
     }
   }
 
@@ -69,10 +94,12 @@
   {
     boost::shared_ptr<UndoStack> undoStack = undoStackW_.lock();
     if (undoStack.get() != NULL)
+    {
       undoStack->Undo();
+    }
     else
     {
-      LOG(ERROR) << "Internal error: no undo stack in the viewport controller!";
+      LOG(ERROR) << "Internal error: no undo stack!";
     }
   }
 
@@ -80,10 +107,12 @@
   {
     boost::shared_ptr<UndoStack> undoStack = undoStackW_.lock();
     if (undoStack.get() != NULL)
+    {
       undoStack->Redo();
+    }
     else
     {
-      LOG(ERROR) << "Internal error: no undo stack in the viewport controller!";
+      LOG(ERROR) << "Internal error: no undo stack!";
     }
   }
 
@@ -91,10 +120,12 @@
   {
     boost::shared_ptr<UndoStack> undoStack = undoStackW_.lock();
     if (undoStack.get() != NULL)
+    {
       return undoStack->CanUndo();
+    }
     else
     {
-      LOG(ERROR) << "Internal error: no undo stack in the viewport controller!";
+      LOG(ERROR) << "Internal error: no undo stack!";
       return false;
     }
   }
@@ -103,21 +134,18 @@
   {
     boost::shared_ptr<UndoStack> undoStack = undoStackW_.lock();
     if (undoStack.get() != NULL)
+    {
       return undoStack->CanRedo();
+    }
     else
     {
-      LOG(ERROR) << "Internal error: no undo stack in the viewport controller!";
+      LOG(ERROR) << "Internal error: no undo stack!";
       return false;
     }
   }
   
-  bool ViewportController::HandlePointerEvent(PointerEvent e)
-  {
-    throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-  }
-
-  std::vector<boost::shared_ptr<MeasureTool> > ViewportController::HitTestMeasureTools(
-    ScenePoint2D p)
+  std::vector<boost::shared_ptr<MeasureTool> > 
+    ViewportController::HitTestMeasureTools(ScenePoint2D p)
   {
     std::vector<boost::shared_ptr<MeasureTool> > ret;
     
@@ -128,8 +156,7 @@
     }
     return ret;
   }
-
-
+  
   void ViewportController::ResetMeasuringToolsHighlight()
   {
     for (size_t i = 0; i < measureTools_.size(); ++i)
@@ -138,68 +165,59 @@
     }
   }
 
-  const OrthancStone::AffineTransform2D& ViewportController::GetCanvasToSceneTransform() const
+  OrthancStone::AffineTransform2D 
+    ViewportController::GetCanvasToSceneTransform() const
   {
-    return GetScene().GetCanvasToSceneTransform();
+    return scene_->GetCanvasToSceneTransform();
   }
 
-  const OrthancStone::AffineTransform2D& ViewportController::GetSceneToCanvasTransform() const
+  OrthancStone::AffineTransform2D 
+    ViewportController::GetSceneToCanvasTransform() const
   {
-    return GetScene().GetSceneToCanvasTransform();
+    return scene_->GetSceneToCanvasTransform();
   }
 
   void ViewportController::SetSceneToCanvasTransform(
     const AffineTransform2D& transform)
   {
-    viewport_.GetScene().SetSceneToCanvasTransform(transform);
+    scene_->SetSceneToCanvasTransform(transform);
+
+    canvasToSceneFactor_ = scene_->GetCanvasToSceneTransform().ComputeZoom();
     BroadcastMessage(SceneTransformChanged(*this));
-    
-    // update the canvas to scene factor
-    canvasToSceneFactor_ = 0.0;
-    canvasToSceneFactor_ = GetCanvasToSceneFactor();
   }
 
-  void ViewportController::FitContent(
-    unsigned int canvasWidth, unsigned int canvasHeight)
+  void ViewportController::FitContent(unsigned int viewportWidth,
+                                      unsigned int viewportHeight)
   {
-    viewport_.GetScene().FitContent(canvasWidth, canvasHeight);
+    scene_->FitContent(viewportWidth, viewportHeight);
+    canvasToSceneFactor_ = scene_->GetCanvasToSceneTransform().ComputeZoom();
     BroadcastMessage(SceneTransformChanged(*this));
   }
 
-  void ViewportController::FitContent()
+  void ViewportController::AddMeasureTool(
+    boost::shared_ptr<MeasureTool> measureTool)
   {
-    if (viewport_.HasCompositor())
-    {
-      const ICompositor& compositor = viewport_.GetCompositor();
-      viewport_.GetScene().FitContent(compositor.GetCanvasWidth(), compositor.GetCanvasHeight());
-      BroadcastMessage(SceneTransformChanged(*this));
-    }
-  }
-
-  void ViewportController::AddMeasureTool(boost::shared_ptr<MeasureTool> measureTool)
-  {
-    ORTHANC_ASSERT(std::find(measureTools_.begin(), measureTools_.end(), measureTool)
-      == measureTools_.end(), "Duplicate measure tool");
+    ORTHANC_ASSERT(std::find(measureTools_.begin(), 
+                             measureTools_.end(), 
+                             measureTool) == measureTools_.end(), 
+                  "Duplicate measure tool");
     measureTools_.push_back(measureTool);
   }
 
-  void ViewportController::RemoveMeasureTool(boost::shared_ptr<MeasureTool> measureTool)
+  void ViewportController::RemoveMeasureTool(
+    boost::shared_ptr<MeasureTool> measureTool)
   {
-    ORTHANC_ASSERT(std::find(measureTools_.begin(), measureTools_.end(), measureTool)
-      != measureTools_.end(), "Measure tool not found");
+    ORTHANC_ASSERT(std::find(measureTools_.begin(), 
+                             measureTools_.end(), 
+                             measureTool) != measureTools_.end(), 
+                  "Measure tool not found");
     measureTools_.erase(
       std::remove(measureTools_.begin(), measureTools_.end(), measureTool), 
       measureTools_.end());
   }
 
-
   double ViewportController::GetCanvasToSceneFactor() const
   {
-    if (canvasToSceneFactor_ == 0)
-    {
-      canvasToSceneFactor_ =
-        GetScene().GetCanvasToSceneTransform().ComputeZoom();
-    }
     return canvasToSceneFactor_;
   }
 
@@ -222,4 +240,68 @@
   {
     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(viewport_, 
+                                                    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();
+      }
+    }
+  }
 }
--- a/Framework/Scene2DViewport/ViewportController.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Scene2DViewport/ViewportController.h	Tue Mar 31 15:51:02 2020 +0200
@@ -22,14 +22,43 @@
 
 #include "PredeclaredTypes.h"
 
-#include "../Viewport/IViewport.h"
-#include "../Scene2D/PointerEvent.h"
+#include "../Messages/IObservable.h"
+#include "../Scene2D/Scene2D.h"
 #include "../Scene2DViewport/IFlexiblePointerTracker.h"
 
+#include <Core/Compatibility.h>
+
+#include <boost/enable_shared_from_this.hpp>
 #include <stack>
 
 namespace OrthancStone
 {
+  // TODO - Move this to another file
+  class IViewportInteractor : public boost::noncopyable
+  {
+  public:
+    virtual ~IViewportInteractor()
+    {
+    }
+
+    virtual IFlexiblePointerTracker* CreateTracker(boost::shared_ptr<IViewport> viewport,
+                                                   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<IViewport> viewport,
+                                                   const PointerEvent& event,
+                                                   unsigned int viewportWidth,
+                                                   unsigned int viewportHeight) ORTHANC_OVERRIDE;
+  };
+
+
   class UndoStack;
 
   const double ARC_RADIUS_CANVAS_COORD = 30.0;
@@ -74,31 +103,26 @@
   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<ViewportController>
   {
   public:
     ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, \
-      SceneTransformChanged, ViewportController);
+                                        SceneTransformChanged, \
+                                        ViewportController);
 
-    ViewportController(boost::weak_ptr<UndoStack> undoStackW,
-                       MessageBroker& broker,
-                       IViewport& viewport);
-
+    ViewportController(boost::shared_ptr<IViewport> viewport);
 
     ~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 
     measure tool, if needed
     */
-    std::vector<boost::shared_ptr<MeasureTool> > HitTestMeasureTools(ScenePoint2D p);
+    std::vector<boost::shared_ptr<MeasureTool> > HitTestMeasureTools(
+      ScenePoint2D p);
 
     /**
     This function will traverse the measuring tools and will clear their 
@@ -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<IFlexiblePointerTracker> 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> measureTool);
@@ -173,31 +197,74 @@
     /** 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<IViewport&>(viewport_).GetScene();
+      return *scene_;
+    }
+
+    Scene2D& GetScene()
+    {
+      return *scene_;
+    }
+
+    /**
+    This method is used in a move pattern: when the ownership of the scene 
+    managed by this viewport controller must be transferred to another 
+    controller.
+    */
+    Scene2D* ReleaseScene()
+    {
+      return scene_.release();
+    }
+
+    /**
+    This method is used when one wishes to replace the scene that is currently
+    managed by the controller. The previous scene is deleted and the controller
+    now has ownership of the new one.
+    */
+    void AcquireScene(Scene2D* scene)
+    {
+      scene_.reset(scene);
+    }
+
+    /**
+    Sets the undo stack that is used by PushCommand, Undo...
+    */
+    void SetUndoStack(boost::weak_ptr<UndoStack> undoStackW)
+    {
+      undoStackW_ = undoStackW;
+    }
+    
+    bool HasActiveTracker() const
+    {
+      return activeTracker_.get() != NULL;
     }
 
   private:
     double GetCanvasToSceneFactor() const;
 
-    boost::weak_ptr<UndoStack>                   undoStackW_;
-
-    boost::shared_ptr<UndoStack>                 GetUndoStack();
-    boost::shared_ptr<const UndoStack>           GetUndoStack() const;
+    boost::shared_ptr<IViewport>                  viewport_;
+    boost::weak_ptr<UndoStack>                    undoStackW_;  // Global stack, possibly shared by all viewports
+    std::vector<boost::shared_ptr<MeasureTool> >  measureTools_;
+    boost::shared_ptr<IFlexiblePointerTracker>    activeTracker_;  // TODO - Couldn't this be a "std::unique_ptr"?
 
-    std::vector<boost::shared_ptr<MeasureTool> > measureTools_;
-    boost::shared_ptr<IFlexiblePointerTracker>   tracker_;
-    
+    std::unique_ptr<Scene2D>   scene_;
+
     // this is cached
-    mutable double              canvasToSceneFactor_;
-    
-    // Refactoring on 2019-07-10: Removing shared_ptr from scene
-    IViewport&      viewport_;
+    double  canvasToSceneFactor_;    
   };
 }
--- a/Framework/StoneEnumerations.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/StoneEnumerations.h	Tue Mar 31 15:51:02 2020 +0200
@@ -24,21 +24,6 @@
 #include <string>
 
 
-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,
--- a/Framework/StoneInitialization.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/StoneInitialization.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -21,20 +21,58 @@
 
 #include "StoneInitialization.h"
 
-#include <Core/OrthancException.h>
-
 #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 <QCoreApplication>
+#endif
+
 #if ORTHANC_ENABLE_CURL == 1
-#include <Core/HttpClient.h>
+#  include <Core/HttpClient.h>
+#endif
+
+#if ORTHANC_ENABLE_DCMTK == 1
+#  include <Core/DicomParsing/FromDcmtkBridge.h>
 #endif
 
+#if ORTHANC_ENABLE_WASM == 1
+static double viewportsTimeout_ = 1000;
+static std::unique_ptr<OrthancStone::WebGLViewportsRegistry>  viewportsRegistry_;
+#endif
+
+#include "Toolbox/LinearAlgebra.h"
+
+#include <Core/OrthancException.h>
+#include <Core/Toolbox.h>
+
+#include <locale>
+
+
 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
 }
--- a/Framework/StoneInitialization.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/StoneInitialization.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <Core/Logging.h>
 
 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
 }
--- a/Framework/Toolbox/CoordinateSystem3D.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Toolbox/CoordinateSystem3D.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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;
+  }
 }
--- a/Framework/Toolbox/CoordinateSystem3D.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Toolbox/CoordinateSystem3D.h	Tue Mar 31 15:51:02 2020 +0200
@@ -22,6 +22,7 @@
 #pragma once
 
 #include "LinearAlgebra.h"
+#include "../Scene2D/ScenePoint2D.h"
 
 #include <Plugins/Samples/Common/IDicomDataset.h>
 
@@ -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);
   };
 }
--- a/Framework/Toolbox/DicomInstanceParameters.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Toolbox/DicomInstanceParameters.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -265,7 +265,7 @@
   }
 
   void DicomInstanceParameters::Data::ApplyRescaleAndDoseScaling(Orthanc::ImageAccessor& image,
-                                                   bool useDouble) const
+                                                                 bool useDouble) const
   {
     if (image.GetFormat() != Orthanc::PixelFormat_Float32)
     {
@@ -468,4 +468,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;
+    }
+  }
 }
--- a/Framework/Toolbox/DicomInstanceParameters.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Toolbox/DicomInstanceParameters.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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);
+    }
   };
 }
--- a/Framework/Toolbox/DicomStructureSet.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Toolbox/DicomStructureSet.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -27,12 +27,11 @@
 #include <Core/Logging.h>
 #include <Core/OrthancException.h>
 #include <Core/Toolbox.h>
-#include <Plugins/Samples/Common/FullOrthancDataset.h>
 #include <Plugins/Samples/Common/DicomDatasetReader.h>
 
 #if defined(_MSC_VER)
-#pragma warning(push)
-#pragma warning(disable:4244)
+#  pragma warning(push)
+#  pragma warning(disable:4244)
 #endif
 
 #include <limits>
@@ -43,9 +42,14 @@
 #include <boost/geometry/multi/geometries/multi_polygon.hpp>
 
 #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<double> BoostPoint;
 typedef boost::geometry::model::polygon<BoostPoint> BoostPolygon;
 typedef boost::geometry::model::multi_polygon<BoostPolygon>  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<int>(structures_[i].red_) << "," 
-                   << static_cast<int>(structures_[i].green_) << ","
-                   << static_cast<int>(structures_[i].blue_) << ")";
+                << "\" with interpretation \"" << structures_[i].interpretation_
+                << "\" containing " << countSlices << " slices (color: " 
+                << static_cast<int>(structures_[i].red_) << "," 
+                << static_cast<int>(structures_[i].green_) << ","
+                << static_cast<int>(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<Point2D> >& polygons,
-                                           const Structure& structure,
-                                           const CoordinateSystem3D& slice) const
+  bool DicomStructureSet::ProjectStructure(
+#if USE_BOOST_UNION_FOR_POLYGONS == 1
+    std::vector< std::vector<Point2D> >& polygons,
 #else
-  bool DicomStructureSet::ProjectStructure(std::vector< std::pair<Point2D, Point2D> >& segments,
+    std::vector< std::pair<Point2D, Point2D> >& 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<Point2D>());
           
           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<BoostPolygon> 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<std::pair<RtStructRectangleInSlab, double> > 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<float>(x1), 
-            static_cast<float>(y1), 
-            static_cast<float>(x2), 
-            static_cast<float>(y2)),curZ));
+                                               static_cast<float>(x1), 
+                                               static_cast<float>(y1), 
+                                               static_cast<float>(x2), 
+                                               static_cast<float>(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<Point2D> > polygons;
+    if (ProjectStructure(polygons, structureIndex, plane))
+    {
+      for (size_t j = 0; j < polygons.size(); j++)
+      {
+        std::vector<ScenePoint2D> 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<Point2D, Point2D> >  segments;
+
+    if (ProjectStructure(segments, structureIndex, plane))
+    {
+      for (size_t j = 0; j < segments.size(); j++)
+      {
+        std::vector<ScenePoint2D> 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
+  }
 }
--- a/Framework/Toolbox/DicomStructureSet.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Toolbox/DicomStructureSet.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <Core/DicomParsing/ParsedDicomFile.h>
+#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<Point2D> >& polygons,
-                          const Structure& structure,
-                          const CoordinateSystem3D& slice) const;
+    bool ProjectStructure(
+#if USE_BOOST_UNION_FOR_POLYGONS == 1
+      std::vector< std::vector<Point2D> >& polygons,
 #else
-    bool ProjectStructure(std::vector< std::pair<Point2D, Point2D> >& segments,
+      std::vector< std::pair<Point2D, Point2D> >& 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<Point2D> >& 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<Point2D, Point2D> >& 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));
+    }
   };
 }
--- a/Framework/Toolbox/GenericToolbox.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Toolbox/GenericToolbox.h	Tue Mar 31 15:51:02 2020 +0200
@@ -20,15 +20,27 @@
 
 #pragma once
 
+#include <Core/Compatibility.h>
+#include <Core/OrthancException.h>
+
+#include <boost/shared_ptr.hpp>
+
 #include <string>
 #include <stdint.h>
 #include <math.h>
 
+#include <memory>
 
 namespace OrthancStone
 {
   namespace GenericToolbox
   {
+    /**
+    Fast floating point string validation.
+    No trimming applied, so the input must match regex 
+    /^[-]?[0-9]*\.?[0-9]*([eE][-+]?[0-9]+)?$/
+    The following are allowed as edge cases: "" and "-"
+    */
     inline bool LegitDoubleString(const char* text)
     {
       const char* p = text;
@@ -70,6 +82,11 @@
       return true;
     }
 
+    /**
+    Fast integer string validation.
+    No trimming applied, so the input must match regex /^-?[0-9]*$/
+    The following are allowed as edge cases: "" and "-"
+    */
     inline bool LegitIntegerString(const char* text)
     {
       const char* p = text;
@@ -86,6 +103,9 @@
     }
 
     /*
+      Fast string --> double conversion.
+      Must pass the LegitDoubleString test
+
       String to doubles with at most 18 digits
     */
     inline bool StringToDouble(double& r, const char* text)
@@ -210,10 +230,17 @@
       return StringToDouble(r, text.c_str());
     }
 
+    /**
+    Fast string to integer conversion. Leading zeroes and minus are accepted,
+    but a leading + sign is NOT.
+    Must pass the LegitIntegerString function test.
+    In addition, an empty string (or lone minus sign) yields 0.
+    */
+
     template<typename T>
     inline bool StringToInteger(T& r, const char* text)
     {
-      if (!LegitDoubleString(text))
+      if (!LegitIntegerString(text))
         return false;
 
       r = 0;
@@ -244,22 +271,36 @@
     }
 
     /**
-    "rgb(12,23,255)"  --> red, green, blue and returns true
-    "everything else" --> returns false (other values left untouched)
+    if input is "rgb(12,23,255)"  --> function fills `red`, `green` and `blue` and returns true
+    else ("everything else")      --> function returns false and leaves all values untouched
     */
     bool GetRgbValuesFromString(uint8_t& red, uint8_t& green, uint8_t& blue, const char* text);
 
     /**
-    See other overload
+    See main overload
     */
     inline bool GetRgbValuesFromString(uint8_t& red, uint8_t& green, uint8_t& blue, const std::string& text)
     {
       return GetRgbValuesFromString(red, green, blue, text.c_str());
     }
 
-    bool GetRgbaValuesFromString(uint8_t& red, uint8_t& green, uint8_t& blue, uint8_t& alpha, const char* text);
+    /**
+    Same as GetRgbValuesFromString
+    */
+    bool GetRgbaValuesFromString(uint8_t& red,
+                                 uint8_t& green,
+                                 uint8_t& blue,
+                                 uint8_t& alpha,
+                                 const char* text);
 
-    inline bool GetRgbaValuesFromString(uint8_t& red, uint8_t& green, uint8_t& blue, uint8_t& alpha, const std::string& text)
+    /**
+    Same as GetRgbValuesFromString
+    */
+    inline bool GetRgbaValuesFromString(uint8_t& red, 
+                                        uint8_t& green, 
+                                        uint8_t& blue, 
+                                        uint8_t& alpha, 
+                                        const std::string& text)
     {
       return GetRgbaValuesFromString(red, green, blue, alpha, text.c_str());
     }
--- a/Framework/Toolbox/LinearAlgebra.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Toolbox/LinearAlgebra.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -22,6 +22,7 @@
 #include "LinearAlgebra.h"
 
 #include "../StoneException.h"
+#include "GenericToolbox.h"
 
 #include <Core/Logging.h>
 #include <Core/OrthancException.h>
@@ -32,6 +33,7 @@
 
 #include <stdio.h>
 #include <iostream>
+#include <cstdlib>
 
 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<double>()". 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<double>(items[i]);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Toolbox/ParsedDicomCache.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "ParsedDicomCache.h"
+
+namespace OrthancStone
+{
+  class ParsedDicomCache::Item : public Orthanc::ICacheable
+  {
+  private:
+    std::unique_ptr<Orthanc::ParsedDicomFile>  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<std::string>(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<Item&>(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();
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Toolbox/ParsedDicomCache.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <Core/Cache/MemoryObjectCache.h>
+#include <Core/DicomParsing/ParsedDicomFile.h>
+
+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;
+    };
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Toolbox/ParsedDicomDataset.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "ParsedDicomDataset.h"
+
+#include <dcmtk/dcmdata/dcfilefo.h>
+
+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;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Toolbox/ParsedDicomDataset.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <Core/DicomParsing/ParsedDicomFile.h>
+#include <Plugins/Samples/Common/IDicomDataset.h>
+
+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;
+  };
+}
--- a/Framework/Toolbox/SlicesSorter.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Toolbox/SlicesSorter.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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<double>(i) * reference.GetNormal();        
+      OrthancStone::Vector p = reference.GetOrigin() + spacing * static_cast<double>(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;
   }
 
 
--- a/Framework/Toolbox/SlicesSorter.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Toolbox/SlicesSorter.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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;
--- a/Framework/Viewport/IViewport.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Viewport/IViewport.h	Tue Mar 31 15:51:02 2020 +0200
@@ -21,34 +21,52 @@
 #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;
+
+      /**
+      Do not store the result! Only access the compositor interface through
+      the lock.
+      */
+      virtual ICompositor& GetCompositor() = 0;
+
+      /**
+      Do not store the result! Only access the compositor interface through
+      the lock.
+      */
+      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;
   };
 }
-
--- a/Framework/Viewport/SdlViewport.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Viewport/SdlViewport.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -26,40 +26,116 @@
 
 namespace OrthancStone
 {
-  SdlOpenGLViewport::SdlOpenGLViewport(const char* title,
+  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()
+  {
+    refreshEvent_ = SDL_RegisterEvents(1);
+
+    if (refreshEvent_ == static_cast<uint32_t>(-1))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+  }
+  
+  void SdlViewport::PostConstructor()
+  {
+    controller_ = boost::make_shared<ViewportController>(shared_from_this());
+  }
+    
+  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 std::string& title,
                                        unsigned int width,
                                        unsigned int height,
                                        bool allowDpiScaling) :
-    context_(title, width, height, allowDpiScaling)
+    context_(title.c_str(), width, height, allowDpiScaling)
+  {
+    AcquireCompositor(new OpenGLCompositor(context_));  // (*)
+  }
+
+  boost::shared_ptr<SdlOpenGLViewport> SdlOpenGLViewport::Create(
+    const std::string& title,
+    unsigned int width,
+    unsigned int height,
+    bool allowDpiScaling)
   {
-    compositor_.reset(new OpenGLCompositor(context_, GetScene()));
+    boost::shared_ptr<SdlOpenGLViewport> that =
+      boost::shared_ptr<SdlOpenGLViewport>(new SdlOpenGLViewport(title, width, height, allowDpiScaling));
+    that->SdlViewport::PostConstructor();
+    return that;
+  }
+
+  SdlOpenGLViewport::~SdlOpenGLViewport()
+  {
+    // Make sure that the "OpenGLCompositor" is destroyed BEFORE the
+    // "OpenGLContext" it references (*)
+    ClearCompositor();
   }
 
-  SdlOpenGLViewport::SdlOpenGLViewport(const char* title,
-                                       unsigned int width,
-                                       unsigned int height,
-                                       boost::shared_ptr<Scene2D>& scene,
-                                       bool allowDpiScaling) :
-    SdlViewport(scene),
-    context_(title, width, height, allowDpiScaling)
+
+  void SdlOpenGLViewport::Paint()
   {
-    compositor_.reset(new OpenGLCompositor(context_, GetScene()));
+    SdlLock lock(*this);
+    lock.GetCompositor().Refresh(lock.GetController().GetScene());
   }
 
-  void SdlOpenGLViewport::Refresh()
+
+  void SdlOpenGLViewport::UpdateSize(unsigned int width, unsigned int height)
   {
-    compositor_->Refresh();
+    // nothing to do in OpenGL, the OpenGLCompositor::UpdateSize will be called automatically
+    SdlLock lock(*this);
+    lock.Invalidate();
   }
 
 
+  void SdlOpenGLViewport::ToggleMaximize()
+  {
+    // 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 +146,66 @@
     }
   }
   
-  void SdlCairoViewport::Refresh()
+  void SdlCairoViewport::Paint()
   {
-    compositor_.Refresh();
-    window_.Render(sdlSurface_);
+    SdlLock lock(*this);
+
+    lock.GetCompositor().Refresh(lock.GetController().GetScene());
+    CreateSdlSurfaceFromCompositor(dynamic_cast<CairoCompositor&>(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<CairoCompositor&>(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<int>(width) &&
+          sdlSurface_->h == static_cast<int>(height) &&
+          sdlSurface_->pitch == static_cast<int>(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";
--- a/Framework/Viewport/SdlViewport.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Viewport/SdlViewport.h	Tue Mar 31 15:51:02 2020 +0200
@@ -39,26 +39,93 @@
 #include "../OpenGL/SdlOpenGLContext.h"
 #include "../Scene2D/OpenGLCompositor.h"
 #include "../Scene2D/CairoCompositor.h"
-#include "ViewportBase.h"
+#include "IViewport.h"
+
+#include <SDL_events.h>
+
+// TODO: required for UndoStack injection
+// I don't like it either :)
+#include <boost/weak_ptr.hpp>
+
+#include <boost/thread/recursive_mutex.hpp>
 
 namespace OrthancStone
 {
-  class SdlViewport : public ViewportBase
+  class UndoStack;
+
+  class SdlViewport : public IViewport,
+                      public boost::enable_shared_from_this<SdlViewport>
   {
-  public:
-    SdlViewport()
+  private:
+    boost::recursive_mutex                 mutex_;
+    uint32_t                               refreshEvent_;
+    boost::shared_ptr<ViewportController>  controller_;
+    std::unique_ptr<ICompositor>           compositor_;
+
+    void SendRefreshEvent();
+
+  protected:
+    class SdlLock : public ILock
     {
+    private:
+      SdlViewport&                        that_;
+      boost::recursive_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<Scene2D>& scene) : 
-      ViewportBase(scene)
+    void AcquireCompositor(ICompositor* compositor /* takes ownership */);
+
+  protected:
+    SdlViewport();
+    void PostConstructor();
+
+  public:
+
+    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,87 +133,57 @@
   {
   private:
     SdlOpenGLContext  context_;
-    std::unique_ptr<OpenGLCompositor>   compositor_;
-
-  public:
-    SdlOpenGLViewport(const char* title,
-                      unsigned int width,
-                      unsigned int height,
-                      bool allowDpiScaling = true);
-
-    SdlOpenGLViewport(const char* title,
-                      unsigned int width,
-                      unsigned int height,
-                      boost::shared_ptr<Scene2D>& scene,
-                      bool allowDpiScaling = true);
 
-    virtual SdlWindow& GetWindow() ORTHANC_OVERRIDE
-    {
-      return context_.GetWindow();
-    }
-
-    virtual void Refresh() ORTHANC_OVERRIDE;
+  private:
+    SdlOpenGLViewport(const std::string& title,
+                      unsigned int       width,
+                      unsigned int       height,
+                      bool               allowDpiScaling = true);
+  public:
+    static boost::shared_ptr<SdlOpenGLViewport> Create(const std::string&,
+                                                       unsigned int width,
+                                                       unsigned int height,
+                                                       bool allowDpiScaling = true);
 
-    virtual void UpdateSize(unsigned int width, unsigned int height) ORTHANC_OVERRIDE
-    {
-      // nothing to do in OpenGL, the OpenGLCompositor::UpdateSize will be called automatically
-    }
+
+    virtual ~SdlOpenGLViewport();
+
+    virtual void Paint() ORTHANC_OVERRIDE;
 
-    virtual bool HasCompositor() const ORTHANC_OVERRIDE
-    {
-      return true;
-    }
+    virtual void UpdateSize(unsigned int width, 
+                            unsigned int height) ORTHANC_OVERRIDE;
 
-    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_;
+
+    void CreateSdlSurfaceFromCompositor(CairoCompositor& compositor);
 
   private:
-    void UpdateSdlSurfaceSize(unsigned int width,
-                              unsigned int height);
-
+    SdlCairoViewport(const char* title,
+                     unsigned int width,
+                     unsigned int height,
+                     bool allowDpiScaling = true);
   public:
-    SdlCairoViewport(const char* title,
+    static boost::shared_ptr<SdlCairoViewport> Create(const char* title,
                      unsigned int width,
                      unsigned int height,
                      bool allowDpiScaling = true);
 
-    SdlCairoViewport(const char* title,
-                     unsigned int width,
-                     unsigned int height,
-                     boost::shared_ptr<Scene2D>& scene,
-                     bool allowDpiScaling = true);
 
-    ~SdlCairoViewport();
+    virtual ~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;
   };
 }
--- a/Framework/Viewport/SdlWindow.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Viewport/SdlWindow.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -31,6 +31,8 @@
 #endif 
 // WIN32
 
+#include <SDL_render.h>
+#include <SDL_video.h>
 #include <SDL.h>
 
 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)
--- a/Framework/Viewport/SdlWindow.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Viewport/SdlWindow.h	Tue Mar 31 15:51:02 2020 +0200
@@ -23,18 +23,22 @@
 
 #if ORTHANC_ENABLE_SDL == 1
 
-#include <SDL_render.h>
-#include <SDL_video.h>
 #include <boost/noncopyable.hpp>
 
+// 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();
 
--- a/Framework/Viewport/ViewportBase.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ /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 <http://www.gnu.org/licenses/>.
- **/
-
-#include "ViewportBase.h"
-
-#include <Core/OrthancException.h>
-
-#include <boost/make_shared.hpp>
-
-namespace OrthancStone
-{
-  ViewportBase::ViewportBase() :
-    scene_(boost::make_shared<Scene2D>())
-  {
-  }
-
-  
-  ViewportBase::ViewportBase(boost::shared_ptr<Scene2D>& 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<double>(x) + 0.5 - static_cast<double>(compositor.GetCanvasWidth()) / 2.0,
-        static_cast<double>(y) + 0.5 - static_cast<double>(compositor.GetCanvasHeight()) / 2.0);
-    }
-    else
-    {
-      return ScenePoint2D(0, 0);
-    }
-  }
-}
--- a/Framework/Viewport/ViewportBase.h	Tue Mar 31 15:47:29 2020 +0200
+++ /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 <http://www.gnu.org/licenses/>.
- **/
-
-#pragma once
-
-#include "IViewport.h"
-
-#include <boost/shared_ptr.hpp>
-
-namespace OrthancStone
-{
-  class ViewportBase : public IViewport
-  {
-  private:
-    boost::shared_ptr<Scene2D>  scene_;
-
-  public:
-    ViewportBase();
-
-    ViewportBase(boost::shared_ptr<Scene2D>& 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<IViewport*>(static_cast<const IViewport*>(this));
-      return mutableThis->GetCompositor();
-    }
-
-  };
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Viewport/WebAssemblyCairoViewport.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -0,0 +1,137 @@
+/**
+ * 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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "WebAssemblyCairoViewport.h"
+
+#include "../Scene2D/CairoCompositor.h"
+
+#include <Core/Images/Image.h>
+
+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<unsigned int>(boost::math::iround(w));
+      height = static_cast<unsigned int>(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<CairoCompositor&>(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<uint8_t*>(javascript_->GetBuffer());
+    for (unsigned int y = 0; y < height; y++)
+    {
+      const uint8_t* p = reinterpret_cast<const uint8_t*>(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<CairoCompositor&>(compositor).UpdateSize(width, height);
+  }
+
+
+  WebAssemblyCairoViewport::WebAssemblyCairoViewport(
+    const std::string& canvasId) :
+    WebAssemblyViewport(canvasId)
+  {
+    unsigned int width, height;
+    GetCanvasSize(width, height);
+    emscripten_set_canvas_element_size(GetFullCanvasId().c_str(), 
+                                       width, 
+                                       height);
+
+    AcquireCompositor(new CairoCompositor(width, height));
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Viewport/WebAssemblyCairoViewport.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "WebAssemblyViewport.h"
+
+namespace OrthancStone
+{
+  class WebAssemblyCairoViewport : public WebAssemblyViewport
+  {
+  private:
+    std::unique_ptr<Orthanc::ImageAccessor>  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();
+    }
+  };
+}
--- a/Framework/Viewport/WebAssemblyViewport.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Viewport/WebAssemblyViewport.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -21,311 +21,304 @@
 
 #include "WebAssemblyViewport.h"
 
-#include "../StoneException.h"
+#include "../Toolbox/GenericToolbox.h"
 
-#include <emscripten/html5.h>
+#include <Core/OrthancException.h>
+
+#include <boost/make_shared.hpp>
+#include <boost/enable_shared_from_this.hpp>
 
 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<int>(source.targetX);
+    int y = static_cast<int>(source.targetY);
 
-  WebAssemblyOpenGLViewport::WebAssemblyOpenGLViewport(const std::string& canvas,
-    boost::shared_ptr<Scene2D>& 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<WebAssemblyOpenGLViewport*>(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<WebAssemblyOpenGLViewport*>(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(TRACE) << "WebAssemblyOpenGLViewport::Refresh";
-
-      // 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)
+  {
+    LOG(TRACE) << __func__;
+    WebAssemblyViewport* that = reinterpret_cast<WebAssemblyViewport*>(userData);
+
+    if (that->compositor_.get() != NULL &&
+        that->controller_ /* should always be true */)
+    {
+      that->Paint(*that->compositor_, *that->controller_);
+    }
+      
+    LOG(TRACE) << "Exiting: " << __func__;
+    return true;
+  }
+
+  EM_BOOL WebAssemblyViewport::OnResize(int eventType, const EmscriptenUiEvent *uiEvent, void *userData)
+  {
+    LOG(TRACE) << __func__;
+    WebAssemblyViewport* that = reinterpret_cast<WebAssemblyViewport*>(userData);
+
+    if (that->compositor_.get() != NULL)
     {
-      if (e.GetErrorCode() == ErrorCode_WebGLContextLost)
+      that->UpdateSize(*that->compositor_);
+      that->Invalidate();
+    }
+      
+    LOG(TRACE) << "Exiting: " << __func__;
+    return true;
+  }
+
+
+  EM_BOOL WebAssemblyViewport::OnMouseDown(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData)
+  {
+    WebAssemblyViewport* that = reinterpret_cast<WebAssemblyViewport*>(userData);
+
+    LOG(TRACE) << "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();
+    }
+
+    LOG(TRACE) << "Exiting: " << __func__;
+    return true;
+  }
+
+    
+  EM_BOOL WebAssemblyViewport::OnMouseMove(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData)
+  {
+    LOG(TRACE) << "WebAssemblyViewport::OnMouseMove CP1. userData = " << userData;
+    
+    WebAssemblyViewport* that = reinterpret_cast<WebAssemblyViewport*>(userData);
+    LOG(TRACE) << "WebAssemblyViewport::OnMouseMove CP2";
+
+    if (that->compositor_.get() != NULL &&
+        that->controller_->HasActiveTracker())
+    {
+      LOG(TRACE) << "WebAssemblyViewport::OnMouseMove CP3";
+      PointerEvent pointer;
+      ConvertMouseEvent(pointer, *mouseEvent, *that->compositor_);
+      LOG(TRACE) << "WebAssemblyViewport::OnMouseMove CP4";
+      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;
+        LOG(TRACE) << "WebAssemblyViewport::OnMouseMove CP5";
+        that->Invalidate();
+        LOG(TRACE) << "WebAssemblyViewport::OnMouseMove CP6";
       }
     }
-    catch (...)
-    {
-      // something else nasty happened
-      throw;
-    }
+
+    LOG(TRACE) << "Exiting: " << __func__;
+    return true;
   }
-
-  void WebAssemblyOpenGLViewport::RestoreCompositor()
+    
+  EM_BOOL WebAssemblyViewport::OnMouseUp(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData)
   {
-    // the context must have been restored!
-    ORTHANC_ASSERT(!context_.IsContextLost());
-    if (compositor_.get() == NULL)
+    LOG(TRACE) << __func__;
+    WebAssemblyViewport* that = reinterpret_cast<WebAssemblyViewport*>(userData);
+
+    if (that->compositor_.get() != NULL)
     {
-      compositor_.reset(new OpenGLCompositor(context_, GetScene()));
-    }
-    else
-    {
-      LOG(WARNING) << "RestoreCompositor() called for \"" << GetCanvasIdentifier() << "\" while it was NOT lost! Nothing done.";
+      PointerEvent pointer;
+      ConvertMouseEvent(pointer, *mouseEvent, *that->compositor_);
+      that->controller_->HandleMouseRelease(pointer);
+      that->Invalidate();
     }
-  }
 
-  bool WebAssemblyOpenGLViewport::OpenGLContextLost()
-  {
-    LOG(ERROR) << "WebAssemblyOpenGLViewport::OpenGLContextLost() for canvas: " << GetCanvasIdentifier();
-    DisableCompositor();
+    LOG(TRACE) << "Exiting: " << __func__;
     return true;
   }
 
-  bool WebAssemblyOpenGLViewport::OpenGLContextRestored()
+  void WebAssemblyViewport::Invalidate()
   {
-    LOG(ERROR) << "WebAssemblyOpenGLViewport::OpenGLContextRestored() for canvas: " << GetCanvasIdentifier();
-    
-    // maybe the context has already been restored by other means (the 
-    // Refresh() function)
-    if (!HasCompositor())
-    {
-      RestoreCompositor();
-      UpdateSize();
-    }
-    return false;
+    emscripten_request_animation_frame(OnRequestAnimationFrame, reinterpret_cast<void*>(this));
   }
 
-  void WebAssemblyOpenGLViewport::RegisterContextCallbacks()
+  void WebAssemblyViewport::AcquireCompositor(ICompositor* compositor /* takes ownership */)
   {
-#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<void*>(this);
-    EMSCRIPTEN_RESULT status = EMSCRIPTEN_RESULT_SUCCESS;
-
-    //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)
+    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, bool enableEmscriptenEvents) :
+    shortCanvasId_(canvasId),
+    fullCanvasId_(canvasId),
+    interactor_(new DefaultViewportInteractor),
+    enableEmscriptenEvents_(enableEmscriptenEvents)
   {
   }
 
-  WebAssemblyCairoViewport::WebAssemblyCairoViewport(const std::string& canvas,
-    boost::shared_ptr<Scene2D>& scene) :
-    WebAssemblyViewport(canvas, scene),
-    canvas_(canvas),
-    compositor_(GetScene(), 1024, 768)
+  void WebAssemblyViewport::PostConstructor()
   {
+    boost::shared_ptr<IViewport> viewport = shared_from_this();
+    controller_.reset(new ViewportController(viewport));
+
+    LOG(INFO) << "Initializing Stone viewport on HTML canvas: " 
+      << shortCanvasId_;
+
+    if (shortCanvasId_.empty() ||
+        shortCanvasId_[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();
+        }
+      },
+      shortCanvasId_.c_str()   // $0
+      );
+
+    if (enableEmscriptenEvents_)
+    {
+      // 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,
+                                     reinterpret_cast<void*>(this),
+                                     false,
+                                     OnResize);
+
+      emscripten_set_mousedown_callback(fullCanvasId_.c_str(),
+                                        reinterpret_cast<void*>(this),
+                                        false,
+                                        OnMouseDown);
+
+      emscripten_set_mousemove_callback(fullCanvasId_.c_str(),
+                                        reinterpret_cast<void*>(this),
+                                        false,
+                                        OnMouseMove);
+
+      emscripten_set_mouseup_callback(fullCanvasId_.c_str(),
+                                      reinterpret_cast<void*>(this),
+                                      false,
+                                      OnMouseUp);
+    }
   }
 
-  void WebAssemblyCairoViewport::UpdateSize()
+  WebAssemblyViewport::~WebAssemblyViewport()
   {
-    LOG(INFO) << "updating cairo viewport size";
-    double w, h;
-    emscripten_get_element_css_size(canvas_.c_str(), &w, &h);
+    if (enableEmscriptenEvents_)
+    {
+      emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW,
+                                     reinterpret_cast<void*>(this),
+                                     false,
+                                     NULL);
+
+      emscripten_set_mousedown_callback(fullCanvasId_.c_str(),
+                                        reinterpret_cast<void*>(this),
+                                        false,
+                                        OnMouseDown);
+
+      emscripten_set_mousemove_callback(fullCanvasId_.c_str(),
+                                        reinterpret_cast<void*>(this),
+                                        false,
+                                        OnMouseMove);
 
-    /**
-     * 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)
+      emscripten_set_mouseup_callback(fullCanvasId_.c_str(),
+                                      reinterpret_cast<void*>(this),
+                                      false,
+                                      OnMouseUp);
+    }
+  }
+  
+  IViewport::ILock* WebAssemblyViewport::Lock()
+  {
+    return new WasmLock(*this);
+  }
+  
+  void WebAssemblyViewport::AcquireInteractor(IViewportInteractor* interactor)
+  {
+    if (interactor == NULL)
     {
-      canvasWidth = static_cast<unsigned int>(boost::math::iround(w));
-      canvasHeight = static_cast<unsigned int>(boost::math::iround(h));
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
     }
-
-    emscripten_set_canvas_element_size(canvas_.c_str(), canvasWidth, canvasHeight);
-    compositor_.UpdateSize(canvasWidth, canvasHeight);
-  }
-
-  void WebAssemblyCairoViewport::Refresh()
-  {
-    LOG(INFO) << "refreshing cairo viewport, TODO: blit to the canvans.getContext('2d')";
-    GetCompositor().Refresh();
+    else
+    {
+      interactor_.reset(interactor);
+    }
   }
 }
--- a/Framework/Viewport/WebAssemblyViewport.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Viewport/WebAssemblyViewport.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
  **/
@@ -21,113 +21,98 @@
 
 #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 <Core/Compatibility.h>
+
+#include <emscripten.h>
+#include <emscripten/html5.h>
+
+#include <memory>
+#include <string>
 
 namespace OrthancStone
 {
-  class WebAssemblyViewport : public ViewportBase
+  class WebAssemblyViewport : public IViewport,
+                              public boost::enable_shared_from_this<WebAssemblyViewport>
+
   {
   private:
-    std::string  canvasIdentifier_;
+    class WasmLock;
+    
+    std::string                           shortCanvasId_;
+    std::string                           fullCanvasId_;
+    std::unique_ptr<ICompositor>          compositor_;
+    std::unique_ptr<ViewportController>   controller_;
+    std::unique_ptr<IViewportInteractor>  interactor_;
+    bool                                  enableEmscriptenEvents_;
 
-  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<Scene2D>& scene) 
-      : ViewportBase(scene)
-      , canvasIdentifier_(canvasIdentifier)
-    {
+      compositor_.reset();
     }
 
-    const std::string& GetCanvasIdentifier() const
+    bool HasCompositor() const
     {
-      return canvasIdentifier_;
+      return compositor_.get() != NULL;
     }
-  };
+
+    void AcquireCompositor(ICompositor* compositor /* takes ownership */);
 
+    virtual void Paint(ICompositor& compositor,
+                       ViewportController& controller) = 0;
 
-  class WebAssemblyOpenGLViewport : public WebAssemblyViewport
-  {
-  private:
-    OpenGL::WebAssemblyOpenGLContext  context_;
-    std::unique_ptr<OpenGLCompositor>   compositor_;
-    double                            cssWidth_;
-    double                            cssHeight_;
-    int                               pixelWidth_;
-    int                               pixelHeight_;
+    virtual void UpdateSize(ICompositor& compositor) = 0;
 
-  private:
-    void UpdateSizeIfNeeded();
+    /**
+    The second argument is temporary and should be deleted once the migration 
+    to interactors is finished.
+    */
+    WebAssemblyViewport(const std::string& canvasId, 
+                        bool enableEmscriptenEvents = true);
+
+    void PostConstructor();
 
   public:
-    WebAssemblyOpenGLViewport(const std::string& canvas);
-    
-    WebAssemblyOpenGLViewport(const std::string& canvas,
-                              boost::shared_ptr<Scene2D>& scene);
-    
-    // This function must be called each time the browser window is resized
-    void UpdateSize();
+    virtual ILock* Lock() ORTHANC_OVERRIDE;
+
+    ~WebAssemblyViewport();
+
 
-    virtual bool HasCompositor() const ORTHANC_OVERRIDE
+    /**
+    This method takes ownership
+    */
+    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<Scene2D>& 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_;
     }
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Viewport/WebGLViewport.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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),
+    context_(GetFullCanvasId())
+  {
+    AcquireCompositor(new OpenGLCompositor(context_));
+  }
+
+  boost::shared_ptr<WebGLViewport> WebGLViewport::Create(
+    const std::string& canvasId)
+  {
+    boost::shared_ptr<WebGLViewport> that = 
+      boost::shared_ptr<WebGLViewport>(new WebGLViewport(canvasId));
+    that->WebAssemblyViewport::PostConstructor();
+    return that;
+  }
+
+  WebGLViewport::~WebGLViewport()
+  {
+    // Make sure to delete the compositor before its parent "context_" gets
+    // deleted
+    ClearCompositor();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Viewport/WebGLViewport.h	Tue Mar 31 15:51:02 2020 +0200
@@ -0,0 +1,52 @@
+/**
+ * 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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "WebAssemblyViewport.h"
+#include "../OpenGL/WebAssemblyOpenGLContext.h"
+
+namespace OrthancStone
+{
+  class WebGLViewport : public WebAssemblyViewport
+  {
+  private:
+    OpenGL::WebAssemblyOpenGLContext  context_;
+
+    WebGLViewport(const std::string& canvasId);
+
+  protected:
+    virtual void Paint(ICompositor& compositor,
+                       ViewportController& controller) ORTHANC_OVERRIDE;
+    
+    virtual void UpdateSize(ICompositor& compositor) ORTHANC_OVERRIDE;
+
+  public:
+    static boost::shared_ptr<WebGLViewport> Create(const std::string& canvasId);
+
+    virtual ~WebGLViewport();
+
+    bool IsContextLost()
+    {
+      return context_.IsContextLost();
+    } 
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Viewport/WebGLViewportsRegistry.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -0,0 +1,185 @@
+/**
+ * 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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "WebGLViewportsRegistry.h"
+
+#include "../Toolbox/GenericToolbox.h"
+
+#include <Core/OrthancException.h>
+
+#include <boost/make_shared.hpp>
+
+namespace OrthancStone
+{
+  void WebGLViewportsRegistry::LaunchTimer()
+  {
+    timeOutID_ = emscripten_set_timeout(
+      OnTimeoutCallback, 
+      timeoutMS_, 
+      reinterpret_cast<void*>(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<WebGLViewport> viewport;
+
+        // we need to steal the properties from the old viewport
+        // and set them to the new viewport
+        {
+          std::unique_ptr<IViewport::ILock> lock(it->second->Lock());
+
+          // TODO: remove ViewportController
+          Scene2D* scene = lock->GetController().ReleaseScene();
+          viewport = WebGLViewport::Create(it->first);
+
+          {
+            std::unique_ptr<IViewport::ILock> newLock(viewport->Lock());
+            newLock->GetController().AcquireScene(scene);
+          }
+        }
+
+        // Replace the old WebGL viewport by the new one
+        it->second = viewport;
+
+        // Tag the fresh canvas as needing a repaint
+        {
+          std::unique_ptr<IViewport::ILock> lock(it->second->Lock());
+          lock->Invalidate();
+        }
+      }
+    }
+      
+    LaunchTimer();
+  }
+
+  void WebGLViewportsRegistry::OnTimeoutCallback(void *userData)
+  {
+    // This object dies with the process or tab. 
+    WebGLViewportsRegistry* that = 
+      reinterpret_cast<WebGLViewportsRegistry*>(userData);
+    that->OnTimeout();
+  }
+
+  WebGLViewportsRegistry::WebGLViewportsRegistry(double timeoutMS) :
+    timeoutMS_(timeoutMS),
+    timeOutID_(0)
+  {
+    if (timeoutMS <= 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }          
+    
+    LaunchTimer();
+  }
+
+  WebGLViewportsRegistry::~WebGLViewportsRegistry()
+  {
+    emscripten_clear_timeout(timeOutID_);
+    Clear();
+  }
+
+  boost::shared_ptr<WebGLViewport> 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<WebGLViewport> viewport = 
+        WebGLViewport::Create(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);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Viewport/WebGLViewportsRegistry.h	Tue Mar 31 15:51:02 2020 +0200
@@ -0,0 +1,83 @@
+/**
+ * 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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "WebGLViewport.h"
+
+#include <boost/enable_shared_from_this.hpp>
+
+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,
+    public boost::enable_shared_from_this<WebGLViewportsRegistry>
+  {
+  private:
+    typedef std::map<std::string, boost::shared_ptr<WebGLViewport> >  Viewports;
+
+    double     timeoutMS_;
+    Viewports  viewports_;
+    long       timeOutID_;
+
+    void LaunchTimer();
+
+    void OnTimeout();
+
+    static void OnTimeoutCallback(void *userData);
+    
+  public:
+    WebGLViewportsRegistry(double timeoutMS /* in milliseconds */);
+    
+    ~WebGLViewportsRegistry();
+
+    boost::shared_ptr<WebGLViewport> Add(const std::string& canvasId);
+
+    void Remove(const std::string& canvasId);
+
+    void Clear();
+
+    class Accessor : public boost::noncopyable
+    {
+    private:
+      WebGLViewportsRegistry&            that_;
+      std::unique_ptr<IViewport::ILock>  lock_;
+
+    public:
+      Accessor(WebGLViewportsRegistry& that,
+               const std::string& canvasId);
+
+      bool IsValid() const
+      {
+        return lock_.get() != NULL;
+      }
+
+      IViewport::ILock& GetViewport() const;
+    };
+  };
+}
--- a/Framework/Volumes/DicomVolumeImage.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Volumes/DicomVolumeImage.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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
--- a/Framework/Volumes/DicomVolumeImageMPRSlicer.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Volumes/DicomVolumeImageMPRSlicer.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -47,8 +47,7 @@
     revision_(volume_.GetRevision())
   {
     valid_ = (volume_.HasDicomParameters() &&
-              volume_.GetGeometry().DetectSlice(projection_, sliceIndex_, 
-                                                cuttingPlane));
+              volume_.GetGeometry().DetectSlice(projection_, sliceIndex_, cuttingPlane));
   }
 
 
@@ -85,21 +84,6 @@
 
       texture.reset(dynamic_cast<TextureBaseSceneLayer*>
                     (configurator->CreateTextureFromDicom(reader.GetAccessor(), parameters)));
-
-      // <DEBUG-BLOCK>
-#if 0
-      Orthanc::JpegWriter writer;
-      writer.SetQuality(60);
-      static int index = 0;
-      std::string filePath = "C:\\temp\\sliceReader_P";
-      filePath += boost::lexical_cast<std::string>(projection_);
-      filePath += "_I";
-      filePath += boost::lexical_cast<std::string>(index);
-      filePath += ".jpg";
-      index++;
-      writer.WriteToFile(filePath, reader.GetAccessor());
-#endif
-      // <END-OF-DEBUG-BLOCK>
     }
     
     const CoordinateSystem3D& system = volume_.GetGeometry().GetProjectionGeometry(projection_);
@@ -108,53 +92,11 @@
     cuttingPlane.ProjectPoint(x0, y0, system.GetOrigin());
     cuttingPlane.ProjectPoint(x1, y1, system.GetOrigin() + system.GetAxisX());
 
-    // <DEBUG-BLOCK>
-#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
-    // <END-OF-DEBUG-BLOCK>
-
-#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;
@@ -167,19 +109,6 @@
     Vector tmp = volume_.GetGeometry().GetVoxelDimensions(projection_);
     texture->SetPixelSpacing(tmp[0], tmp[1]);
 
-    // <DEBUG-BLOCK>
-    {
-      //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();
-    }
-    // <END-OF-DEBUG-BLOCK>
-
     return texture.release();
   }
 
--- a/Framework/Volumes/IVolumeSlicer.cpp~	Tue Mar 31 15:47:29 2020 +0200
+++ /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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#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;
-  };
-}
--- a/Framework/Volumes/IVolumeSlicer.h~	Tue Mar 31 15:47:29 2020 +0200
+++ /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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#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;
-  };
-}
--- a/Framework/Volumes/VolumeImageGeometry.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Volumes/VolumeImageGeometry.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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<unsigned int>(std::floor(z));
-    if (d >= projectionDepth)
-    {
-      return false;
-    }
     else
     {
-      slice = d;
-      return true;
+      unsigned int d = static_cast<unsigned int>(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<double>(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<double>(z) * dim[2] * normal);
 
     return plane;
   }
--- a/Framework/Volumes/VolumeSceneLayerSource.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Volumes/VolumeSceneLayerSource.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -21,6 +21,9 @@
 
 #include "VolumeSceneLayerSource.h"
 
+#include "../Scene2D/NullLayer.h"
+#include "../StoneException.h"
+
 #include <Core/OrthancException.h>
 
 namespace OrthancStone
@@ -42,17 +45,23 @@
   }
 
 
-  VolumeSceneLayerSource::VolumeSceneLayerSource(Scene2D& scene,
-                                                 int layerDepth,
-                                                 const boost::shared_ptr<IVolumeSlicer>& slicer) :
-    scene_(scene),
-    layerDepth_(layerDepth),
-    slicer_(slicer)
+  VolumeSceneLayerSource::VolumeSceneLayerSource(
+    Scene2D& scene,
+    int layerDepth,
+    const boost::shared_ptr<IVolumeSlicer>& slicer)
+    : scene_(scene)
+    , layerDepth_(layerDepth)
+    , slicer_(slicer)
   {
     if (slicer == NULL)
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
     }
+    ORTHANC_ASSERT(!scene_.HasLayer(layerDepth_));
+
+    // we need to book the scene layer depth by adding a dummy layer
+    std::unique_ptr<NullLayer> nullLayer(new NullLayer);
+    scene_.SetLayer(layerDepth_,nullLayer.release());
   }
 
   VolumeSceneLayerSource::~VolumeSceneLayerSource()
--- a/Framework/Volumes/VolumeSceneLayerSource.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Framework/Volumes/VolumeSceneLayerSource.h	Tue Mar 31 15:51:02 2020 +0200
@@ -38,13 +38,14 @@
   class VolumeSceneLayerSource : public boost::noncopyable
   {
   private:
-    Scene2D&                                scene_;
-    int                                     layerDepth_;
-    boost::shared_ptr<IVolumeSlicer>        slicer_;
+    Scene2D&                                  scene_;
+    int                                       layerDepth_;
+    boost::shared_ptr<IVolumeSlicer>          slicer_;
     std::unique_ptr<ILayerStyleConfigurator>  configurator_;
     std::unique_ptr<CoordinateSystem3D>       lastPlane_;
-    uint64_t                                lastRevision_;
-    uint64_t                                lastConfiguratorRevision_;
+    uint64_t                                  lastRevision_;
+    uint64_t                                  lastConfiguratorRevision_;
+    bool                                      layerInScene_;
 
     void ClearLayer();
 
@@ -71,6 +72,14 @@
 
     ILayerStyleConfigurator& GetConfigurator() const;
 
+    /**
+    Make sure the Scene2D is protected from concurrent accesses before 
+    calling this method.
+
+    If the scene that has been supplied to the ctor is part of an IViewport, 
+    you can lock the whole viewport data (including scene) by means of the 
+    IViewport::Lock method.
+    */ 
     void Update(const CoordinateSystem3D& plane);  
   };
 }
--- a/Platforms/Generic/DelayedCallCommand.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Platforms/Generic/DelayedCallCommand.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -26,13 +26,11 @@
 
 namespace Deprecated
 {
-  DelayedCallCommand::DelayedCallCommand(OrthancStone::MessageBroker& broker,
-                                         OrthancStone::MessageHandler<IDelayedCallExecutor::TimeoutMessage>* callback,  // takes ownership
+  DelayedCallCommand::DelayedCallCommand(MessageHandler<IDelayedCallExecutor::TimeoutMessage>* callback,  // takes ownership
                                          unsigned int timeoutInMs,
                                          Orthanc::IDynamicObject* payload /* takes ownership */,
                                          OrthancStone::NativeStoneApplicationContext& context
                                          ) :
-    IObservable(broker),
     callback_(callback),
     payload_(payload),
     context_(context),
--- a/Platforms/Generic/DelayedCallCommand.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Platforms/Generic/DelayedCallCommand.h	Tue Mar 31 15:51:02 2020 +0200
@@ -35,15 +35,14 @@
   class DelayedCallCommand : public IOracleCommand, OrthancStone::IObservable
   {
   protected:
-    std::unique_ptr<OrthancStone::MessageHandler<IDelayedCallExecutor::TimeoutMessage> >  callback_;
+    std::unique_ptr<MessageHandler<IDelayedCallExecutor::TimeoutMessage> >  callback_;
     std::unique_ptr<Orthanc::IDynamicObject>  payload_;
     OrthancStone::NativeStoneApplicationContext&          context_;
     boost::posix_time::ptime                expirationTimePoint_;
     unsigned int                            timeoutInMs_;
 
   public:
-    DelayedCallCommand(OrthancStone::MessageBroker& broker,
-                       OrthancStone::MessageHandler<IDelayedCallExecutor::TimeoutMessage>* callback,  // takes ownership
+    DelayedCallCommand(MessageHandler<IDelayedCallExecutor::TimeoutMessage>* callback,  // takes ownership
                        unsigned int timeoutInMs,
                        Orthanc::IDynamicObject* payload /* takes ownership */,
                        OrthancStone::NativeStoneApplicationContext& context
--- a/Platforms/Generic/OracleDelayedCallExecutor.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Platforms/Generic/OracleDelayedCallExecutor.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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<IDelayedCallExecutor::TimeoutMessage>* callback,
+    virtual void Schedule(MessageHandler<IDelayedCallExecutor::TimeoutMessage>* callback,
                           unsigned int timeoutInMs = 1000)
     {
-      oracle_.Submit(new DelayedCallCommand(broker_, callback, timeoutInMs, NULL, context_));
+      oracle_.Submit(new DelayedCallCommand(callback, timeoutInMs, NULL, context_));
     }
   };
 }
--- a/Platforms/Generic/OracleWebService.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Platforms/Generic/OracleWebService.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -29,19 +29,17 @@
   class OracleWebService::WebServiceCachedGetCommand : public IOracleCommand, OrthancStone::IObservable
   {
   protected:
-    std::unique_ptr<OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage> >  successCallback_;
+    std::unique_ptr<MessageHandler<IWebService::HttpRequestSuccessMessage> >  successCallback_;
     std::unique_ptr<Orthanc::IDynamicObject>                                  payload_;
     boost::shared_ptr<BaseWebService::CachedHttpRequestSuccessMessage>      cachedMessage_;
     OrthancStone::NativeStoneApplicationContext&                                          context_;
 
   public:
-    WebServiceCachedGetCommand(OrthancStone::MessageBroker& broker,
-                               OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,  // takes ownership
+    WebServiceCachedGetCommand(MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,  // takes ownership
                                boost::shared_ptr<BaseWebService::CachedHttpRequestSuccessMessage> 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<BaseWebService::CachedHttpRequestSuccessMessage> cachedMessage,
                                                 Orthanc::IDynamicObject* payload, // takes ownership
-                                                OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback)
+                                                MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback)
   {
-    oracle_.Submit(new WebServiceCachedGetCommand(GetBroker(), successCallback, cachedMessage, payload, context_));
+    oracle_.Submit(new WebServiceCachedGetCommand(successCallback, cachedMessage, payload, context_));
   }
 
 
--- a/Platforms/Generic/OracleWebService.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Platforms/Generic/OracleWebService.h	Tue Mar 31 15:51:02 2020 +0200
@@ -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<IWebService::HttpRequestSuccessMessage>* successCallback, // takes ownership
-                           OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, // takes ownership
+                           MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback, // takes ownership
+                           MessageHandler<IWebService::HttpRequestErrorMessage>* 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<IWebService::HttpRequestSuccessMessage>* successCallback,
-                             OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,
+                             MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,
+                             MessageHandler<IWebService::HttpRequestErrorMessage>* 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<IWebService::HttpRequestSuccessMessage>* successCallback,   // takes ownership
-                                  OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,// takes ownership
+                                  MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,   // takes ownership
+                                  MessageHandler<IWebService::HttpRequestErrorMessage>* 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<BaseWebService::CachedHttpRequestSuccessMessage> cachedHttpMessage,
                                         Orthanc::IDynamicObject* payload, // takes ownership
-                                        OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback);
+                                        MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback);
 
   };
 }
--- a/Platforms/Generic/WebServiceCommandBase.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Platforms/Generic/WebServiceCommandBase.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -25,16 +25,14 @@
 
 namespace Deprecated
 {
-  WebServiceCommandBase::WebServiceCommandBase(OrthancStone::MessageBroker& broker,
-                                               OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,
-                                               OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
+  WebServiceCommandBase::WebServiceCommandBase(MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,
+                                               MessageHandler<IWebService::HttpRequestErrorMessage>* 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),
--- a/Platforms/Generic/WebServiceCommandBase.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Platforms/Generic/WebServiceCommandBase.h	Tue Mar 31 15:51:02 2020 +0200
@@ -37,8 +37,8 @@
   class WebServiceCommandBase : public IOracleCommand, OrthancStone::IObservable
   {
   protected:
-    std::unique_ptr<OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage> >  successCallback_;
-    std::unique_ptr<OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage> >    failureCallback_;
+    std::unique_ptr<MessageHandler<IWebService::HttpRequestSuccessMessage> >  successCallback_;
+    std::unique_ptr<MessageHandler<IWebService::HttpRequestErrorMessage> >    failureCallback_;
     Orthanc::WebServiceParameters           parameters_;
     std::string                             url_;
     IWebService::HttpHeaders                headers_;
@@ -51,9 +51,8 @@
     unsigned int                            timeoutInSeconds_;
 
   public:
-    WebServiceCommandBase(OrthancStone::MessageBroker& broker,
-                          OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,  // takes ownership
-                          OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,  // takes ownership
+    WebServiceCommandBase(MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,  // takes ownership
+                          MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,  // takes ownership
                           const Orthanc::WebServiceParameters& parameters,
                           const std::string& url,
                           const IWebService::HttpHeaders& headers,
--- a/Platforms/Generic/WebServiceDeleteCommand.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Platforms/Generic/WebServiceDeleteCommand.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -25,16 +25,15 @@
 
 namespace Deprecated
 {
-  WebServiceDeleteCommand::WebServiceDeleteCommand(OrthancStone::MessageBroker& broker,
-                                                   OrthancStone::MessageHandler<Deprecated::IWebService::HttpRequestSuccessMessage>* successCallback,  // takes ownership
-                                                   OrthancStone::MessageHandler<Deprecated::IWebService::HttpRequestErrorMessage>* failureCallback,  // takes ownership
+  WebServiceDeleteCommand::WebServiceDeleteCommand(MessageHandler<Deprecated::IWebService::HttpRequestSuccessMessage>* successCallback,  // takes ownership
+                                                   MessageHandler<Deprecated::IWebService::HttpRequestErrorMessage>* 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)
   {
   }
 
--- a/Platforms/Generic/WebServiceDeleteCommand.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Platforms/Generic/WebServiceDeleteCommand.h	Tue Mar 31 15:51:02 2020 +0200
@@ -28,9 +28,8 @@
   class WebServiceDeleteCommand : public WebServiceCommandBase
   {
   public:
-    WebServiceDeleteCommand(OrthancStone::MessageBroker& broker,
-                            OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,  // takes ownership
-                            OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,  // takes ownership
+    WebServiceDeleteCommand(MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,  // takes ownership
+                            MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,  // takes ownership
                             const Orthanc::WebServiceParameters& parameters,
                             const std::string& url,
                             const IWebService::HttpHeaders& headers,
--- a/Platforms/Generic/WebServiceGetCommand.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Platforms/Generic/WebServiceGetCommand.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -25,17 +25,15 @@
 
 namespace Deprecated
 {
-
-  WebServiceGetCommand::WebServiceGetCommand(OrthancStone::MessageBroker& broker,
-                                             OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,  // takes ownership
-                                             OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,  // takes ownership
+  WebServiceGetCommand::WebServiceGetCommand(MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,  // takes ownership
+                                             MessageHandler<IWebService::HttpRequestErrorMessage>* 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)
   {
   }
 
--- a/Platforms/Generic/WebServiceGetCommand.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Platforms/Generic/WebServiceGetCommand.h	Tue Mar 31 15:51:02 2020 +0200
@@ -28,9 +28,8 @@
   class WebServiceGetCommand : public WebServiceCommandBase
   {
   public:
-    WebServiceGetCommand(OrthancStone::MessageBroker& broker,
-                         OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,  // takes ownership
-                         OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,  // takes ownership
+    WebServiceGetCommand(MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,  // takes ownership
+                         MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,  // takes ownership
                          const Orthanc::WebServiceParameters& parameters,
                          const std::string& url,
                          const IWebService::HttpHeaders& headers,
--- a/Platforms/Generic/WebServicePostCommand.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/Platforms/Generic/WebServicePostCommand.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -25,9 +25,8 @@
 
 namespace Deprecated
 {
-  WebServicePostCommand::WebServicePostCommand(OrthancStone::MessageBroker& broker,
-                                               OrthancStone::MessageHandler<Deprecated::IWebService::HttpRequestSuccessMessage>* successCallback,  // takes ownership
-                                               OrthancStone::MessageHandler<Deprecated::IWebService::HttpRequestErrorMessage>* failureCallback,  // takes ownership
+  WebServicePostCommand::WebServicePostCommand(MessageHandler<Deprecated::IWebService::HttpRequestSuccessMessage>* successCallback,  // takes ownership
+                                               MessageHandler<Deprecated::IWebService::HttpRequestErrorMessage>* 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)
   {
   }
--- a/Platforms/Generic/WebServicePostCommand.h	Tue Mar 31 15:47:29 2020 +0200
+++ b/Platforms/Generic/WebServicePostCommand.h	Tue Mar 31 15:51:02 2020 +0200
@@ -31,9 +31,8 @@
     std::string  body_;
 
   public:
-    WebServicePostCommand(OrthancStone::MessageBroker& broker,
-                          OrthancStone::MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,  // takes ownership
-                          OrthancStone::MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,  // takes ownership
+    WebServicePostCommand(MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,  // takes ownership
+                          MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,  // takes ownership
                           const Orthanc::WebServiceParameters& parameters,
                           const std::string& url,
                           const IWebService::HttpHeaders& headers,
--- a/Resources/CMake/OrthancStoneConfiguration.cmake	Tue Mar 31 15:47:29 2020 +0200
+++ b/Resources/CMake/OrthancStoneConfiguration.cmake	Tue Mar 31 15:51:02 2020 +0200
@@ -121,12 +121,16 @@
     -DORTHANC_ENABLE_SDL=1
     )
 elseif(ENABLE_QT)
-  message("QT is enabled")
-  include(${CMAKE_CURRENT_LIST_DIR}/QtConfiguration.cmake)
   add_definitions(
     -DORTHANC_ENABLE_QT=1
     -DORTHANC_ENABLE_SDL=0
     )
+  if(DISABLE_STONE_QT_CMAKE_FILE)
+    message("QT is enabled, but QtConfiguration.cmake will not be included")  
+  else()    
+    message("QT is enabled")  
+    include(${CMAKE_CURRENT_LIST_DIR}/QtConfiguration.cmake)
+  endif()
 else()
   message("SDL and QT are both disabled")
   unset(USE_SYSTEM_SDL CACHE)
@@ -250,61 +254,43 @@
 ## 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}/Framework/Loaders/GenericLoadersContext.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Loaders/GenericLoadersContext.h
+    ${ORTHANC_STONE_ROOT}/Platforms/Generic/DelayedCallCommand.cpp
+    ${ORTHANC_STONE_ROOT}/Platforms/Generic/DelayedCallCommand.h
+    ${ORTHANC_STONE_ROOT}/Platforms/Generic/Oracle.cpp
+    ${ORTHANC_STONE_ROOT}/Platforms/Generic/Oracle.h
+    ${ORTHANC_STONE_ROOT}/Platforms/Generic/OracleDelayedCallExecutor.h
     ${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/WebServiceCommandBase.h
     ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceDeleteCommand.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/WebServiceDeleteCommand.h
+    ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceGetCommand.cpp
+    ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceGetCommand.h
+    ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServicePostCommand.cpp
+    ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServicePostCommand.h
     )
 
-  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
+      ${ORTHANC_STONE_ROOT}/Framework/Viewport/SdlWindow.h
       )
   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/OpenGL/SdlOpenGLContext.h
+        ${ORTHANC_STONE_ROOT}/Framework/Viewport/SdlViewport.cpp
+        ${ORTHANC_STONE_ROOT}/Framework/Viewport/SdlViewport.h
         )
-
-      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,60 +317,156 @@
     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
+      ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Viewport/CairoFont.h
+      )
+  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_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_STONE_DEPRECATED)
+  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/dev.h
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/SmartLoader.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/SmartLoader.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/CircleMeasureTracker.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/CircleMeasureTracker.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/ColorFrameRenderer.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/ColorFrameRenderer.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/DicomSeriesVolumeSlicer.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/DicomSeriesVolumeSlicer.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/DicomStructureSetSlicer.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/DicomStructureSetSlicer.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/FrameRenderer.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/FrameRenderer.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/GrayscaleFrameRenderer.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/GrayscaleFrameRenderer.h
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/ILayerRenderer.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/IVolumeSlicer.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/LineLayerRenderer.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/LineLayerRenderer.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/LineMeasureTracker.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/LineMeasureTracker.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/RenderStyle.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/RenderStyle.h
+    # ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/SeriesFrameRendererFactory.cpp
+    # ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/SeriesFrameRendererFactory.h
+    # ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/SingleFrameRendererFactory.cpp
+    # ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/SingleFrameRendererFactory.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/SliceOutlineRenderer.cpp
-    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/SmartLoader.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Layers/SliceOutlineRenderer.h
+    ${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/Toolbox/BaseWebService.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/BaseWebService.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/DicomFrameConverter.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/DicomFrameConverter.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/DownloadStack.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/DownloadStack.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/IDelayedCallExecutor.h
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/ISeriesLoader.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/IWebService.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/IWebService.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/MessagingToolbox.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/MessagingToolbox.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/OrthancApiClient.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/OrthancApiClient.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/OrthancSlicesLoader.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/OrthancSlicesLoader.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/ParallelSlices.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/ParallelSlices.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/ParallelSlicesCursor.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/ParallelSlicesCursor.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/Slice.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/Slice.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/ViewportGeometry.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Toolbox/ViewportGeometry.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Viewport/IMouseTracker.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Viewport/IStatusBar.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Viewport/IViewport.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Viewport/WidgetViewport.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Viewport/WidgetViewport.h
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Volumes/IGeometryProvider.h
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Volumes/ISlicedVolume.h
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Volumes/IVolumeLoader.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Volumes/StructureSetLoader.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Volumes/StructureSetLoader.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/CairoWidget.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/CairoWidget.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/EmptyWidget.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/EmptyWidget.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/IWidget.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/IWorldSceneInteractor.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/IWorldSceneMouseTracker.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/LayoutWidget.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/LayoutWidget.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/PanMouseTracker.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/PanMouseTracker.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/PanZoomMouseTracker.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/PanZoomMouseTracker.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/SliceViewerWidget.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/SliceViewerWidget.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/TestCairoWidget.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/TestCairoWidget.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/TestWorldSceneWidget.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/TestWorldSceneWidget.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/WidgetBase.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/WidgetBase.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/WorldSceneWidget.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/WorldSceneWidget.h
     ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/ZoomMouseTracker.cpp
-    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/dev.h
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Widgets/ZoomMouseTracker.h
+
     ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyAlphaLayer.cpp
     ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyDicomLayer.cpp
     ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayer.cpp
@@ -405,17 +487,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,57 +535,92 @@
   ${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/IMessageEmitter.h
   ${ORTHANC_STONE_ROOT}/Framework/Messages/IObservable.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Messages/IObserver.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Messages/IObservable.h
   ${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/Messages/ObserverBase.h
+
   ${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/CairoCompositor.h
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Color.h
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/ColorSceneLayer.h
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/ColorTextureSceneLayer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/ColorTextureSceneLayer.h
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/FloatTextureSceneLayer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/FloatTextureSceneLayer.h
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/GrayscaleStyleConfigurator.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/GrayscaleStyleConfigurator.h
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/ICompositor.h
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/ILayerStyleConfigurator.h
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/InfoPanelSceneLayer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/InfoPanelSceneLayer.h
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/IPointerTracker.h
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/ISceneLayer.h
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/LookupTableStyleConfigurator.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/LookupTableStyleConfigurator.h
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/LookupTableTextureSceneLayer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/LookupTableTextureSceneLayer.h
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/NullLayer.h
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PanSceneTracker.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PanSceneTracker.h
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PointerEvent.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PointerEvent.h
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PolylineSceneLayer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PolylineSceneLayer.h
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/RotateSceneTracker.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/RotateSceneTracker.h
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Scene2D.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Scene2D.h
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/ScenePoint2D.h
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/TextSceneLayer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/TextSceneLayer.h
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/TextureBaseSceneLayer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/TextureBaseSceneLayer.h
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/ZoomSceneTracker.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/ZoomSceneTracker.h
+
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoBaseRenderer.h
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoColorTextureRenderer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoColorTextureRenderer.h
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoFloatTextureRenderer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoFloatTextureRenderer.h
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoInfoPanelRenderer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoInfoPanelRenderer.h
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoLookupTableTextureRenderer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoLookupTableTextureRenderer.h
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoPolylineRenderer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoPolylineRenderer.h
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoTextRenderer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CairoTextRenderer.h
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CompositorHelper.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/CompositorHelper.h
   ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/FixedPointAligner.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/LookupTableStyleConfigurator.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/LookupTableTextureSceneLayer.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PanSceneTracker.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PointerEvent.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/PolylineSceneLayer.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/RotateSceneTracker.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Scene2D.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/TextSceneLayer.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/TextureBaseSceneLayer.cpp
-  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/ZoomSceneTracker.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/FixedPointAligner.h
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/ICairoContextProvider.h
+  
   ${ORTHANC_STONE_ROOT}/Framework/Scene2DViewport/AngleMeasureTool.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Scene2DViewport/AngleMeasureTool.h
   ${ORTHANC_STONE_ROOT}/Framework/Scene2DViewport/CreateAngleMeasureCommand.cpp
@@ -588,8 +717,6 @@
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/UndoRedoStack.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
@@ -648,38 +775,40 @@
     ${ORTHANC_STONE_ROOT}/Framework/OpenGL/OpenGLTexture.cpp
     ${ORTHANC_STONE_ROOT}/Framework/Scene2D/OpenGLCompositor.h
     ${ORTHANC_STONE_ROOT}/Framework/Scene2D/OpenGLCompositor.cpp
+
+    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLAdvancedPolylineRenderer.cpp
     ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLAdvancedPolylineRenderer.h
-    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLAdvancedPolylineRenderer.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLBasicPolylineRenderer.cpp
     ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLBasicPolylineRenderer.h
-    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLBasicPolylineRenderer.cpp
-    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLColorTextureProgram.h
     ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLColorTextureProgram.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLColorTextureProgram.h
+    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLColorTextureRenderer.cpp
     ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLColorTextureRenderer.h
-    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLColorTextureRenderer.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLFloatTextureProgram.cpp
     ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLFloatTextureProgram.h
-    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLFloatTextureProgram.cpp
-    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLFloatTextureRenderer.h
     ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLFloatTextureRenderer.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLFloatTextureRenderer.h
+    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLInfoPanelRenderer.cpp
     ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLInfoPanelRenderer.h
-    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLInfoPanelRenderer.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLLinesProgram.cpp
     ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLLinesProgram.h
-    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLLinesProgram.cpp
-    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLLookupTableTextureRenderer.h
     ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLLookupTableTextureRenderer.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLLookupTableTextureRenderer.h
+    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLShaderVersionDirective.h
+    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLTextProgram.cpp
     ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLTextProgram.h
-    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLTextProgram.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLTextRenderer.cpp
     ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLTextRenderer.h
-    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLTextRenderer.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLTextureProgram.cpp
     ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLTextureProgram.h
-    ${ORTHANC_STONE_ROOT}/Framework/Scene2D/Internals/OpenGLTextureProgram.cpp
     )
 
   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()
--- a/Resources/CMake/QtConfiguration.cmake	Tue Mar 31 15:47:29 2020 +0200
+++ b/Resources/CMake/QtConfiguration.cmake	Tue Mar 31 15:51:02 2020 +0200
@@ -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 "<?xml ...?>"
-  # 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 "<?xml ...?>"
+  # 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)
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Conventions.txt	Tue Mar 31 15:51:02 2020 +0200
@@ -0,0 +1,88 @@
+
+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::unique_ptr<>" and "boost::shared_ptr<>" (*not*
+  "std::shared_ptr<>"). We provide an implementation of std::unique_ptr for
+  pre-C++11 compilers.
+
+* 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::unique_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<>".
--- a/UnitTestsSources/GenericToolboxTests.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/UnitTestsSources/GenericToolboxTests.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -20,8 +20,9 @@
 
 #include <Framework/Toolbox/GenericToolbox.h>
 
-#include <boost/chrono.hpp>
+#include <boost/date_time/posix_time/posix_time.hpp>
 #include <boost/lexical_cast.hpp>
+#include <boost/enable_shared_from_this.hpp>
 
 #include "gtest/gtest.h"
 
@@ -3878,19 +3879,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<boost::chrono::microseconds>(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 +3902,8 @@
           ok = false;
         }
       }
-      boost::chrono::system_clock::time_point end = boost::chrono::system_clock::now();
-      boost::chrono::microseconds elapsed =
-        boost::chrono::duration_cast<boost::chrono::microseconds>(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 +4092,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<boost::chrono::microseconds>(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 +4115,8 @@
           ok = false;
         }
       }
-      boost::chrono::system_clock::time_point end = boost::chrono::system_clock::now();
-      boost::chrono::microseconds elapsed =
-        boost::chrono::duration_cast<boost::chrono::microseconds>(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;
 
@@ -4251,3 +4244,10 @@
   EXPECT_EQ(0, green);
   EXPECT_EQ(0, blue);
 }
+
+
+
+
+
+
+
--- a/UnitTestsSources/TestMessageBroker.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/UnitTestsSources/TestMessageBroker.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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<MyObserver>
   {
   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<MyObservable::MyCustomMessage>(broker, *this));
-    }
   };
 }
 
 
 TEST(MessageBroker, TestPermanentConnectionSimpleUseCase)
 {
-  MessageBroker broker;
-  MyObservable  observable(broker);
-  MyObserver    observer(broker);
+  MyObservable  observable;
+  boost::shared_ptr<MyObserver>  observer(new MyObserver);
 
   // create a permanent connection between an observable and an observer
-  observable.RegisterObserverCallback(new Callable<MyObserver, MyObservable::MyCustomMessage>(observer, &MyObserver::HandleCompletedMessage));
+  observer->Register<MyObservable::MyCustomMessage>(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<MyObserver, MyObservable::MyCustomMessage>(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<MyObserver>  observer(new MyObserver);
 
   // create a permanent connection between an observable and an observer
-  observable.RegisterObserverCallback(new Callable<MyObserver, MyObservable::MyCustomMessage>(*observer, &MyObserver::HandleCompletedMessage));
+  observer->Register<MyObservable::MyCustomMessage>(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<MyObserver, MyObservable::MyCustomMessage>(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<MyObserver, MyObservable::MyCustomMessage>(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<MyObservable::MyCustomMessage>(*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<MyObservable::MyCustomMessage>(*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
--- a/UnitTestsSources/UnitTestsMain.cpp	Tue Mar 31 15:47:29 2020 +0200
+++ b/UnitTestsSources/UnitTestsMain.cpp	Tue Mar 31 15:51:02 2020 +0200
@@ -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<float>(z)) / static_cast<float>(g.GetDepth());  // Z-center of the voxel
+    
+    for (unsigned int y = 0; y < g.GetHeight(); y++)
+    {
+      const float yy = (0.5f + static_cast<float>(y)) / static_cast<float>(g.GetHeight());  // Y-center of the voxel
+
+      for (unsigned int x = 0; x < g.GetWidth(); x++)
+      {
+        const float xx = (0.5f + static_cast<float>(x)) / static_cast<float>(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<double>(x) * sx,
+                      static_cast<double>(y) * sy) +
+                    z * sz * g.GetAxialGeometry().GetNormal());
+        ASSERT_TRUE(IsEqualVectorL1(p, q));
+        
+        q = (g.GetCoronalGeometry().MapSliceToWorldCoordinates(
+               static_cast<double>(x) * sx,
+               static_cast<double>(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<double>(y) * sy,
+               static_cast<double>(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<double>(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<double>(i) * 
+                                    (-s.GetNormal()) * g.GetVoxelDimensions(projection)[2]));
+      }
+      else
+      {
+        ASSERT_TRUE(IsEqualVectorL1(plane.GetOrigin(), s.GetOrigin() + static_cast<double>(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_DOUBLE_EQ(1.2, v[0]);
+
+  ASSERT_TRUE(OrthancStone::LinearAlgebra::ParseVector(v, "-1.2e+2"));
+  ASSERT_EQ(1u, v.size());
+  ASSERT_DOUBLE_EQ(-120.0, v[0]);
+
+  ASSERT_TRUE(OrthancStone::LinearAlgebra::ParseVector(v, "-1e-2\\2"));
+  ASSERT_EQ(2u, v.size());
+  ASSERT_DOUBLE_EQ(-0.01, v[0]);
+  ASSERT_DOUBLE_EQ(2.0, v[1]);
+
+  ASSERT_TRUE(OrthancStone::LinearAlgebra::ParseVector(v, "1.3671875\\1.3671875"));
+  ASSERT_EQ(2u, v.size());
+  ASSERT_DOUBLE_EQ(1.3671875, v[0]);
+  ASSERT_DOUBLE_EQ(1.3671875, v[1]); 
+}
+
+
 int main(int argc, char **argv)
 {
   Orthanc::Logging::Initialize();