changeset 1381:f4a06ad1580b

Branch broker is now the new default
author Benjamin Golinvaux <bgo@osimis.io>
date Wed, 22 Apr 2020 14:05:47 +0200
parents 4431ffdcc2a4 (current diff) 6ea4062c1a0d (diff)
children 9d138883be66
files Applications/Samples/BasicPetCtFusionApplication.h Applications/Samples/CMakeLists.txt Applications/Samples/CMakeLists.txt.old Applications/Samples/EmptyApplication.h Applications/Samples/LayoutPetCtFusionApplication.h Applications/Samples/Qt/SampleMainWindow.cpp Applications/Samples/Qt/SampleMainWindow.h Applications/Samples/Qt/SampleMainWindow.ui Applications/Samples/Qt/SampleMainWindowWithButtons.cpp Applications/Samples/Qt/SampleMainWindowWithButtons.h Applications/Samples/Qt/SampleMainWindowWithButtons.ui Applications/Samples/Qt/SampleQtApplicationRunner.h Applications/Samples/SampleApplicationBase.h Applications/Samples/SampleInteractor.h Applications/Samples/SampleList.h Applications/Samples/SampleMainNative.cpp Applications/Samples/SampleMainWasm.cpp Applications/Samples/Samples-status.md Applications/Samples/SimpleViewer/AppStatus.h Applications/Samples/SimpleViewer/MainWidgetInteractor.cpp Applications/Samples/SimpleViewer/MainWidgetInteractor.h Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.cpp Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.h Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.ui Applications/Samples/SimpleViewer/Qt/mainQt.cpp Applications/Samples/SimpleViewer/SimpleViewerApplication.cpp Applications/Samples/SimpleViewer/SimpleViewerApplication.h Applications/Samples/SimpleViewer/ThumbnailInteractor.cpp Applications/Samples/SimpleViewer/ThumbnailInteractor.h Applications/Samples/SimpleViewer/Wasm/SimpleViewerWasmApplicationAdapter.cpp Applications/Samples/SimpleViewer/Wasm/SimpleViewerWasmApplicationAdapter.h Applications/Samples/SimpleViewer/Wasm/mainWasm.cpp Applications/Samples/SimpleViewer/Wasm/simple-viewer.html Applications/Samples/SimpleViewer/Wasm/simple-viewer.ts Applications/Samples/SimpleViewer/Wasm/styles.css Applications/Samples/SimpleViewer/Wasm/tsconfig-simple-viewer.json Applications/Samples/SimpleViewerApplicationSingleFile.h Applications/Samples/SingleFrameApplication.h Applications/Samples/SingleFrameEditorApplication.h Applications/Samples/SingleVolumeApplication.h Applications/Samples/StoneSampleCommands.yml Applications/Samples/StoneSampleCommands_generate.py Applications/Samples/StoneSampleCommands_generated.hpp Applications/Samples/StoneSampleCommands_generated.ts Applications/Samples/SynchronizedSeriesApplication.h Applications/Samples/TestPatternApplication.h Applications/Samples/Web/index.html Applications/Samples/Web/samples-styles.css Applications/Samples/Web/simple-viewer-single-file.html Applications/Samples/Web/simple-viewer-single-file.ts Applications/Samples/Web/simple-viewer-single-file.tsconfig.json Applications/Samples/Web/single-frame-editor.html Applications/Samples/Web/single-frame-editor.ts Applications/Samples/Web/single-frame-editor.tsconfig.json Applications/Samples/Web/single-frame.html Applications/Samples/Web/single-frame.ts Applications/Samples/Web/single-frame.tsconfig.json Applications/Samples/Web/tsconfig-samples.json Applications/Samples/build-wasm.sh Applications/Samples/build-wasm.sh.old Applications/Samples/build-web-ext.sh Applications/Samples/build-web.sh Applications/Samples/get-requirements-windows.ps1 Applications/Samples/nginx.local.conf Applications/Samples/package-lock.json Applications/Samples/rt-viewer-demo/CMakeLists.txt Applications/Samples/rt-viewer-demo/build-sdl-msvc15.ps1 Applications/Samples/rt-viewer-demo/build-wasm.sh Applications/Samples/rt-viewer-demo/build-web.sh Applications/Samples/rt-viewer-demo/index.html Applications/Samples/rt-viewer-demo/main.cpp Applications/Samples/rt-viewer-demo/nginx.local.conf Applications/Samples/rt-viewer-demo/rt-viewer-demo.html Applications/Samples/rt-viewer-demo/rt-viewer-demo.ts Applications/Samples/rt-viewer-demo/rt-viewer-demo.tsconfig.json Applications/Samples/rt-viewer-demo/samples-styles.css Applications/Samples/rt-viewer-demo/start-serving-files.sh Applications/Samples/rt-viewer-demo/stop-serving-files.sh Applications/Samples/rt-viewer-demo/tsconfig-samples.json Applications/Samples/tsconfig-stone.json Framework/Loaders/DicomStructureSetLoader.cpp Framework/Loaders/DicomStructureSetLoader.h Framework/Loaders/DicomStructureSetLoader2.cpp Framework/Loaders/DicomStructureSetLoader2.h Framework/Loaders/LoaderCache.cpp Framework/Loaders/LoaderCache.h Framework/Loaders/LoaderStateMachine.cpp Framework/Loaders/LoaderStateMachine.h Framework/Loaders/OrthancMultiframeVolumeLoader.cpp Framework/Loaders/OrthancMultiframeVolumeLoader.h Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h Framework/Messages/IObserver.cpp Framework/Messages/LockingEmitter.h Framework/Messages/MessageBroker.h Framework/Messages/MessageForwarder.cpp Framework/Messages/MessageForwarder.h Framework/Oracle/OracleCommandWithPayload.cpp Framework/Oracle/OracleCommandWithPayload.h Framework/Viewport/ViewportBase.cpp Framework/Viewport/ViewportBase.h Framework/Volumes/IVolumeSlicer.cpp~ Framework/Volumes/IVolumeSlicer.h~ Samples/MultiPlatform/BasicScene/BasicScene.cpp Samples/MultiPlatform/BasicScene/BasicScene.h Samples/MultiPlatform/BasicScene/mainQt.cpp Samples/MultiPlatform/BasicScene/mainSdl.cpp Samples/Qt/BasicSceneWindow.cpp Samples/Qt/BasicSceneWindow.h Samples/Qt/BasicSceneWindow.ui Samples/Qt/CMakeLists.txt Samples/Qt/QStoneOpenGlWidget.cpp Samples/Qt/QStoneOpenGlWidget.h Samples/Qt/Scene2DInteractor.cpp Samples/Qt/Scene2DInteractor.h Samples/Sdl/BasicScene.cpp Samples/Sdl/CMakeLists.txt Samples/Sdl/FusionMprSdl.cpp Samples/Sdl/FusionMprSdl.h Samples/Sdl/Loader.cpp Samples/Sdl/RadiographyEditor.cpp Samples/Sdl/TrackerSample.cpp Samples/Sdl/TrackerSampleApp.cpp Samples/Sdl/TrackerSampleApp.h Samples/Sdl/cpp.hint Samples/WebAssembly/BasicMPR.cpp Samples/WebAssembly/BasicMPR.html Samples/WebAssembly/BasicScene.cpp Samples/WebAssembly/BasicScene.html Samples/WebAssembly/CMakeLists.txt Samples/WebAssembly/Configuration.json Samples/WebAssembly/ConfigurationLocalSJO.json Samples/WebAssembly/NOTES.txt Samples/WebAssembly/app.js Samples/WebAssembly/dev.h Samples/WebAssembly/index.html
diffstat 521 files changed, 31475 insertions(+), 19587 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Mon Apr 20 18:26:32 2020 +0200
+++ b/.hgignore	Wed Apr 22 14:05:47 2020 +0200
@@ -5,15 +5,15 @@
 .vs/
 .vscode/
 Applications/Qt/archive/
-Applications/Samples/ThirdPartyDownloads/
-Applications/Samples/build-wasm/
-Applications/Samples/build-web/
-Applications/Samples/node_modules/
-Applications/Samples/rt-viewer-demo/ThirdPartyDownloads/
-Applications/Samples/rt-viewer-demo/build-sdl-msvc15/
-Applications/Samples/rt-viewer-demo/build-tsc-output/
-Applications/Samples/rt-viewer-demo/build-wasm/
-Applications/Samples/rt-viewer-demo/build-web/
+Applications/Samples/Deprecated/ThirdPartyDownloads/
+Applications/Samples/Deprecated/build-wasm/
+Applications/Samples/Deprecated/build-web/
+Applications/Samples/Deprecated/node_modules/
+Applications/Samples/Deprecated/rt-viewer-demo/ThirdPartyDownloads/
+Applications/Samples/Deprecated/rt-viewer-demo/build-sdl-msvc15/
+Applications/Samples/Deprecated/rt-viewer-demo/build-tsc-output/
+Applications/Samples/Deprecated/rt-viewer-demo/build-wasm/
+Applications/Samples/Deprecated/rt-viewer-demo/build-web/
 Applications/build-*
 CMakeLists.txt.user
 Platforms/Generic/ThirdPartyDownloads/
@@ -31,12 +31,16 @@
 Resources/CommandTool/protoc-tests/generated_js/
 Resources/CommandTool/protoc-tests/generated_ts/
 Resources/CommandTool/protoc-tests/node_modules/
-Samples/Sdl/ThirdPartyDownloads/
-Samples/Sdl/CMakeLists.txt.orig
-Samples/Qt/ThirdPartyDownloads/
-
-Samples/WebAssembly/build/
-Samples/WebAssembly/ThirdPartyDownloads/
-Samples/WebAssembly/installDir/
+Samples/WebAssembly/*/ThirdPartyDownloads/
+Samples/WebAssembly/*/out
+Samples/Sdl/*/ThirdPartyDownloads/
+Samples/Sdl/*/out
+Samples/Deprecated/Sdl/ThirdPartyDownloads/
+Samples/Deprecated/Sdl/CMakeLists.txt.orig
+Samples/Deprecated/Qt/ThirdPartyDownloads/
+Samples/Deprecated/WebAssembly/build/
+Samples/Deprecated/WebAssembly/ThirdPartyDownloads/
+Samples/Deprecated/WebAssembly/installDir/
 
 node_modules/
+
--- a/Applications/Generic/GuiAdapter.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ b/Applications/Generic/GuiAdapter.cpp	Wed Apr 22 14:05:47 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>
@@ -47,6 +47,15 @@
     return os;
   }
 
+  std::ostream& operator<<(
+    std::ostream& os, const GuiAdapterMouseEvent& event)
+  {
+    os << "targetX: " << event.targetX << " targetY: " << event.targetY << " button: " << event.button
+      << "ctrlKey: " << event.ctrlKey << "shiftKey: " << event.shiftKey << "altKey: " << event.altKey;
+      
+    return os;
+  }
+
 #if ORTHANC_ENABLE_WASM == 1
   void GuiAdapter::Run(GuiAdapterRunFunc /*func*/, void* /*cookie*/)
   {
@@ -147,7 +156,7 @@
   template<typename GenericFunc>
   struct FuncAdapterPayload
   {
-    std::string canvasId;
+    std::string canvasCssSelector;
     void* userData;
     GenericFunc callback;
   };
@@ -158,7 +167,6 @@
     EM_BOOL OnEventAdapterFunc(
       int eventType, const EmscriptenEvent* emEvent, void* userData)
   {
-
     // userData is OnMouseWheelFuncAdapterPayload
     FuncAdapterPayload<GenericFunc>* payload =
       reinterpret_cast<FuncAdapterPayload<GenericFunc>*>(userData);
@@ -170,7 +178,7 @@
 
     GuiAdapterEvent guiEvent;
     ConvertFromPlatform(guiEvent, eventType, *emEvent);
-    bool ret = (*(payload->callback))(payload->canvasId, &guiEvent, payload->userData);
+    bool ret = (*(payload->callback))(payload->canvasCssSelector, &guiEvent, payload->userData);
     return static_cast<EM_BOOL>(ret);
   }
 
@@ -186,7 +194,7 @@
 
     GuiAdapterEvent guiEvent;
     ConvertFromPlatform(guiEvent, *wheelEvent);
-    bool ret = (*(payload->callback))(payload->canvasId, &guiEvent, payload->userData);
+    bool ret = (*(payload->callback))(payload->canvasCssSelector, &guiEvent, payload->userData);
     return static_cast<EM_BOOL>(ret);
   }
 
@@ -202,6 +210,76 @@
     return static_cast<EM_BOOL>(ret);
   }
 
+  /*
+  
+  Explanation
+  ===========
+
+  - in "older" Emscripten, where DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR doesn't exist or is set to 0,
+    the following strings need to be used to register events:
+    - for canvas, the canvas DOM id. In case of <canvas id="mycanvas1" width='640' ...></canvas>", the string needs 
+      to be "mycanvas"
+    - for the window (for key events), the string needs to be "#window"
+  - in newer Emscripten where DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR==1 (or maybe is not there anymore, in the
+    future as of 2020-04-20)
+    - for canvas, the canvas DOM id. In case of <canvas id="mycanvas1" width='640' ...></canvas>", the string needs
+      to be "#mycanvas"  (notice the "number sign", aka "hash", NOT AKA "sharp", as can be read on https://en.wikipedia.org/wiki/Number_sign)
+    - for the window (for key events), the string needs to be EMSCRIPTEN_EVENT_TARGET_WINDOW. I do not mean 
+      "EMSCRIPTEN_EVENT_TARGET_WINDOW", but the #define EMSCRIPTEN_EVENT_TARGET_WINDOW         ((const char*)2) that
+      can be found in emscripten/html5.h
+
+  The code below converts the input canvasId (as in the old emscripten) to the emscripten-compliant one, with the
+  following compile condition : #if DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR == 1
+
+  If the DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR build parameter disappears, you might want to refactor this code
+  or continue to pass the DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR compile macro (which is different from the CMake
+  variable)     
+
+  What we are doing below:
+  - in older Emscripten, the registration functions will receive "mycanvas" and "#window" and the callbacks will receive 
+    the same std::string in their payload ("mycanvas" and "#window")
+
+  - in newer Emscripten, the registration functions will receive "#mycanvas" and EMSCRIPTEN_EVENT_TARGET_WINDOW, but 
+    the callbacks will receive "#mycanvas" and "#window" (since it is not possible to store the EMSCRIPTEN_EVENT_TARGET_WINDOW
+    magic value in an std::string, while we still want the callback to be able to change its behavior according to the
+    target element.
+  
+  */
+
+  void convertElementTarget(const char*& outCanvasCssSelectorSz, std::string& outCanvasCssSelector, const std::string& canvasId)
+  {
+    // only "#window" can start with a #
+    if (canvasId[0] == '#')
+    {
+      ORTHANC_ASSERT(canvasId == "#window");
+    }
+#if DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR == 1
+    if (canvasId == "#window")
+    {
+      // we store this in the payload so that the callback can 
+      outCanvasCssSelector = "#window";
+      outCanvasCssSelectorSz = EMSCRIPTEN_EVENT_TARGET_WINDOW;
+    }
+    else
+    {
+      outCanvasCssSelector = "#" + canvasId;
+      outCanvasCssSelectorSz = outCanvasCssSelector.c_str();
+    }
+#else
+    if (canvasId == "#window")
+    {
+      // we store this in the payload so that the callback can 
+      outCanvasCssSelector = "#window";
+      outCanvasCssSelectorSz = outCanvasCssSelector.c_str();;
+    }
+    else
+    {
+      outCanvasCssSelector = canvasId;
+      outCanvasCssSelectorSz = outCanvasCssSelector.c_str();;
+    }
+#endif
+  }
+
   // resize: (const char* target, void* userData, EM_BOOL useCapture, em_ui_callback_func callback)
   template<
     typename GenericFunc,
@@ -212,24 +290,26 @@
       EmscriptenSetCallbackFunc emFunc,
       std::string canvasId, void* userData, bool capture, GenericFunc func)
   {
-    // TODO: write RemoveCallback with an int id that gets returned from
-    // here
-    FuncAdapterPayload<GenericFunc>* payload =
-      new FuncAdapterPayload<GenericFunc>();
-    std::unique_ptr<FuncAdapterPayload<GenericFunc> > payloadP(payload);
-    payload->canvasId = canvasId;
+    std::string canvasCssSelector;
+    const char* canvasCssSelectorSz = NULL;
+    convertElementTarget(canvasCssSelectorSz, canvasCssSelector, canvasId);
+
+    // TODO: write RemoveCallback with an int id that gets returned from here
+
+    // create userdata payload
+    std::unique_ptr<FuncAdapterPayload<GenericFunc> > payload(new FuncAdapterPayload<GenericFunc>());
+    payload->canvasCssSelector = canvasCssSelector;
     payload->callback = func;
     payload->userData = userData;
-    void* userDataRaw = reinterpret_cast<void*>(payload);
-    // LOG(INFO) << "SetCallback -- userDataRaw: " << userDataRaw <<
-    //   " payload: " << payload << " payload->userData: " << payload->userData;
+    void* userDataRaw = reinterpret_cast<void*>(payload.release());
+
+    // call the registration function
     (*emFunc)(
-      canvasId.c_str(),
+      canvasCssSelectorSz,
       userDataRaw,
       static_cast<EM_BOOL>(capture),
       &OnEventAdapterFunc<GenericFunc, GuiAdapterEvent, EmscriptenEvent>,
       EM_CALLBACK_THREAD_CONTEXT_CALLING_THREAD);
-    payloadP.release();
   }
 
   template<
@@ -241,15 +321,22 @@
       EmscriptenSetCallbackFunc emFunc,
       std::string canvasId, void* userData, bool capture, GenericFunc func)
   {
-    std::unique_ptr<FuncAdapterPayload<GenericFunc> > payload(
-      new FuncAdapterPayload<GenericFunc>()
-    );
-    payload->canvasId = canvasId;
+    std::string canvasCssSelector;
+    const char* canvasCssSelectorSz = NULL;
+    convertElementTarget(canvasCssSelectorSz, canvasCssSelector, canvasId);
+
+    // TODO: write RemoveCallback with an int id that gets returned from here
+
+    // create userdata payload
+    std::unique_ptr<FuncAdapterPayload<GenericFunc> > payload(new FuncAdapterPayload<GenericFunc>());
+    payload->canvasCssSelector = canvasCssSelector;
     payload->callback = func;
     payload->userData = userData;
     void* userDataRaw = reinterpret_cast<void*>(payload.release());
+
+    // call the registration function
     (*emFunc)(
-      canvasId.c_str(),
+      canvasCssSelectorSz,
       userDataRaw,
       static_cast<EM_BOOL>(capture),
       &OnEventAdapterFunc2<GenericFunc, GuiAdapterEvent, EmscriptenEvent>,
@@ -263,11 +350,10 @@
       EmscriptenSetCallbackFunc emFunc,
       void* userData, GenericFunc func)
   {
-    // LOG(ERROR) << "SetAnimationFrameCallback !!!!!! (RequestAnimationFrame)";
     std::unique_ptr<FuncAdapterPayload<GenericFunc> > payload(
       new FuncAdapterPayload<GenericFunc>()
     );
-    payload->canvasId = "UNDEFINED";
+    payload->canvasCssSelector = "UNDEFINED";
     payload->callback = func;
     payload->userData = userData;
     void* userDataRaw = reinterpret_cast<void*>(payload.release());
@@ -358,6 +444,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,13 +456,11 @@
       capture,
       func);
   }
+#endif
 
   void GuiAdapter::RequestAnimationFrame(
     OnAnimationFrameFunc func, void* userData)
   {
-    // LOG(ERROR) << "-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+";
-    // LOG(ERROR) << "RequestAnimationFrame";
-    // LOG(ERROR) << "-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+";
     SetAnimationFrameCallback<OnAnimationFrameFunc>(
       &emscripten_request_animation_frame_loop,
       userData,
@@ -393,10 +479,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 +603,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 +660,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 +688,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 +890,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 +924,31 @@
   }
 # endif
 
+#if 0
+  // TODO: remove this when generic sdl event handlers are implemented in 
+  // the DoseView
+  // 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 DoseView 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 +967,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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Applications/Generic/GuiAdapter.h	Wed Apr 22 14:05:47 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,11 @@
 
 namespace OrthancStone
 {
+#if ORTHANC_ENABLE_SDL == 1
+  class SdlViewport;
+#endif
+
+#if 0
 
   /**
   This interface is used to store the widgets that are controlled by the 
@@ -64,8 +69,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 +109,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
 
@@ -180,6 +202,7 @@
   };
 
   std::ostream& operator<<(std::ostream& os, const GuiAdapterKeyboardEvent& event);
+  std::ostream& operator<<(std::ostream& os, const GuiAdapterMouseEvent& event);
 
   /*
     Mousedown event trigger when either the left or right (or middle) mouse is pressed 
@@ -224,11 +247,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);
@@ -236,14 +255,14 @@
     }
 
     /**
-      emscripten_set_resize_callback("#window", NULL, false, OnWindowResize);
+      emscripten_set_resize_callback("EMSCRIPTEN_EVENT_TARGET_WINDOW", NULL, false, OnWindowResize);
 
-      emscripten_set_wheel_callback("mycanvas1", widget1_.get(), false, OnXXXMouseWheel);
-      emscripten_set_wheel_callback("mycanvas2", widget2_.get(), false, OnXXXMouseWheel);
-      emscripten_set_wheel_callback("mycanvas3", widget3_.get(), false, OnXXXMouseWheel);
+      emscripten_set_wheel_callback("#mycanvas1", widget1_.get(), false, OnXXXMouseWheel);
+      emscripten_set_wheel_callback("#mycanvas2", widget2_.get(), false, OnXXXMouseWheel);
+      emscripten_set_wheel_callback("#mycanvas3", widget3_.get(), false, OnXXXMouseWheel);
 
-      emscripten_set_keydown_callback("#window", NULL, false, OnKeyDown);
-      emscripten_set_keyup_callback("#window", NULL, false, OnKeyUp);
+      emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, OnKeyDown); ---> NO!
+      emscripten_set_keyup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, OnKeyUp);
 
       emscripten_request_animation_frame_loop(OnAnimationFrame, NULL);
     
@@ -252,27 +271,33 @@
 
     */
 
-    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
+    // (this special string is converted to EMSCRIPTEN_EVENT_TARGET_WINDOW in DOM, when DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1)
+    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 +305,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 +324,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 +341,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 +364,6 @@
     */
     void OnMouseWheelEvent(uint32_t windowID, const GuiAdapterWheelEvent& event);
 
-    boost::shared_ptr<IGuiAdapterWidget> GetWidgetFromWindowId();
-
 #endif
 
     /**
--- a/Applications/Generic/NativeStoneApplicationContext.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ b/Applications/Generic/NativeStoneApplicationContext.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Applications/Generic/NativeStoneApplicationContext.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Applications/Generic/NativeStoneApplicationRunner.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Applications/Generic/NativeStoneApplicationRunner.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Applications/IStoneApplication.h	Wed Apr 22 14:05:47 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/BasicPetCtFusionApplication.h	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,202 +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 "SampleInteractor.h"
-
-#include <Core/Logging.h>
-
-namespace OrthancStone
-{
-  namespace Samples
-  {
-    class BasicPetCtFusionApplication : public SampleApplicationBase
-    {
-    private:
-      class Interactor : public SampleInteractor
-      {
-      public:
-        static void SetStyle(LayeredSceneWidget& widget,
-                             bool ct,
-                             bool pet)
-        {
-          if (ct)
-          {
-            RenderStyle style;
-            style.windowing_ = ImageWindowing_Bone;
-            widget.SetLayerStyle(0, style);
-          }
-          else
-          {
-            RenderStyle style;
-            style.visible_ = false;
-            widget.SetLayerStyle(0, style);
-          }
-
-          if (ct && pet)
-          {
-            RenderStyle style;
-            style.applyLut_ = true;
-            style.alpha_ = 0.5;
-            widget.SetLayerStyle(1, style);
-          }
-          else if (pet)
-          {
-            RenderStyle style;
-            style.applyLut_ = true;
-            widget.SetLayerStyle(1, style);
-          }
-          else
-          {
-            RenderStyle style;
-            style.visible_ = false;
-            widget.SetLayerStyle(1, style);
-          }
-        }
-
-
-        static bool IsVisible(LayeredSceneWidget& widget,
-                              size_t layer)
-        {
-          RenderStyle style = widget.GetLayerStyle(layer);
-          return style.visible_;
-        }
-
-
-        static void ToggleInterpolation(LayeredSceneWidget& widget,
-                                        size_t layer)
-        {
-          RenderStyle style = widget.GetLayerStyle(layer);
-         
-          if (style.interpolation_ == ImageInterpolation_Bilinear)
-          {
-            style.interpolation_ = ImageInterpolation_Nearest;
-          }
-          else
-          {
-            style.interpolation_ = ImageInterpolation_Bilinear;
-          }
-
-          widget.SetLayerStyle(layer, style);
-        }
-
-
-        Interactor(VolumeImage& volume,
-                   VolumeProjection projection, 
-                   bool reverse) :
-          SampleInteractor(volume, projection, reverse)
-        {
-        }
-
-
-        virtual void KeyPressed(WorldSceneWidget& widget,
-                                char key,
-                                KeyboardModifiers modifiers,
-                                IStatusBar* statusBar)
-        {
-          LayeredSceneWidget& layered = dynamic_cast<LayeredSceneWidget&>(widget);
-
-          switch (key)
-          {
-            case 'c':
-              // Toggle the visibility of the CT layer
-              SetStyle(layered, !IsVisible(layered, 0), IsVisible(layered, 1));
-              break;
-
-            case 'p':
-              // Toggle the visibility of the PET layer
-              SetStyle(layered, IsVisible(layered, 0), !IsVisible(layered, 1));
-              break;
-
-            case 'i':
-            {
-              // Toggle on/off the interpolation
-              ToggleInterpolation(layered, 0);
-              ToggleInterpolation(layered, 1);
-              break;
-            }
-
-            default:
-              break;
-          }
-        }
-      };
-
-
-    public:
-      virtual void DeclareCommandLineOptions(boost::program_options::options_description& options)
-      {
-        boost::program_options::options_description generic("Sample options");
-        generic.add_options()
-          ("ct", boost::program_options::value<std::string>(), 
-           "Orthanc ID of the CT series")
-          ("pet", boost::program_options::value<std::string>(), 
-           "Orthanc ID of the PET series")
-          ("threads", boost::program_options::value<unsigned int>()->default_value(3), 
-           "Number of download threads for the CT series")
-          ;
-
-        options.add(generic);    
-      }
-
-      virtual void Initialize(BasicApplicationContext& context,
-                              IStatusBar& statusBar,
-                              const boost::program_options::variables_map& parameters)
-      {
-        using namespace OrthancStone;
-
-        if (parameters.count("ct") != 1 ||
-            parameters.count("pet") != 1)
-        {
-          LOG(ERROR) << "The series ID is missing";
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-        }
-
-        std::string ct = parameters["ct"].as<std::string>();
-        std::string pet = parameters["pet"].as<std::string>();
-        unsigned int threads = parameters["threads"].as<unsigned int>();
-
-        VolumeImage& ctVolume = context.AddSeriesVolume(ct, true /* progressive download */, threads);
-        VolumeImage& petVolume = context.AddSeriesVolume(pet, true /* progressive download */, 1);
-
-        // Take the PET volume as the reference for the slices
-        std::unique_ptr<Interactor> interactor(new Interactor(petVolume, VolumeProjection_Axial, false /* don't reverse normal */));
-
-        std::unique_ptr<LayeredSceneWidget> widget(new LayeredSceneWidget);
-        widget->AddLayer(new VolumeImage::LayerFactory(ctVolume));
-        widget->AddLayer(new VolumeImage::LayerFactory(petVolume));
-        widget->SetSlice(interactor->GetCursor().GetCurrentSlice());
-        widget->SetInteractor(*interactor);
-
-        Interactor::SetStyle(*widget, true, true);   // Initially, show both CT and PET layers
-
-        context.AddInteractor(interactor.release());
-        context.SetCentralWidget(widget.release());
-
-        statusBar.SetMessage("Use the key \"t\" to toggle the fullscreen mode");
-        statusBar.SetMessage("Use the key \"c\" to show/hide the CT layer");
-        statusBar.SetMessage("Use the key \"p\" to show/hide the PET layer");
-        statusBar.SetMessage("Use the key \"i\" to toggle the smoothing of the images");
-      }
-    };
-  }
-}
--- a/Applications/Samples/CMakeLists.txt	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,292 +0,0 @@
-# Usage (Linux):
-# to build the WASM samples
-# source ~/Downloads/emsdk/emsdk_env.sh && cmake -DCMAKE_TOOLCHAIN_FILE=${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_BUILD_TYPE=Release -DSTONE_SOURCES_DIR=$currentDir/../../../orthanc-stone -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT=$currentDir/../../../orthanc -DALLOW_DOWNLOADS=ON .. -DENABLE_WASM=ON
-# to build the Qt samples
-
-cmake_minimum_required(VERSION 2.8.3)
-project(OrthancStone)
-
-include(../../Resources/CMake/OrthancStoneParameters.cmake)
-
-set(ENABLE_STONE_DEPRECATED ON)  # Need deprecated classes for these samples
-set(EMSCRIPTEN_SET_LLVM_WASM_BACKEND ON)
-
-include(${ORTHANC_ROOT}/Resources/CMake/DownloadPackage.cmake)
-DownloadPackage(
-  "a24b8136b8f3bb93f166baf97d9328de"
-  "http://orthanc.osimis.io/ThirdPartyDownloads/ubuntu-font-family-0.83.zip"
-  "${CMAKE_BINARY_DIR}/ubuntu-font-family-0.83")
-
-set(ORTHANC_STONE_APPLICATION_RESOURCES
-  UBUNTU_FONT  ${CMAKE_BINARY_DIR}/ubuntu-font-family-0.83/Ubuntu-R.ttf
-  )
-
-if (OPENSSL_NO_CAPIENG)
-add_definitions(-DOPENSSL_NO_CAPIENG=1)
-endif()
-
-
-# the following block has been borrowed from orthanc/**/Compiler.cmake
-if (MSVC_MULTIPLE_PROCESSES)
-# "If you omit the processMax argument in the /MP option, the
-# compiler obtains the number of effective processors from the
-# operating system, and then creates one process per effective
-# processor"
-# https://blog.kitware.com/cmake-building-with-all-your-cores/
-# https://docs.microsoft.com/en-us/cpp/build/reference/mp-build-with-multiple-processes
-set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /MP")
-set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP")
-endif()
-
-#set(ENABLE_DCMTK ON)
-
-set(ENABLE_SDL OFF CACHE BOOL "Target SDL Native application")
-set(ENABLE_QT OFF CACHE BOOL "Target Qt Native application")
-set(ENABLE_WASM OFF CACHE BOOL "Target WASM application")
-
-if (ENABLE_WASM)
-  #####################################################################
-  ## Configuration of the Emscripten compiler for WebAssembly target
-  #####################################################################
-
-  set(WASM_FLAGS "-s WASM=1")
-  set(WASM_FLAGS "${WASM_FLAGS} -s STRICT=1") # drops support for all deprecated build options
-  set(WASM_FLAGS "${WASM_FLAGS} -s FILESYSTEM=1") # if we don't include it, gen_uuid.c fails to build because srand, getpid(), ... are not defined
-  set(WASM_FLAGS "${WASM_FLAGS} -s DISABLE_EXCEPTION_CATCHING=0") # actually enable exception catching 
-  set(WASM_FLAGS "${WASM_FLAGS} -s ERROR_ON_MISSING_LIBRARIES=1")
-  
-  if (CMAKE_BUILD_TYPE MATCHES DEBUG)
-    set(WASM_FLAGS "${WASM_FLAGS} -g4") # generate debug information
-    set(WASM_FLAGS "${WASM_FLAGS} -s ASSERTIONS=2") # more runtime checks
-  else()
-    set(WASM_FLAGS "${WASM_FLAGS} -Os") # optimize for web (speed and size)
-  endif()
-
-  set(WASM_MODULE_NAME "StoneFrameworkModule" CACHE STRING "Name of the WebAssembly module")
-
-  set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WASM_FLAGS}")
-  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WASM_FLAGS}")
-
-  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${WASM_FLAGS}")  # not always clear which flags are for the compiler and which one are for the linker -> pass them all to the linker too
-  # set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --js-library ${STONE_SOURCES_DIR}/Applications/Samples/samples-library.js")
-  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/WasmWebService.js")
-  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/WasmDelayedCallExecutor.js")
-  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/default-library.js")
-  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\"]'")
-  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s EXPORT_NAME='\"${WASM_MODULE_NAME}\"'")
-  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1")
-  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s TOTAL_MEMORY=536870912")
-  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s TOTAL_STACK=128000000")
-
-  add_definitions(-DORTHANC_ENABLE_WASM=1)
-  set(ORTHANC_SANDBOXED ON)
-
-elseif (ENABLE_QT OR ENABLE_SDL)
-
-  set(ENABLE_NATIVE ON)
-  set(ORTHANC_SANDBOXED OFF)
-  set(ENABLE_CRYPTO_OPTIONS ON)
-  set(ENABLE_GOOGLE_TEST ON)
-  set(ENABLE_WEB_CLIENT ON)
-
-else()
-  set(ENABLE_NATIVE ON)
-  set(ENABLE_OPENGL OFF)
-  
-endif()
-
-
-#####################################################################
-## Configuration for Orthanc
-#####################################################################
-
-# include(../../Resources/CMake/Version.cmake)
-
-if (ORTHANC_STONE_VERSION STREQUAL "mainline")
-  set(ORTHANC_FRAMEWORK_VERSION "mainline")
-  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
-else()
-  set(ORTHANC_FRAMEWORK_VERSION "1.4.1")
-  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
-endif()
-
-set(ORTHANC_FRAMEWORK_SOURCE "${ORTHANC_FRAMEWORK_DEFAULT_SOURCE}" CACHE STRING "Source of the Orthanc source code (can be \"hg\", \"archive\", \"web\" or \"path\")")
-set(ORTHANC_FRAMEWORK_ARCHIVE "" CACHE STRING "Path to the Orthanc archive, if ORTHANC_FRAMEWORK_SOURCE is \"archive\"")
-set(ORTHANC_FRAMEWORK_ROOT "" CACHE STRING "Path to the Orthanc source directory, if ORTHANC_FRAMEWORK_SOURCE is \"path\"")
-
-add_definitions(
-  -DORTHANC_ENABLE_LOGGING_PLUGIN=0
-  )
-
-
-#####################################################################
-## Build a static library containing the Orthanc Stone framework
-#####################################################################
-
-
-LIST(APPEND ORTHANC_BOOST_COMPONENTS program_options)
-
-include(../../Resources/CMake/OrthancStoneConfiguration.cmake)
-
-add_library(OrthancStone STATIC
-  ${ORTHANC_STONE_SOURCES}
-  )
-
-#####################################################################
-## Build all the sample applications
-#####################################################################
-
-include_directories(${ORTHANC_STONE_ROOT})
-
-# files common to all samples
-list(APPEND SAMPLE_APPLICATIONS_SOURCES
-  ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleInteractor.h
-  ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleApplicationBase.h
-  )
-
-if (ENABLE_QT)
-  list(APPEND SAMPLE_APPLICATIONS_SOURCES
-    ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleQtApplicationRunner.h
-    ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindow.cpp
-    ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindowWithButtons.cpp
-    )
-
-  ORTHANC_QT_WRAP_UI(SAMPLE_APPLICATIONS_SOURCES
-    ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindow.ui
-    ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindowWithButtons.ui
-    )
-
-  ORTHANC_QT_WRAP_CPP(SAMPLE_APPLICATIONS_SOURCES
-    ${ORTHANC_STONE_ROOT}/Applications/Qt/QCairoWidget.h
-    ${ORTHANC_STONE_ROOT}/Applications/Qt/QStoneMainWindow.h
-    ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindow.h
-    ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindowWithButtons.h
-    )
-endif()
-
-if (ENABLE_NATIVE)
-  list(APPEND SAMPLE_APPLICATIONS_SOURCES
-    ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleMainNative.cpp
-    )
-
-elseif (ENABLE_WASM)
-
-  list(APPEND SAMPLE_APPLICATIONS_SOURCES
-    ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleMainWasm.cpp
-    ${STONE_WASM_SOURCES}
-    )
-endif()
-
-
-macro(BuildSingleFileSample Target Header Sample)
-  add_executable(${Target}
-    ${ORTHANC_STONE_ROOT}/Applications/Samples/${Header}
-    ${SAMPLE_APPLICATIONS_SOURCES}
-    )
-  set_target_properties(${Target} PROPERTIES COMPILE_DEFINITIONS ORTHANC_STONE_SAMPLE=${Sample})
-  target_link_libraries(${Target} OrthancStone)
-endmacro()
-
-
-if (ENABLE_SDL)
-  #BuildSingleFileSample(OrthancStoneEmpty EmptyApplication.h 1)
-  #BuildSingleFileSample(OrthancStoneTestPattern TestPatternApplication.h 2)
-  BuildSingleFileSample(OrthancStoneSingleFrame SingleFrameApplication.h 3)
-  #BuildSingleFileSample(OrthancStoneSingleVolume SingleVolumeApplication.h 4)
-  #BuildSingleFileSample(OrthancStoneBasicPetCtFusion 5)
-  #BuildSingleFileSample(OrthancStoneSynchronizedSeries 6)
-  #BuildSingleFileSample(OrthancStoneLayoutPetCtFusion 7)
-  BuildSingleFileSample(OrthancStoneSimpleViewerSingleFile SimpleViewerApplicationSingleFile.h 8)  # we keep that one just as a sample before we convert another sample to this pattern
-  BuildSingleFileSample(OrthancStoneSingleFrameEditor SingleFrameEditorApplication.h 9)
-endif()
-  
-##### SimpleViewer sample (Qt and WASM only) #######
-
-if (ENABLE_QT OR ENABLE_WASM)
-
-    if (ENABLE_QT)
-      list(APPEND SIMPLE_VIEWER_APPLICATION_SOURCES
-        ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.cpp
-        ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.ui
-        ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Qt/mainQt.cpp
-        )
-
-      ORTHANC_QT_WRAP_UI(SIMPLE_VIEWER_APPLICATION_SOURCES
-        ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.ui
-        )
-
-      ORTHANC_QT_WRAP_CPP(SIMPLE_VIEWER_APPLICATION_SOURCES
-        ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.h
-        )
-
-elseif (ENABLE_WASM)
-        list(APPEND SIMPLE_VIEWER_APPLICATION_SOURCES
-            ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Wasm/mainWasm.cpp
-            ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Wasm/SimpleViewerWasmApplicationAdapter.cpp
-            ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Wasm/SimpleViewerWasmApplicationAdapter.h
-            ${STONE_WASM_SOURCES}
-          )
-    endif()
-
-    add_executable(OrthancStoneSimpleViewer
-      ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/AppStatus.h
-      ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/MainWidgetInteractor.cpp
-      ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/MainWidgetInteractor.h
-      ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/SimpleViewerApplication.cpp
-      ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/SimpleViewerApplication.h
-      ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/ThumbnailInteractor.cpp
-      ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/ThumbnailInteractor.h
-      ${SIMPLE_VIEWER_APPLICATION_SOURCES}
-      )
-    target_link_libraries(OrthancStoneSimpleViewer OrthancStone)
-
-    BuildSingleFileSample(OrthancStoneSingleFrameEditor SingleFrameEditorApplication.h 9)
-endif()
-
-#####################################################################
-## Build the unit tests
-#####################################################################
-
-if (ENABLE_NATIVE)
-  add_executable(UnitTests
-    ${GOOGLE_TEST_SOURCES}
-    ${ORTHANC_STONE_ROOT}/UnitTestsSources/GenericToolboxTests.cpp
-    ${ORTHANC_STONE_ROOT}/UnitTestsSources/ImageToolboxTests.cpp
-    ${ORTHANC_STONE_ROOT}/UnitTestsSources/PixelTestPatternsTests.cpp
-    ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestCommands.cpp
-    ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestMessageBroker.cpp
-    ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestStrategy.cpp
-    ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestStructureSet.cpp
-    ${ORTHANC_STONE_ROOT}/UnitTestsSources/UnitTestsMain.cpp
-    )
-
-  target_link_libraries(UnitTests OrthancStone)
-
-  add_custom_command(
-    TARGET UnitTests
-    POST_BUILD
-    COMMAND ${CMAKE_COMMAND} -E copy
-      "${ORTHANC_STONE_ROOT}/UnitTestsSources/72c773ac-5059f2c4-2e6a9120-4fd4bca1-45701661.json" 
-      "$<TARGET_FILE_DIR:UnitTests>/72c773ac-5059f2c4-2e6a9120-4fd4bca1-45701661.json"
-    )
-
-endif()
-
-#####################################################################
-## Generate the documentation if Doxygen is present
-#####################################################################
-
-find_package(Doxygen)
-if (DOXYGEN_FOUND)
-  configure_file(
-    ${ORTHANC_STONE_ROOT}/Resources/OrthancStone.doxygen
-    ${CMAKE_CURRENT_BINARY_DIR}/OrthancStone.doxygen
-    @ONLY)
-
-  add_custom_target(doc
-    ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/OrthancStone.doxygen
-    COMMENT "Generating documentation with Doxygen" VERBATIM
-    )
-else()
-  message("Doxygen not found. The documentation will not be built.")
-endif()
--- a/Applications/Samples/CMakeLists.txt.old	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,248 +0,0 @@
-# Usage: see README file
-
-cmake_minimum_required(VERSION 2.8.3)
-
-# Automatically link Qt executables to qtmain target on Windows
-# ("OLD" == do not link)
-if(POLICY CMP0020)
-  cmake_policy(SET CMP0020 OLD)
-endif()
-
-# Only interpret if() arguments as variables or keywords when unquoted.
-# NEW = do NOT dereference *quoted* variables
-if(POLICY CMP0054)
-  cmake_policy(SET CMP0054 NEW)
-endif()
-
-project(OrthancStone)
-
-include(../../Resources/CMake/OrthancStoneParameters.cmake)
-
-#set(ENABLE_DCMTK ON)
-
-set(ENABLE_SDL OFF CACHE BOOL "Target SDL Native application")
-set(ENABLE_QT OFF CACHE BOOL "Target Qt Native application")
-set(ENABLE_WASM OFF CACHE BOOL "Target WASM application")
-
-# TODO: replace or compute STONE_SOURCES_DIR from CMAKE_CURRENT_LIST_FILE
-
-if (ENABLE_WASM)
-  #####################################################################
-  ## Configuration of the Emscripten compiler for WebAssembly target
-  #####################################################################
-
-  set(WASM_FLAGS "-s WASM=1 -O0 -g0")
-  message("*****************************************************************************")
-  message("WARNING: optimizations are disabled in emcc!!! Enable them for production use")
-  message("*****************************************************************************")
-  set(WASM_MODULE_NAME "StoneFrameworkModule" CACHE STRING "Name of the WebAssembly module")
-  set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WASM_FLAGS}")
-  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WASM_FLAGS}")
-  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/WasmWebService.js --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/WasmDelayedCallExecutor.js --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/default-library.js  -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\"]'")
-
-  # Handling of memory
-  #set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1")  # Resize
-  #set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s TOTAL_MEMORY=536870912")  # 512MB
-  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s EXPORT_NAME='\"${WASM_MODULE_NAME}\"' -s ALLOW_MEMORY_GROWTH=1 -s TOTAL_MEMORY=536870912 -s TOTAL_STACK=128000000")  # 512MB + resize
-  #set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1 -s TOTAL_MEMORY=1073741824")  # 1GB + resize
-
-  # To debug exceptions
-  #set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s DEMANGLE_SUPPORT=1 -s ASSERTIONS=2")
-
-  add_definitions(-DORTHANC_ENABLE_WASM=1)
-  set(ORTHANC_SANDBOXED ON)
-
-elseif (ENABLE_QT OR ENABLE_SDL)
-
-  set(ENABLE_NATIVE ON)
-  set(ORTHANC_SANDBOXED OFF)
-  set(ENABLE_CRYPTO_OPTIONS ON)
-  set(ENABLE_GOOGLE_TEST ON)
-  set(ENABLE_WEB_CLIENT ON)
-
-endif()
-
-#####################################################################
-## Configuration for Orthanc
-#####################################################################
-
-# include(../../Resources/CMake/Version.cmake)
-
-if (ORTHANC_STONE_VERSION STREQUAL "mainline")
-  set(ORTHANC_FRAMEWORK_VERSION "mainline")
-  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
-else()
-  set(ORTHANC_FRAMEWORK_VERSION "1.4.1")
-  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
-endif()
-
-set(ORTHANC_FRAMEWORK_SOURCE "${ORTHANC_FRAMEWORK_DEFAULT_SOURCE}" CACHE STRING "Source of the Orthanc source code (can be \"hg\", \"archive\", \"web\" or \"path\")")
-set(ORTHANC_FRAMEWORK_ARCHIVE "" CACHE STRING "Path to the Orthanc archive, if ORTHANC_FRAMEWORK_SOURCE is \"archive\"")
-set(ORTHANC_FRAMEWORK_ROOT "" CACHE STRING "Path to the Orthanc source directory, if ORTHANC_FRAMEWORK_SOURCE is \"path\"")
-
-#####################################################################
-## Build a static library containing the Orthanc Stone framework
-#####################################################################
-
-LIST(APPEND ORTHANC_BOOST_COMPONENTS program_options)
-
-include(../../Resources/CMake/OrthancStoneConfiguration.cmake)
-
-add_library(OrthancStone STATIC
-  ${ORTHANC_STONE_SOURCES}
-  )
-
-#####################################################################
-## Build all the sample applications
-#####################################################################
-
-include_directories(${ORTHANC_STONE_ROOT})
-
-# files common to all samples
-list(APPEND SAMPLE_APPLICATIONS_SOURCES
-  ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleInteractor.h
-  ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleApplicationBase.h
-  )
-
-if (ENABLE_QT)
-  list(APPEND SAMPLE_APPLICATIONS_SOURCES
-    ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleQtApplicationRunner.h
-    ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindow.cpp
-    ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindowWithButtons.cpp
-    )
-
-  ORTHANC_QT_WRAP_UI(SAMPLE_APPLICATIONS_SOURCES
-    ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindow.ui
-    ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindowWithButtons.ui
-    )
-
-  ORTHANC_QT_WRAP_CPP(SAMPLE_APPLICATIONS_SOURCES
-    ${ORTHANC_STONE_ROOT}/Applications/Qt/QCairoWidget.h
-    ${ORTHANC_STONE_ROOT}/Applications/Qt/QStoneMainWindow.h
-    ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindow.h
-    ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindowWithButtons.h
-    )
-endif()
-
-if (ENABLE_NATIVE)
-  list(APPEND SAMPLE_APPLICATIONS_SOURCES
-    ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleMainNative.cpp
-    )
-
-elseif (ENABLE_WASM)
-
-  list(APPEND SAMPLE_APPLICATIONS_SOURCES
-    ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleMainWasm.cpp
-    ${STONE_WASM_SOURCES}
-    )
-endif()
-
-
-macro(BuildSingleFileSample Target Header Sample)
-  add_executable(${Target}
-    ${ORTHANC_STONE_ROOT}/Applications/Samples/${Header}
-    ${SAMPLE_APPLICATIONS_SOURCES}
-    )
-  set_target_properties(${Target} PROPERTIES COMPILE_DEFINITIONS ORTHANC_STONE_SAMPLE=${Sample})
-  target_link_libraries(${Target} OrthancStone)
-  
-  if (ENABLE_QT AND (CMAKE_SYSTEM_NAME STREQUAL "Windows"))
-    message("(ENABLE_QT and (CMAKE_SYSTEM_NAME matches \"Windows\")) is true")
-    add_custom_command(
-      TARGET ${Target} POST_BUILD
-      COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:Qt5::Core> $<TARGET_FILE_DIR:${Target}>
-      COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:Qt5::Widgets> $<TARGET_FILE_DIR:${Target}>
-      COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:Qt5::Gui> $<TARGET_FILE_DIR:${Target}>
-    )
-  endif()
-endmacro()
-
-#BuildSingleFileSample(OrthancStoneEmpty EmptyApplication.h 1)
-#BuildSingleFileSample(OrthancStoneTestPattern TestPatternApplication.h 2)
-BuildSingleFileSample(OrthancStoneSingleFrame SingleFrameApplication.h 3)
-#BuildSingleFileSample(OrthancStoneSingleVolume SingleVolumeApplication.h 4)
-#BuildSingleFileSample(OrthancStoneBasicPetCtFusion 5)
-#BuildSingleFileSample(OrthancStoneSynchronizedSeries 6)
-#BuildSingleFileSample(OrthancStoneLayoutPetCtFusion 7)
-BuildSingleFileSample(OrthancStoneSimpleViewerSingleFile SimpleViewerApplicationSingleFile.h 8)  # we keep that one just as a sample before we convert another sample to this pattern
-BuildSingleFileSample(OrthancStoneSingleFrameEditor SingleFrameEditorApplication.h 9)
-
-##### SimpleViewer sample (Qt and WASM only) #######
-
-if (ENABLE_QT OR ENABLE_WASM)
-
-      # GenerateCodeFromFlatBufferSchema("${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/ApplicationCommands.fbs")
-
-      list(APPEND SIMPLE_VIEWER_APPLICATION_SOURCES ${FLATC_AUTOGENERATED_SOURCES})
-      message(STATUS "SIMPLE_VIEWER_APPLICATION_SOURCES = ${SIMPLE_VIEWER_APPLICATION_SOURCES}")
-      message(STATUS "FLATC_AUTOGENERATED_SOURCES = ${FLATC_AUTOGENERATED_SOURCES}")
-
-    if (ENABLE_QT)
-      list(APPEND SIMPLE_VIEWER_APPLICATION_SOURCES
-        ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.cpp
-        ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.ui
-        ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Qt/mainQt.cpp
-        )
-
-      ORTHANC_QT_WRAP_UI(SIMPLE_VIEWER_APPLICATION_SOURCES
-        ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.ui
-        )
-
-      ORTHANC_QT_WRAP_CPP(SIMPLE_VIEWER_APPLICATION_SOURCES
-        ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.h
-        )
-
-elseif (ENABLE_WASM)
-        list(APPEND SIMPLE_VIEWER_APPLICATION_SOURCES
-            ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Wasm/mainWasm.cpp
-            ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Wasm/SimpleViewerWasmApplicationAdapter.cpp
-            ${STONE_WASM_SOURCES}
-          )
-    endif()
-
-    add_executable(OrthancStoneSimpleViewer
-      ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/SimpleViewerApplication.cpp
-      ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/ThumbnailInteractor.cpp
-      ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/MainWidgetInteractor.cpp
-      ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/AppStatus.h
-      ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Messages.h
-      ${SIMPLE_VIEWER_APPLICATION_SOURCES}
-      )
-    target_link_libraries(OrthancStoneSimpleViewer OrthancStone)
-
-endif()
-
-#####################################################################
-## Build the unit tests
-#####################################################################
-
-if (ENABLE_NATIVE)
-  add_executable(UnitTests
-    ${GOOGLE_TEST_SOURCES}
-    ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestCommands.cpp
-    ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestExceptions.cpp
-    ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestMessageBroker.cpp
-    ${ORTHANC_STONE_ROOT}/UnitTestsSources/UnitTestsMain.cpp
-    )
-
-  target_link_libraries(UnitTests OrthancStone)
-endif()
-
-#####################################################################
-## Generate the documentation if Doxygen is present
-#####################################################################
-
-find_package(Doxygen)
-if (DOXYGEN_FOUND)
-  configure_file(
-    ${ORTHANC_STONE_ROOT}/Resources/OrthancStone.doxygen
-    ${CMAKE_CURRENT_BINARY_DIR}/OrthancStone.doxygen
-    @ONLY)
-
-  add_custom_target(doc
-    ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/OrthancStone.doxygen
-    COMMENT "Generating documentation with Doxygen" VERBATIM
-    )
-else()
-  message("Doxygen not found. The documentation will not be built.")
-endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/BasicPetCtFusionApplication.h	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,202 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-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 "SampleInteractor.h"
+
+#include <Core/Logging.h>
+
+namespace OrthancStone
+{
+  namespace Samples
+  {
+    class BasicPetCtFusionApplication : public SampleApplicationBase
+    {
+    private:
+      class Interactor : public SampleInteractor
+      {
+      public:
+        static void SetStyle(LayeredSceneWidget& widget,
+                             bool ct,
+                             bool pet)
+        {
+          if (ct)
+          {
+            RenderStyle style;
+            style.windowing_ = ImageWindowing_Bone;
+            widget.SetLayerStyle(0, style);
+          }
+          else
+          {
+            RenderStyle style;
+            style.visible_ = false;
+            widget.SetLayerStyle(0, style);
+          }
+
+          if (ct && pet)
+          {
+            RenderStyle style;
+            style.applyLut_ = true;
+            style.alpha_ = 0.5;
+            widget.SetLayerStyle(1, style);
+          }
+          else if (pet)
+          {
+            RenderStyle style;
+            style.applyLut_ = true;
+            widget.SetLayerStyle(1, style);
+          }
+          else
+          {
+            RenderStyle style;
+            style.visible_ = false;
+            widget.SetLayerStyle(1, style);
+          }
+        }
+
+
+        static bool IsVisible(LayeredSceneWidget& widget,
+                              size_t layer)
+        {
+          RenderStyle style = widget.GetLayerStyle(layer);
+          return style.visible_;
+        }
+
+
+        static void ToggleInterpolation(LayeredSceneWidget& widget,
+                                        size_t layer)
+        {
+          RenderStyle style = widget.GetLayerStyle(layer);
+         
+          if (style.interpolation_ == ImageInterpolation_Bilinear)
+          {
+            style.interpolation_ = ImageInterpolation_Nearest;
+          }
+          else
+          {
+            style.interpolation_ = ImageInterpolation_Bilinear;
+          }
+
+          widget.SetLayerStyle(layer, style);
+        }
+
+
+        Interactor(VolumeImage& volume,
+                   VolumeProjection projection, 
+                   bool reverse) :
+          SampleInteractor(volume, projection, reverse)
+        {
+        }
+
+
+        virtual void KeyPressed(WorldSceneWidget& widget,
+                                char key,
+                                KeyboardModifiers modifiers,
+                                IStatusBar* statusBar)
+        {
+          LayeredSceneWidget& layered = dynamic_cast<LayeredSceneWidget&>(widget);
+
+          switch (key)
+          {
+            case 'c':
+              // Toggle the visibility of the CT layer
+              SetStyle(layered, !IsVisible(layered, 0), IsVisible(layered, 1));
+              break;
+
+            case 'p':
+              // Toggle the visibility of the PET layer
+              SetStyle(layered, IsVisible(layered, 0), !IsVisible(layered, 1));
+              break;
+
+            case 'i':
+            {
+              // Toggle on/off the interpolation
+              ToggleInterpolation(layered, 0);
+              ToggleInterpolation(layered, 1);
+              break;
+            }
+
+            default:
+              break;
+          }
+        }
+      };
+
+
+    public:
+      virtual void DeclareCommandLineOptions(boost::program_options::options_description& options)
+      {
+        boost::program_options::options_description generic("Sample options");
+        generic.add_options()
+          ("ct", boost::program_options::value<std::string>(), 
+           "Orthanc ID of the CT series")
+          ("pet", boost::program_options::value<std::string>(), 
+           "Orthanc ID of the PET series")
+          ("threads", boost::program_options::value<unsigned int>()->default_value(3), 
+           "Number of download threads for the CT series")
+          ;
+
+        options.add(generic);    
+      }
+
+      virtual void Initialize(BasicApplicationContext& context,
+                              IStatusBar& statusBar,
+                              const boost::program_options::variables_map& parameters)
+      {
+        using namespace OrthancStone;
+
+        if (parameters.count("ct") != 1 ||
+            parameters.count("pet") != 1)
+        {
+          LOG(ERROR) << "The series ID is missing";
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+
+        std::string ct = parameters["ct"].as<std::string>();
+        std::string pet = parameters["pet"].as<std::string>();
+        unsigned int threads = parameters["threads"].as<unsigned int>();
+
+        VolumeImage& ctVolume = context.AddSeriesVolume(ct, true /* progressive download */, threads);
+        VolumeImage& petVolume = context.AddSeriesVolume(pet, true /* progressive download */, 1);
+
+        // Take the PET volume as the reference for the slices
+        std::unique_ptr<Interactor> interactor(new Interactor(petVolume, VolumeProjection_Axial, false /* don't reverse normal */));
+
+        std::unique_ptr<LayeredSceneWidget> widget(new LayeredSceneWidget);
+        widget->AddLayer(new VolumeImage::LayerFactory(ctVolume));
+        widget->AddLayer(new VolumeImage::LayerFactory(petVolume));
+        widget->SetSlice(interactor->GetCursor().GetCurrentSlice());
+        widget->SetInteractor(*interactor);
+
+        Interactor::SetStyle(*widget, true, true);   // Initially, show both CT and PET layers
+
+        context.AddInteractor(interactor.release());
+        context.SetCentralWidget(widget.release());
+
+        statusBar.SetMessage("Use the key \"t\" to toggle the fullscreen mode");
+        statusBar.SetMessage("Use the key \"c\" to show/hide the CT layer");
+        statusBar.SetMessage("Use the key \"p\" to show/hide the PET layer");
+        statusBar.SetMessage("Use the key \"i\" to toggle the smoothing of the images");
+      }
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/CMakeLists.txt	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,292 @@
+# Usage (Linux):
+# to build the WASM samples
+# source ~/Downloads/emsdk/emsdk_env.sh && cmake -DCMAKE_TOOLCHAIN_FILE=${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_BUILD_TYPE=Release -DSTONE_SOURCES_DIR=$currentDir/../../../orthanc-stone -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT=$currentDir/../../../orthanc -DALLOW_DOWNLOADS=ON .. -DENABLE_WASM=ON
+# to build the Qt samples
+
+cmake_minimum_required(VERSION 2.8.3)
+project(OrthancStone)
+
+include(../../../Resources/CMake/OrthancStoneParameters.cmake)
+
+set(ENABLE_STONE_DEPRECATED ON)  # Need deprecated classes for these samples
+set(EMSCRIPTEN_SET_LLVM_WASM_BACKEND ON)
+
+include(${ORTHANC_ROOT}/Resources/CMake/DownloadPackage.cmake)
+DownloadPackage(
+  "a24b8136b8f3bb93f166baf97d9328de"
+  "http://orthanc.osimis.io/ThirdPartyDownloads/ubuntu-font-family-0.83.zip"
+  "${CMAKE_BINARY_DIR}/ubuntu-font-family-0.83")
+
+set(ORTHANC_STONE_APPLICATION_RESOURCES
+  UBUNTU_FONT  ${CMAKE_BINARY_DIR}/ubuntu-font-family-0.83/Ubuntu-R.ttf
+  )
+
+if (OPENSSL_NO_CAPIENG)
+add_definitions(-DOPENSSL_NO_CAPIENG=1)
+endif()
+
+
+# the following block has been borrowed from orthanc/**/Compiler.cmake
+if (MSVC_MULTIPLE_PROCESSES)
+# "If you omit the processMax argument in the /MP option, the
+# compiler obtains the number of effective processors from the
+# operating system, and then creates one process per effective
+# processor"
+# https://blog.kitware.com/cmake-building-with-all-your-cores/
+# https://docs.microsoft.com/en-us/cpp/build/reference/mp-build-with-multiple-processes
+set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /MP")
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP")
+endif()
+
+#set(ENABLE_DCMTK ON)
+
+set(ENABLE_SDL OFF CACHE BOOL "Target SDL Native application")
+set(ENABLE_QT OFF CACHE BOOL "Target Qt Native application")
+set(ENABLE_WASM OFF CACHE BOOL "Target WASM application")
+
+if (ENABLE_WASM)
+  #####################################################################
+  ## Configuration of the Emscripten compiler for WebAssembly target
+  #####################################################################
+
+  set(WASM_FLAGS "-s WASM=1")
+  set(WASM_FLAGS "${WASM_FLAGS} -s STRICT=1") # drops support for all deprecated build options
+  set(WASM_FLAGS "${WASM_FLAGS} -s FILESYSTEM=1") # if we don't include it, gen_uuid.c fails to build because srand, getpid(), ... are not defined
+  set(WASM_FLAGS "${WASM_FLAGS} -s DISABLE_EXCEPTION_CATCHING=0") # actually enable exception catching 
+  set(WASM_FLAGS "${WASM_FLAGS} -s ERROR_ON_MISSING_LIBRARIES=1")
+  
+  if (CMAKE_BUILD_TYPE MATCHES DEBUG)
+    set(WASM_FLAGS "${WASM_FLAGS} -g4") # generate debug information
+    set(WASM_FLAGS "${WASM_FLAGS} -s ASSERTIONS=2") # more runtime checks
+  else()
+    set(WASM_FLAGS "${WASM_FLAGS} -Os") # optimize for web (speed and size)
+  endif()
+
+  set(WASM_MODULE_NAME "StoneFrameworkModule" CACHE STRING "Name of the WebAssembly module")
+
+  set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WASM_FLAGS}")
+  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WASM_FLAGS}")
+
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${WASM_FLAGS}")  # not always clear which flags are for the compiler and which one are for the linker -> pass them all to the linker too
+  # set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --js-library ${STONE_SOURCES_DIR}/Applications/Samples/samples-library.js")
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/WasmWebService.js")
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/WasmDelayedCallExecutor.js")
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/default-library.js")
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\"]'")
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s EXPORT_NAME='\"${WASM_MODULE_NAME}\"'")
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1")
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s TOTAL_MEMORY=536870912")
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s TOTAL_STACK=128000000")
+
+  add_definitions(-DORTHANC_ENABLE_WASM=1)
+  set(ORTHANC_SANDBOXED ON)
+
+elseif (ENABLE_QT OR ENABLE_SDL)
+
+  set(ENABLE_NATIVE ON)
+  set(ORTHANC_SANDBOXED OFF)
+  set(ENABLE_CRYPTO_OPTIONS ON)
+  set(ENABLE_GOOGLE_TEST ON)
+  set(ENABLE_WEB_CLIENT ON)
+
+else()
+  set(ENABLE_NATIVE ON)
+  set(ENABLE_OPENGL OFF)
+  
+endif()
+
+
+#####################################################################
+## Configuration for Orthanc
+#####################################################################
+
+# include(../../Resources/CMake/Version.cmake)
+
+if (ORTHANC_STONE_VERSION STREQUAL "mainline")
+  set(ORTHANC_FRAMEWORK_VERSION "mainline")
+  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
+else()
+  set(ORTHANC_FRAMEWORK_VERSION "1.4.1")
+  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
+endif()
+
+set(ORTHANC_FRAMEWORK_SOURCE "${ORTHANC_FRAMEWORK_DEFAULT_SOURCE}" CACHE STRING "Source of the Orthanc source code (can be \"hg\", \"archive\", \"web\" or \"path\")")
+set(ORTHANC_FRAMEWORK_ARCHIVE "" CACHE STRING "Path to the Orthanc archive, if ORTHANC_FRAMEWORK_SOURCE is \"archive\"")
+set(ORTHANC_FRAMEWORK_ROOT "" CACHE STRING "Path to the Orthanc source directory, if ORTHANC_FRAMEWORK_SOURCE is \"path\"")
+
+add_definitions(
+  -DORTHANC_ENABLE_LOGGING_PLUGIN=0
+  )
+
+
+#####################################################################
+## Build a static library containing the Orthanc Stone framework
+#####################################################################
+
+
+LIST(APPEND ORTHANC_BOOST_COMPONENTS program_options)
+
+include(../../../Resources/CMake/OrthancStoneConfiguration.cmake)
+
+add_library(OrthancStone STATIC
+  ${ORTHANC_STONE_SOURCES}
+  )
+
+#####################################################################
+## Build all the sample applications
+#####################################################################
+
+include_directories(${ORTHANC_STONE_ROOT})
+
+# files common to all samples
+list(APPEND SAMPLE_APPLICATIONS_SOURCES
+  ${ORTHANC_STONE_ROOT}/Applications/Samples/Deprecated/SampleInteractor.h
+  ${ORTHANC_STONE_ROOT}/Applications/Samples/Deprecated/SampleApplicationBase.h
+  )
+
+if (ENABLE_QT)
+  list(APPEND SAMPLE_APPLICATIONS_SOURCES
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/Deprecated/Qt/SampleQtApplicationRunner.h
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/Deprecated/Qt/SampleMainWindow.cpp
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/Deprecated/Qt/SampleMainWindowWithButtons.cpp
+    )
+
+  ORTHANC_QT_WRAP_UI(SAMPLE_APPLICATIONS_SOURCES
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/Deprecated/Qt/SampleMainWindow.ui
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/Deprecated/Qt/SampleMainWindowWithButtons.ui
+    )
+
+  ORTHANC_QT_WRAP_CPP(SAMPLE_APPLICATIONS_SOURCES
+    ${ORTHANC_STONE_ROOT}/Applications/Qt/QCairoWidget.h
+    ${ORTHANC_STONE_ROOT}/Applications/Qt/QStoneMainWindow.h
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/Deprecated/Qt/SampleMainWindow.h
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/Deprecated/Qt/SampleMainWindowWithButtons.h
+    )
+endif()
+
+if (ENABLE_NATIVE)
+  list(APPEND SAMPLE_APPLICATIONS_SOURCES
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/Deprecated/SampleMainNative.cpp
+    )
+
+elseif (ENABLE_WASM)
+
+  list(APPEND SAMPLE_APPLICATIONS_SOURCES
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/Deprecated/SampleMainWasm.cpp
+    ${STONE_WASM_SOURCES}
+    )
+endif()
+
+
+macro(BuildSingleFileSample Target Header Sample)
+  add_executable(${Target}
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/Deprecated/${Header}
+    ${SAMPLE_APPLICATIONS_SOURCES}
+    )
+  set_target_properties(${Target} PROPERTIES COMPILE_DEFINITIONS ORTHANC_STONE_SAMPLE=${Sample})
+  target_link_libraries(${Target} OrthancStone)
+endmacro()
+
+
+if (ENABLE_SDL)
+  #BuildSingleFileSample(OrthancStoneEmpty EmptyApplication.h 1)
+  #BuildSingleFileSample(OrthancStoneTestPattern TestPatternApplication.h 2)
+  BuildSingleFileSample(OrthancStoneSingleFrame SingleFrameApplication.h 3)
+  #BuildSingleFileSample(OrthancStoneSingleVolume SingleVolumeApplication.h 4)
+  #BuildSingleFileSample(OrthancStoneBasicPetCtFusion 5)
+  #BuildSingleFileSample(OrthancStoneSynchronizedSeries 6)
+  #BuildSingleFileSample(OrthancStoneLayoutPetCtFusion 7)
+  BuildSingleFileSample(OrthancStoneSimpleViewerSingleFile SimpleViewerApplicationSingleFile.h 8)  # we keep that one just as a sample before we convert another sample to this pattern
+  BuildSingleFileSample(OrthancStoneSingleFrameEditor SingleFrameEditorApplication.h 9)
+endif()
+  
+##### SimpleViewer sample (Qt and WASM only) #######
+
+if (ENABLE_QT OR ENABLE_WASM)
+
+    if (ENABLE_QT)
+      list(APPEND SIMPLE_VIEWER_APPLICATION_SOURCES
+        ${ORTHANC_STONE_ROOT}/Applications/Samples/Deprecated/SimpleViewer/Qt/SimpleViewerMainWindow.cpp
+        ${ORTHANC_STONE_ROOT}/Applications/Samples/Deprecated/SimpleViewer/Qt/SimpleViewerMainWindow.ui
+        ${ORTHANC_STONE_ROOT}/Applications/Samples/Deprecated/SimpleViewer/Qt/mainQt.cpp
+        )
+
+      ORTHANC_QT_WRAP_UI(SIMPLE_VIEWER_APPLICATION_SOURCES
+        ${ORTHANC_STONE_ROOT}/Applications/Samples/Deprecated/SimpleViewer/Qt/SimpleViewerMainWindow.ui
+        )
+
+      ORTHANC_QT_WRAP_CPP(SIMPLE_VIEWER_APPLICATION_SOURCES
+        ${ORTHANC_STONE_ROOT}/Applications/Samples/Deprecated/SimpleViewer/Qt/SimpleViewerMainWindow.h
+        )
+
+elseif (ENABLE_WASM)
+        list(APPEND SIMPLE_VIEWER_APPLICATION_SOURCES
+            ${ORTHANC_STONE_ROOT}/Applications/Samples/Deprecated/SimpleViewer/Wasm/mainWasm.cpp
+            ${ORTHANC_STONE_ROOT}/Applications/Samples/Deprecated/SimpleViewer/Wasm/SimpleViewerWasmApplicationAdapter.cpp
+            ${ORTHANC_STONE_ROOT}/Applications/Samples/Deprecated/SimpleViewer/Wasm/SimpleViewerWasmApplicationAdapter.h
+            ${STONE_WASM_SOURCES}
+          )
+    endif()
+
+    add_executable(OrthancStoneSimpleViewer
+      ${ORTHANC_STONE_ROOT}/Applications/Samples/Deprecated/SimpleViewer/AppStatus.h
+      ${ORTHANC_STONE_ROOT}/Applications/Samples/Deprecated/SimpleViewer/MainWidgetInteractor.cpp
+      ${ORTHANC_STONE_ROOT}/Applications/Samples/Deprecated/SimpleViewer/MainWidgetInteractor.h
+      ${ORTHANC_STONE_ROOT}/Applications/Samples/Deprecated/SimpleViewer/SimpleViewerApplication.cpp
+      ${ORTHANC_STONE_ROOT}/Applications/Samples/Deprecated/SimpleViewer/SimpleViewerApplication.h
+      ${ORTHANC_STONE_ROOT}/Applications/Samples/Deprecated/SimpleViewer/ThumbnailInteractor.cpp
+      ${ORTHANC_STONE_ROOT}/Applications/Samples/Deprecated/SimpleViewer/ThumbnailInteractor.h
+      ${SIMPLE_VIEWER_APPLICATION_SOURCES}
+      )
+    target_link_libraries(OrthancStoneSimpleViewer OrthancStone)
+
+    BuildSingleFileSample(OrthancStoneSingleFrameEditor SingleFrameEditorApplication.h 9)
+endif()
+
+#####################################################################
+## Build the unit tests
+#####################################################################
+
+if (ENABLE_NATIVE)
+  add_executable(UnitTests
+    ${GOOGLE_TEST_SOURCES}
+    ${ORTHANC_STONE_ROOT}/UnitTestsSources/GenericToolboxTests.cpp
+    ${ORTHANC_STONE_ROOT}/UnitTestsSources/ImageToolboxTests.cpp
+    ${ORTHANC_STONE_ROOT}/UnitTestsSources/PixelTestPatternsTests.cpp
+    ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestCommands.cpp
+    ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestMessageBroker.cpp
+    ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestStrategy.cpp
+    ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestStructureSet.cpp
+    ${ORTHANC_STONE_ROOT}/UnitTestsSources/UnitTestsMain.cpp
+    )
+
+  target_link_libraries(UnitTests OrthancStone)
+
+  add_custom_command(
+    TARGET UnitTests
+    POST_BUILD
+    COMMAND ${CMAKE_COMMAND} -E copy
+      "${ORTHANC_STONE_ROOT}/UnitTestsSources/72c773ac-5059f2c4-2e6a9120-4fd4bca1-45701661.json" 
+      "$<TARGET_FILE_DIR:UnitTests>/72c773ac-5059f2c4-2e6a9120-4fd4bca1-45701661.json"
+    )
+
+endif()
+
+#####################################################################
+## Generate the documentation if Doxygen is present
+#####################################################################
+
+find_package(Doxygen)
+if (DOXYGEN_FOUND)
+  configure_file(
+    ${ORTHANC_STONE_ROOT}/Resources/OrthancStone.doxygen
+    ${CMAKE_CURRENT_BINARY_DIR}/OrthancStone.doxygen
+    @ONLY)
+
+  add_custom_target(doc
+    ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/OrthancStone.doxygen
+    COMMENT "Generating documentation with Doxygen" VERBATIM
+    )
+else()
+  message("Doxygen not found. The documentation will not be built.")
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/CMakeLists.txt.old	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,248 @@
+# Usage: see README file
+
+cmake_minimum_required(VERSION 2.8.3)
+
+# Automatically link Qt executables to qtmain target on Windows
+# ("OLD" == do not link)
+if(POLICY CMP0020)
+  cmake_policy(SET CMP0020 OLD)
+endif()
+
+# Only interpret if() arguments as variables or keywords when unquoted.
+# NEW = do NOT dereference *quoted* variables
+if(POLICY CMP0054)
+  cmake_policy(SET CMP0054 NEW)
+endif()
+
+project(OrthancStone)
+
+include(../../Resources/CMake/OrthancStoneParameters.cmake)
+
+#set(ENABLE_DCMTK ON)
+
+set(ENABLE_SDL OFF CACHE BOOL "Target SDL Native application")
+set(ENABLE_QT OFF CACHE BOOL "Target Qt Native application")
+set(ENABLE_WASM OFF CACHE BOOL "Target WASM application")
+
+# TODO: replace or compute STONE_SOURCES_DIR from CMAKE_CURRENT_LIST_FILE
+
+if (ENABLE_WASM)
+  #####################################################################
+  ## Configuration of the Emscripten compiler for WebAssembly target
+  #####################################################################
+
+  set(WASM_FLAGS "-s WASM=1 -O0 -g0")
+  message("*****************************************************************************")
+  message("WARNING: optimizations are disabled in emcc!!! Enable them for production use")
+  message("*****************************************************************************")
+  set(WASM_MODULE_NAME "StoneFrameworkModule" CACHE STRING "Name of the WebAssembly module")
+  set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WASM_FLAGS}")
+  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WASM_FLAGS}")
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/WasmWebService.js --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/WasmDelayedCallExecutor.js --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/default-library.js  -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\"]'")
+
+  # Handling of memory
+  #set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1")  # Resize
+  #set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s TOTAL_MEMORY=536870912")  # 512MB
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s EXPORT_NAME='\"${WASM_MODULE_NAME}\"' -s ALLOW_MEMORY_GROWTH=1 -s TOTAL_MEMORY=536870912 -s TOTAL_STACK=128000000")  # 512MB + resize
+  #set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1 -s TOTAL_MEMORY=1073741824")  # 1GB + resize
+
+  # To debug exceptions
+  #set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s DEMANGLE_SUPPORT=1 -s ASSERTIONS=2")
+
+  add_definitions(-DORTHANC_ENABLE_WASM=1)
+  set(ORTHANC_SANDBOXED ON)
+
+elseif (ENABLE_QT OR ENABLE_SDL)
+
+  set(ENABLE_NATIVE ON)
+  set(ORTHANC_SANDBOXED OFF)
+  set(ENABLE_CRYPTO_OPTIONS ON)
+  set(ENABLE_GOOGLE_TEST ON)
+  set(ENABLE_WEB_CLIENT ON)
+
+endif()
+
+#####################################################################
+## Configuration for Orthanc
+#####################################################################
+
+# include(../../Resources/CMake/Version.cmake)
+
+if (ORTHANC_STONE_VERSION STREQUAL "mainline")
+  set(ORTHANC_FRAMEWORK_VERSION "mainline")
+  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
+else()
+  set(ORTHANC_FRAMEWORK_VERSION "1.4.1")
+  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
+endif()
+
+set(ORTHANC_FRAMEWORK_SOURCE "${ORTHANC_FRAMEWORK_DEFAULT_SOURCE}" CACHE STRING "Source of the Orthanc source code (can be \"hg\", \"archive\", \"web\" or \"path\")")
+set(ORTHANC_FRAMEWORK_ARCHIVE "" CACHE STRING "Path to the Orthanc archive, if ORTHANC_FRAMEWORK_SOURCE is \"archive\"")
+set(ORTHANC_FRAMEWORK_ROOT "" CACHE STRING "Path to the Orthanc source directory, if ORTHANC_FRAMEWORK_SOURCE is \"path\"")
+
+#####################################################################
+## Build a static library containing the Orthanc Stone framework
+#####################################################################
+
+LIST(APPEND ORTHANC_BOOST_COMPONENTS program_options)
+
+include(../../Resources/CMake/OrthancStoneConfiguration.cmake)
+
+add_library(OrthancStone STATIC
+  ${ORTHANC_STONE_SOURCES}
+  )
+
+#####################################################################
+## Build all the sample applications
+#####################################################################
+
+include_directories(${ORTHANC_STONE_ROOT})
+
+# files common to all samples
+list(APPEND SAMPLE_APPLICATIONS_SOURCES
+  ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleInteractor.h
+  ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleApplicationBase.h
+  )
+
+if (ENABLE_QT)
+  list(APPEND SAMPLE_APPLICATIONS_SOURCES
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleQtApplicationRunner.h
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindow.cpp
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindowWithButtons.cpp
+    )
+
+  ORTHANC_QT_WRAP_UI(SAMPLE_APPLICATIONS_SOURCES
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindow.ui
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindowWithButtons.ui
+    )
+
+  ORTHANC_QT_WRAP_CPP(SAMPLE_APPLICATIONS_SOURCES
+    ${ORTHANC_STONE_ROOT}/Applications/Qt/QCairoWidget.h
+    ${ORTHANC_STONE_ROOT}/Applications/Qt/QStoneMainWindow.h
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindow.h
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/Qt/SampleMainWindowWithButtons.h
+    )
+endif()
+
+if (ENABLE_NATIVE)
+  list(APPEND SAMPLE_APPLICATIONS_SOURCES
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleMainNative.cpp
+    )
+
+elseif (ENABLE_WASM)
+
+  list(APPEND SAMPLE_APPLICATIONS_SOURCES
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleMainWasm.cpp
+    ${STONE_WASM_SOURCES}
+    )
+endif()
+
+
+macro(BuildSingleFileSample Target Header Sample)
+  add_executable(${Target}
+    ${ORTHANC_STONE_ROOT}/Applications/Samples/${Header}
+    ${SAMPLE_APPLICATIONS_SOURCES}
+    )
+  set_target_properties(${Target} PROPERTIES COMPILE_DEFINITIONS ORTHANC_STONE_SAMPLE=${Sample})
+  target_link_libraries(${Target} OrthancStone)
+  
+  if (ENABLE_QT AND (CMAKE_SYSTEM_NAME STREQUAL "Windows"))
+    message("(ENABLE_QT and (CMAKE_SYSTEM_NAME matches \"Windows\")) is true")
+    add_custom_command(
+      TARGET ${Target} POST_BUILD
+      COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:Qt5::Core> $<TARGET_FILE_DIR:${Target}>
+      COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:Qt5::Widgets> $<TARGET_FILE_DIR:${Target}>
+      COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:Qt5::Gui> $<TARGET_FILE_DIR:${Target}>
+    )
+  endif()
+endmacro()
+
+#BuildSingleFileSample(OrthancStoneEmpty EmptyApplication.h 1)
+#BuildSingleFileSample(OrthancStoneTestPattern TestPatternApplication.h 2)
+BuildSingleFileSample(OrthancStoneSingleFrame SingleFrameApplication.h 3)
+#BuildSingleFileSample(OrthancStoneSingleVolume SingleVolumeApplication.h 4)
+#BuildSingleFileSample(OrthancStoneBasicPetCtFusion 5)
+#BuildSingleFileSample(OrthancStoneSynchronizedSeries 6)
+#BuildSingleFileSample(OrthancStoneLayoutPetCtFusion 7)
+BuildSingleFileSample(OrthancStoneSimpleViewerSingleFile SimpleViewerApplicationSingleFile.h 8)  # we keep that one just as a sample before we convert another sample to this pattern
+BuildSingleFileSample(OrthancStoneSingleFrameEditor SingleFrameEditorApplication.h 9)
+
+##### SimpleViewer sample (Qt and WASM only) #######
+
+if (ENABLE_QT OR ENABLE_WASM)
+
+      # GenerateCodeFromFlatBufferSchema("${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/ApplicationCommands.fbs")
+
+      list(APPEND SIMPLE_VIEWER_APPLICATION_SOURCES ${FLATC_AUTOGENERATED_SOURCES})
+      message(STATUS "SIMPLE_VIEWER_APPLICATION_SOURCES = ${SIMPLE_VIEWER_APPLICATION_SOURCES}")
+      message(STATUS "FLATC_AUTOGENERATED_SOURCES = ${FLATC_AUTOGENERATED_SOURCES}")
+
+    if (ENABLE_QT)
+      list(APPEND SIMPLE_VIEWER_APPLICATION_SOURCES
+        ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.cpp
+        ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.ui
+        ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Qt/mainQt.cpp
+        )
+
+      ORTHANC_QT_WRAP_UI(SIMPLE_VIEWER_APPLICATION_SOURCES
+        ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.ui
+        )
+
+      ORTHANC_QT_WRAP_CPP(SIMPLE_VIEWER_APPLICATION_SOURCES
+        ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.h
+        )
+
+elseif (ENABLE_WASM)
+        list(APPEND SIMPLE_VIEWER_APPLICATION_SOURCES
+            ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Wasm/mainWasm.cpp
+            ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Wasm/SimpleViewerWasmApplicationAdapter.cpp
+            ${STONE_WASM_SOURCES}
+          )
+    endif()
+
+    add_executable(OrthancStoneSimpleViewer
+      ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/SimpleViewerApplication.cpp
+      ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/ThumbnailInteractor.cpp
+      ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/MainWidgetInteractor.cpp
+      ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/AppStatus.h
+      ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Messages.h
+      ${SIMPLE_VIEWER_APPLICATION_SOURCES}
+      )
+    target_link_libraries(OrthancStoneSimpleViewer OrthancStone)
+
+endif()
+
+#####################################################################
+## Build the unit tests
+#####################################################################
+
+if (ENABLE_NATIVE)
+  add_executable(UnitTests
+    ${GOOGLE_TEST_SOURCES}
+    ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestCommands.cpp
+    ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestExceptions.cpp
+    ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestMessageBroker.cpp
+    ${ORTHANC_STONE_ROOT}/UnitTestsSources/UnitTestsMain.cpp
+    )
+
+  target_link_libraries(UnitTests OrthancStone)
+endif()
+
+#####################################################################
+## Generate the documentation if Doxygen is present
+#####################################################################
+
+find_package(Doxygen)
+if (DOXYGEN_FOUND)
+  configure_file(
+    ${ORTHANC_STONE_ROOT}/Resources/OrthancStone.doxygen
+    ${CMAKE_CURRENT_BINARY_DIR}/OrthancStone.doxygen
+    @ONLY)
+
+  add_custom_target(doc
+    ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/OrthancStone.doxygen
+    COMMENT "Generating documentation with Doxygen" VERBATIM
+    )
+else()
+  message("Doxygen not found. The documentation will not be built.")
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/EmptyApplication.h	Wed Apr 22 14:05:47 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/>.
+ **/
+
+
+#pragma once
+
+#include "SampleApplicationBase.h"
+
+#include "../../../Framework/Widgets/EmptyWidget.h"
+
+namespace OrthancStone
+{
+  namespace Samples
+  {
+    class EmptyApplication : public SampleApplicationBase
+    {
+    public:
+      virtual void DeclareStartupOptions(boost::program_options::options_description& options)
+      {
+        boost::program_options::options_description generic("Sample options");
+        generic.add_options()
+          ("red", boost::program_options::value<int>()->default_value(255), "Background color: red channel")
+          ("green", boost::program_options::value<int>()->default_value(0), "Background color: green channel")
+          ("blue", boost::program_options::value<int>()->default_value(0), "Background color: blue channel")
+          ;
+
+        options.add(generic);    
+      }
+
+      virtual void Initialize(IStatusBar& statusBar,
+                              const boost::program_options::variables_map& parameters)
+      {
+        int red = parameters["red"].as<int>();
+        int green = parameters["green"].as<int>();
+        int blue = parameters["blue"].as<int>();
+
+        context_->SetCentralWidget(new EmptyWidget(red, green, blue));
+      }
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/LayoutPetCtFusionApplication.h	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,398 @@
+/**
+ * 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 "SampleInteractor.h"
+
+#include "../../../Framework/Layers/ReferenceLineFactory.h"
+#include "../../../Framework/Layers/DicomStructureSetSlicer.h"
+#include "../../../Framework/Widgets/LayoutWidget.h"
+
+#include <Core/Logging.h>
+
+namespace OrthancStone
+{
+  namespace Samples
+  {
+    class LayoutPetCtFusionApplication : 
+      public SampleApplicationBase,
+      public LayeredSceneWidget::ISliceObserver,
+      public WorldSceneWidget::IWorldObserver
+    {
+    private:
+      class Interactor : public SampleInteractor
+      {
+      private:
+        LayoutPetCtFusionApplication& that_;
+
+      public:
+        Interactor(LayoutPetCtFusionApplication& that,
+                   VolumeImage& volume,
+                   VolumeProjection projection, 
+                   bool reverse) :
+          SampleInteractor(volume, projection, reverse),
+          that_(that)
+        {
+        }
+
+        virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget,
+                                                            const SliceGeometry& slice,
+                                                            const ViewportGeometry& view,
+                                                            MouseButton button,
+                                                            double x,
+                                                            double y,
+                                                            IStatusBar* statusBar)
+        {
+          if (button == MouseButton_Left)
+          {
+            // Center the sibling views over the clicked point
+            Vector p = slice.MapSliceToWorldCoordinates(x, y);
+
+            if (statusBar != NULL)
+            {
+              char buf[64];
+              sprintf(buf, "Click on coordinates (%.02f,%.02f,%.02f) in cm", p[0] / 10.0, p[1] / 10.0, p[2] / 10.0);
+              statusBar->SetMessage(buf);
+            }
+
+            that_.interactorAxial_->LookupSliceContainingPoint(*that_.ctAxial_, p);
+            that_.interactorCoronal_->LookupSliceContainingPoint(*that_.ctCoronal_, p);
+            that_.interactorSagittal_->LookupSliceContainingPoint(*that_.ctSagittal_, p);
+          }
+
+          return NULL;
+        }
+
+        virtual void KeyPressed(WorldSceneWidget& widget,
+                                char key,
+                                KeyboardModifiers modifiers,
+                                IStatusBar* statusBar)
+        {
+          if (key == 's')
+          {
+            that_.FitContent();
+          }
+        }
+      };
+
+      bool                 processingEvent_;
+      Interactor*          interactorAxial_;
+      Interactor*          interactorCoronal_;
+      Interactor*          interactorSagittal_;
+      LayeredSceneWidget*  ctAxial_;
+      LayeredSceneWidget*  ctCoronal_;
+      LayeredSceneWidget*  ctSagittal_;
+      LayeredSceneWidget*  petAxial_;
+      LayeredSceneWidget*  petCoronal_;
+      LayeredSceneWidget*  petSagittal_;
+      LayeredSceneWidget*  fusionAxial_;
+      LayeredSceneWidget*  fusionCoronal_;
+      LayeredSceneWidget*  fusionSagittal_;
+
+
+      void FitContent()
+      {
+        petAxial_->FitContent();
+        petCoronal_->FitContent();
+        petSagittal_->FitContent();
+      }
+
+
+      void AddLayer(LayeredSceneWidget& widget,
+                    VolumeImage& volume,
+                    bool isCt)
+      {
+        size_t layer;
+        widget.AddLayer(layer, new VolumeImage::LayerFactory(volume));
+
+        if (isCt)
+        {
+          RenderStyle style; 
+          style.windowing_ = ImageWindowing_Bone;
+          widget.SetLayerStyle(layer, style);
+        }
+        else
+        {
+          RenderStyle style; 
+          style.applyLut_ = true;
+          style.alpha_ = (layer == 0 ? 1.0f : 0.5f);
+          widget.SetLayerStyle(layer, style);
+        }
+      }
+
+
+      void ConnectSiblingLocations(LayeredSceneWidget& axial,
+                                   LayeredSceneWidget& coronal,
+                                   LayeredSceneWidget& sagittal)
+      {
+        ReferenceLineFactory::Configure(axial, coronal);
+        ReferenceLineFactory::Configure(axial, sagittal);
+        ReferenceLineFactory::Configure(coronal, sagittal);
+      }
+
+
+      void SynchronizeView(const WorldSceneWidget& source,
+                           const ViewportGeometry& view,
+                           LayeredSceneWidget& widget1,
+                           LayeredSceneWidget& widget2,
+                           LayeredSceneWidget& widget3)
+      {
+        if (&source == &widget1 ||
+            &source == &widget2 ||
+            &source == &widget3)
+        {
+          if (&source != &widget1)
+          {
+            widget1.SetView(view);
+          }
+
+          if (&source != &widget2)
+          {
+            widget2.SetView(view);
+          }
+
+          if (&source != &widget3)
+          {
+            widget3.SetView(view);
+          }
+        }
+      }
+
+
+      void SynchronizeSlice(const LayeredSceneWidget& source,
+                            const SliceGeometry& slice,
+                            LayeredSceneWidget& widget1,
+                            LayeredSceneWidget& widget2,
+                            LayeredSceneWidget& widget3)
+      {
+        if (&source == &widget1 ||
+            &source == &widget2 ||
+            &source == &widget3)
+        {
+          if (&source != &widget1)
+          {
+            widget1.SetSlice(slice);
+          }
+
+          if (&source != &widget2)
+          {
+            widget2.SetSlice(slice);
+          }
+
+          if (&source != &widget3)
+          {
+            widget3.SetSlice(slice);
+          }
+        }
+      }
+
+
+      LayeredSceneWidget* CreateWidget()
+      {
+        std::unique_ptr<LayeredSceneWidget> widget(new LayeredSceneWidget);
+        widget->Register(dynamic_cast<WorldSceneWidget::IWorldObserver&>(*this));
+        widget->Register(dynamic_cast<LayeredSceneWidget::ISliceObserver&>(*this));
+        return widget.release();
+      }
+
+
+      void CreateLayout(BasicApplicationContext& context)
+      {
+        std::unique_ptr<OrthancStone::LayoutWidget> layout(new OrthancStone::LayoutWidget);
+        layout->SetBackgroundCleared(true);
+        //layout->SetBackgroundColor(255,0,0);
+        layout->SetPadding(5);
+
+        OrthancStone::LayoutWidget& layoutA = dynamic_cast<OrthancStone::LayoutWidget&>
+          (layout->AddWidget(new OrthancStone::LayoutWidget));
+        layoutA.SetPadding(0, 0, 0, 0, 5);
+        layoutA.SetVertical();
+        petAxial_ = &dynamic_cast<LayeredSceneWidget&>(layoutA.AddWidget(CreateWidget()));
+        OrthancStone::LayoutWidget& layoutA2 = dynamic_cast<OrthancStone::LayoutWidget&>
+          (layoutA.AddWidget(new OrthancStone::LayoutWidget));
+        layoutA2.SetPadding(0, 0, 0, 0, 5);
+        petSagittal_ = &dynamic_cast<LayeredSceneWidget&>(layoutA2.AddWidget(CreateWidget()));
+        petCoronal_ = &dynamic_cast<LayeredSceneWidget&>(layoutA2.AddWidget(CreateWidget()));
+
+        OrthancStone::LayoutWidget& layoutB = dynamic_cast<OrthancStone::LayoutWidget&>
+          (layout->AddWidget(new OrthancStone::LayoutWidget));
+        layoutB.SetPadding(0, 0, 0, 0, 5);
+        layoutB.SetVertical();
+        ctAxial_ = &dynamic_cast<LayeredSceneWidget&>(layoutB.AddWidget(CreateWidget()));
+        OrthancStone::LayoutWidget& layoutB2 = dynamic_cast<OrthancStone::LayoutWidget&>
+          (layoutB.AddWidget(new OrthancStone::LayoutWidget));
+        layoutB2.SetPadding(0, 0, 0, 0, 5);
+        ctSagittal_ = &dynamic_cast<LayeredSceneWidget&>(layoutB2.AddWidget(CreateWidget()));
+        ctCoronal_ = &dynamic_cast<LayeredSceneWidget&>(layoutB2.AddWidget(CreateWidget()));
+
+        OrthancStone::LayoutWidget& layoutC = dynamic_cast<OrthancStone::LayoutWidget&>
+          (layout->AddWidget(new OrthancStone::LayoutWidget));
+        layoutC.SetPadding(0, 0, 0, 0, 5);
+        layoutC.SetVertical();
+        fusionAxial_ = &dynamic_cast<LayeredSceneWidget&>(layoutC.AddWidget(CreateWidget()));
+        OrthancStone::LayoutWidget& layoutC2 = dynamic_cast<OrthancStone::LayoutWidget&>
+          (layoutC.AddWidget(new OrthancStone::LayoutWidget));
+        layoutC2.SetPadding(0, 0, 0, 0, 5);
+        fusionSagittal_ = &dynamic_cast<LayeredSceneWidget&>(layoutC2.AddWidget(CreateWidget()));
+        fusionCoronal_ = &dynamic_cast<LayeredSceneWidget&>(layoutC2.AddWidget(CreateWidget()));
+  
+        context.SetCentralWidget(layout.release());
+      }
+
+
+    public:
+      virtual void DeclareCommandLineOptions(boost::program_options::options_description& options)
+      {
+        boost::program_options::options_description generic("Sample options");
+        generic.add_options()
+          ("ct", boost::program_options::value<std::string>(), 
+           "Orthanc ID of the CT series")
+          ("pet", boost::program_options::value<std::string>(), 
+           "Orthanc ID of the PET series")
+          ("rt", boost::program_options::value<std::string>(), 
+           "Orthanc ID of the DICOM RT-STRUCT series (optional)")
+          ("threads", boost::program_options::value<unsigned int>()->default_value(3), 
+           "Number of download threads for the CT series")
+          ;
+
+        options.add(generic);    
+      }
+
+      virtual void Initialize(BasicApplicationContext& context,
+                              IStatusBar& statusBar,
+                              const boost::program_options::variables_map& parameters)
+      {
+        using namespace OrthancStone;
+
+        processingEvent_ = true;
+
+        if (parameters.count("ct") != 1 ||
+            parameters.count("pet") != 1)
+        {
+          LOG(ERROR) << "The series ID is missing";
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+
+        std::string ct = parameters["ct"].as<std::string>();
+        std::string pet = parameters["pet"].as<std::string>();
+        unsigned int threads = parameters["threads"].as<unsigned int>();
+
+        VolumeImage& ctVolume = context.AddSeriesVolume(ct, true /* progressive download */, threads);
+        VolumeImage& petVolume = context.AddSeriesVolume(pet, true /* progressive download */, 1);
+
+        // Take the PET volume as the reference for the slices
+        interactorAxial_ = &dynamic_cast<Interactor&>
+          (context.AddInteractor(new Interactor(*this, petVolume, VolumeProjection_Axial, false)));
+        interactorCoronal_ = &dynamic_cast<Interactor&>
+          (context.AddInteractor(new Interactor(*this, petVolume, VolumeProjection_Coronal, false)));
+        interactorSagittal_ = &dynamic_cast<Interactor&>
+          (context.AddInteractor(new Interactor(*this, petVolume, VolumeProjection_Sagittal, true)));
+
+        CreateLayout(context);
+
+        AddLayer(*ctAxial_, ctVolume, true);
+        AddLayer(*ctCoronal_, ctVolume, true);
+        AddLayer(*ctSagittal_, ctVolume, true);
+
+        AddLayer(*petAxial_, petVolume, false);
+        AddLayer(*petCoronal_, petVolume, false);
+        AddLayer(*petSagittal_, petVolume, false);
+
+        AddLayer(*fusionAxial_, ctVolume, true);
+        AddLayer(*fusionAxial_, petVolume, false);
+        AddLayer(*fusionCoronal_, ctVolume, true);
+        AddLayer(*fusionCoronal_, petVolume, false);
+        AddLayer(*fusionSagittal_, ctVolume, true);
+        AddLayer(*fusionSagittal_, petVolume, false);
+
+        if (parameters.count("rt") == 1)
+        {
+          DicomStructureSet& rtStruct = context.AddStructureSet(parameters["rt"].as<std::string>());
+
+          Vector p = rtStruct.GetStructureCenter(0);
+          interactorAxial_->GetCursor().LookupSliceContainingPoint(p);
+
+          ctAxial_->AddLayer(new DicomStructureSetSlicer(rtStruct));
+          petAxial_->AddLayer(new DicomStructureSetSlicer(rtStruct));
+          fusionAxial_->AddLayer(new DicomStructureSetSlicer(rtStruct));
+        }        
+
+        ConnectSiblingLocations(*ctAxial_, *ctCoronal_, *ctSagittal_); 
+        ConnectSiblingLocations(*petAxial_, *petCoronal_, *petSagittal_); 
+        ConnectSiblingLocations(*fusionAxial_, *fusionCoronal_, *fusionSagittal_); 
+
+        interactorAxial_->AddWidget(*ctAxial_);
+        interactorAxial_->AddWidget(*petAxial_);
+        interactorAxial_->AddWidget(*fusionAxial_);
+        
+        interactorCoronal_->AddWidget(*ctCoronal_);
+        interactorCoronal_->AddWidget(*petCoronal_);
+        interactorCoronal_->AddWidget(*fusionCoronal_);
+        
+        interactorSagittal_->AddWidget(*ctSagittal_);
+        interactorSagittal_->AddWidget(*petSagittal_);
+        interactorSagittal_->AddWidget(*fusionSagittal_);
+
+        processingEvent_ = false;
+
+        statusBar.SetMessage("Use the key \"t\" to toggle the fullscreen mode");
+        statusBar.SetMessage("Use the key \"s\" to reinitialize the layout");
+      }
+
+      virtual void NotifySizeChange(const WorldSceneWidget& source,
+                                    ViewportGeometry& view)
+      {
+        view.FitContent();
+      }
+
+      virtual void NotifyViewChange(const WorldSceneWidget& source,
+                                    const ViewportGeometry& view)
+      {
+        if (!processingEvent_)  // Avoid reentrant calls
+        {
+          processingEvent_ = true;
+
+          SynchronizeView(source, view, *ctAxial_, *petAxial_, *fusionAxial_);
+          SynchronizeView(source, view, *ctCoronal_, *petCoronal_, *fusionCoronal_);
+          SynchronizeView(source, view, *ctSagittal_, *petSagittal_, *fusionSagittal_);
+          
+          processingEvent_ = false;
+        }
+      }
+
+      virtual void NotifySliceContentChange(const LayeredSceneWidget& source,
+                                     const SliceGeometry& slice)
+      {
+        if (!processingEvent_)  // Avoid reentrant calls
+        {
+          processingEvent_ = true;
+
+          SynchronizeSlice(source, slice, *ctAxial_, *petAxial_, *fusionAxial_);
+          SynchronizeSlice(source, slice, *ctCoronal_, *petCoronal_, *fusionCoronal_);
+          SynchronizeSlice(source, slice, *ctSagittal_, *petSagittal_, *fusionSagittal_);
+          
+          processingEvent_ = false;
+        }
+      }
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/Qt/SampleMainWindow.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,53 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "SampleMainWindow.h"
+
+/**
+ * Don't use "ui_MainWindow.h" instead of <ui_MainWindow.h> below, as
+ * this makes CMake unable to detect when the UI file changes.
+ **/
+#include <ui_SampleMainWindow.h>
+#include "../../../Applications/Samples/SampleApplicationBase.h"
+
+namespace OrthancStone
+{
+  namespace Samples
+  {
+
+    SampleMainWindow::SampleMainWindow(
+      OrthancStone::NativeStoneApplicationContext& context,
+      OrthancStone::Samples::SampleSingleCanvasApplicationBase& stoneSampleApplication,
+      QWidget *parent) :
+      QStoneMainWindow(context, parent),
+      ui_(new Ui::SampleMainWindow),
+      stoneSampleApplication_(stoneSampleApplication)
+    {
+      ui_->setupUi(this);
+      SetCentralStoneWidget(*ui_->cairoCentralWidget);
+    }
+
+    SampleMainWindow::~SampleMainWindow()
+    {
+      delete ui_;
+    }
+
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/Qt/SampleMainWindow.h	Wed Apr 22 14:05:47 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 "../../../Qt/QCairoWidget.h"
+#include "../../../Qt/QStoneMainWindow.h"
+
+namespace Ui 
+{
+  class SampleMainWindow;
+}
+
+namespace OrthancStone
+{
+  namespace Samples
+  {
+
+    class SampleSingleCanvasApplicationBase;
+
+    class SampleMainWindow : public QStoneMainWindow
+    {
+      Q_OBJECT
+
+    private:
+      Ui::SampleMainWindow*   ui_;
+      SampleSingleCanvasApplicationBase&  stoneSampleApplication_;
+
+    public:
+      explicit SampleMainWindow(OrthancStone::NativeStoneApplicationContext& context, SampleSingleCanvasApplicationBase& stoneSampleApplication, QWidget *parent = 0);
+      ~SampleMainWindow();
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/Qt/SampleMainWindow.ui	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SampleMainWindow</class>
+ <widget class="QMainWindow" name="SampleMainWindow">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>903</width>
+    <height>634</height>
+   </rect>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>500</width>
+    <height>300</height>
+   </size>
+  </property>
+  <property name="baseSize">
+   <size>
+    <width>500</width>
+    <height>300</height>
+   </size>
+  </property>
+  <property name="windowTitle">
+   <string>Stone of Orthanc</string>
+  </property>
+  <property name="layoutDirection">
+   <enum>Qt::LeftToRight</enum>
+  </property>
+  <widget class="QWidget" name="centralwidget">
+   <property name="sizePolicy">
+    <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+     <horstretch>0</horstretch>
+     <verstretch>0</verstretch>
+    </sizepolicy>
+   </property>
+   <property name="layoutDirection">
+    <enum>Qt::LeftToRight</enum>
+   </property>
+   <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0">
+    <property name="sizeConstraint">
+     <enum>QLayout::SetDefaultConstraint</enum>
+    </property>
+    <item>
+     <widget class="QCairoWidget" name="cairoCentralWidget">
+      <property name="minimumSize">
+       <size>
+        <width>0</width>
+        <height>500</height>
+       </size>
+      </property>
+     </widget>
+    </item>
+   </layout>
+  </widget>
+  <widget class="QMenuBar" name="menubar">
+   <property name="geometry">
+    <rect>
+     <x>0</x>
+     <y>0</y>
+     <width>903</width>
+     <height>22</height>
+    </rect>
+   </property>
+   <widget class="QMenu" name="menuTest">
+    <property name="title">
+     <string>Test</string>
+    </property>
+   </widget>
+   <addaction name="menuTest"/>
+  </widget>
+  <widget class="QStatusBar" name="statusbar"/>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>QCairoWidget</class>
+   <extends>QGraphicsView</extends>
+   <header location="global">QCairoWidget.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/Qt/SampleMainWindowWithButtons.cpp	Wed Apr 22 14:05:47 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 "SampleMainWindow.h"
+
+/**
+ * Don't use "ui_MainWindow.h" instead of <ui_MainWindow.h> below, as
+ * this makes CMake unable to detect when the UI file changes.
+ **/
+#include <ui_SampleMainWindowWithButtons.h>
+#include "../../../Applications/Samples/SampleApplicationBase.h"
+
+namespace OrthancStone
+{
+  namespace Samples
+  {
+
+    SampleMainWindowWithButtons::SampleMainWindowWithButtons(
+      OrthancStone::NativeStoneApplicationContext& context,
+      OrthancStone::Samples::SampleSingleCanvasWithButtonsApplicationBase& stoneSampleApplication,
+      QWidget *parent) :
+      QStoneMainWindow(context, parent),
+      ui_(new Ui::SampleMainWindowWithButtons),
+      stoneSampleApplication_(stoneSampleApplication)
+    {
+      ui_->setupUi(this);
+      SetCentralStoneWidget(*ui_->cairoCentralWidget);
+
+#if QT_VERSION >= 0x050000
+      connect(ui_->toolButton1, &QToolButton::clicked, this, &SampleMainWindowWithButtons::tool1Clicked);
+      connect(ui_->toolButton2, &QToolButton::clicked, this, &SampleMainWindowWithButtons::tool2Clicked);
+      connect(ui_->pushButton1, &QPushButton::clicked, this, &SampleMainWindowWithButtons::pushButton1Clicked);
+      connect(ui_->pushButton1, &QPushButton::clicked, this, &SampleMainWindowWithButtons::pushButton2Clicked);
+#else
+      connect(ui_->toolButton1, SIGNAL(clicked()), this, SLOT(tool1Clicked()));
+      connect(ui_->toolButton2, SIGNAL(clicked()), this, SLOT(tool2Clicked()));
+      connect(ui_->pushButton1, SIGNAL(clicked()), this, SLOT(pushButton1Clicked()));
+      connect(ui_->pushButton1, SIGNAL(clicked()), this, SLOT(pushButton2Clicked()));
+#endif
+
+      std::string pushButton1Name;
+      std::string pushButton2Name;
+      std::string tool1Name;
+      std::string tool2Name;
+      stoneSampleApplication_.GetButtonNames(pushButton1Name, pushButton2Name, tool1Name, tool2Name);
+
+      ui_->toolButton1->setText(QString::fromStdString(tool1Name));
+      ui_->toolButton2->setText(QString::fromStdString(tool2Name));
+      ui_->pushButton1->setText(QString::fromStdString(pushButton1Name));
+      ui_->pushButton2->setText(QString::fromStdString(pushButton2Name));
+    }
+
+    SampleMainWindowWithButtons::~SampleMainWindowWithButtons()
+    {
+      delete ui_;
+    }
+
+    void SampleMainWindowWithButtons::tool1Clicked()
+    {
+      stoneSampleApplication_.OnTool1Clicked();
+    }
+
+    void SampleMainWindowWithButtons::tool2Clicked()
+    {
+      stoneSampleApplication_.OnTool2Clicked();
+    }
+
+    void SampleMainWindowWithButtons::pushButton1Clicked()
+    {
+      stoneSampleApplication_.OnPushButton1Clicked();
+    }
+
+    void SampleMainWindowWithButtons::pushButton2Clicked()
+    {
+      stoneSampleApplication_.OnPushButton2Clicked();
+    }
+
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/Qt/SampleMainWindowWithButtons.h	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,56 @@
+/**
+ * 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 "../../../Qt/QCairoWidget.h"
+#include "../../../Qt/QStoneMainWindow.h"
+
+namespace Ui 
+{
+  class SampleMainWindowWithButtons;
+}
+
+namespace OrthancStone
+{
+  namespace Samples
+  {
+
+    class SampleSingleCanvasWithButtonsApplicationBase;
+
+    class SampleMainWindowWithButtons : public QStoneMainWindow
+    {
+      Q_OBJECT
+
+    private:
+      Ui::SampleMainWindowWithButtons*   ui_;
+      SampleSingleCanvasWithButtonsApplicationBase&  stoneSampleApplication_;
+
+    public:
+      explicit SampleMainWindowWithButtons(OrthancStone::NativeStoneApplicationContext& context, SampleSingleCanvasWithButtonsApplicationBase& stoneSampleApplication, QWidget *parent = 0);
+      ~SampleMainWindowWithButtons();
+
+    private slots:
+      void tool1Clicked();
+      void tool2Clicked();
+      void pushButton1Clicked();
+      void pushButton2Clicked();
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/Qt/SampleMainWindowWithButtons.ui	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SampleMainWindowWithButtons</class>
+ <widget class="QMainWindow" name="SampleMainWindowWithButtons">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>903</width>
+    <height>634</height>
+   </rect>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>500</width>
+    <height>300</height>
+   </size>
+  </property>
+  <property name="baseSize">
+   <size>
+    <width>500</width>
+    <height>300</height>
+   </size>
+  </property>
+  <property name="windowTitle">
+   <string>Stone of Orthanc</string>
+  </property>
+  <property name="layoutDirection">
+   <enum>Qt::LeftToRight</enum>
+  </property>
+  <widget class="QWidget" name="centralwidget">
+   <property name="sizePolicy">
+    <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+     <horstretch>0</horstretch>
+     <verstretch>0</verstretch>
+    </sizepolicy>
+   </property>
+   <property name="layoutDirection">
+    <enum>Qt::LeftToRight</enum>
+   </property>
+   <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0">
+    <property name="sizeConstraint">
+     <enum>QLayout::SetDefaultConstraint</enum>
+    </property>
+    <item>
+     <widget class="QCairoWidget" name="cairoCentralWidget">
+      <property name="minimumSize">
+       <size>
+        <width>0</width>
+        <height>500</height>
+       </size>
+      </property>
+     </widget>
+    </item>
+    <item>
+     <widget class="QGroupBox" name="horizontalGroupBox">
+      <property name="minimumSize">
+       <size>
+        <width>0</width>
+        <height>100</height>
+       </size>
+      </property>
+      <property name="maximumSize">
+       <size>
+        <width>16777215</width>
+        <height>100</height>
+       </size>
+      </property>
+      <layout class="QHBoxLayout" name="horizontalLayout">
+       <item>
+        <widget class="QToolButton" name="toolButton1">
+         <property name="text">
+          <string>tool1</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QToolButton" name="toolButton2">
+         <property name="text">
+          <string>tool2</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QPushButton" name="pushButton1">
+         <property name="text">
+          <string>action1</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QPushButton" name="pushButton2">
+         <property name="text">
+          <string>action2</string>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+    </item>
+   </layout>
+  </widget>
+  <widget class="QMenuBar" name="menubar">
+   <property name="geometry">
+    <rect>
+     <x>0</x>
+     <y>0</y>
+     <width>903</width>
+     <height>22</height>
+    </rect>
+   </property>
+   <widget class="QMenu" name="menuTest">
+    <property name="title">
+     <string>Test</string>
+    </property>
+   </widget>
+   <addaction name="menuTest"/>
+  </widget>
+  <widget class="QStatusBar" name="statusbar"/>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>QCairoWidget</class>
+   <extends>QGraphicsView</extends>
+   <header location="global">QCairoWidget.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/Qt/SampleQtApplicationRunner.h	Wed Apr 22 14:05:47 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 "../../../Qt/QtStoneApplicationRunner.h"
+
+#if ORTHANC_ENABLE_QT != 1
+#error this file shall be included only with the ORTHANC_ENABLE_QT set to 1
+#endif
+
+namespace OrthancStone
+{
+  namespace Samples
+  {
+    class SampleQtApplicationRunner : public OrthancStone::QtStoneApplicationRunner
+    {
+    protected:
+      virtual void InitializeMainWindow(OrthancStone::NativeStoneApplicationContext& context)
+      {
+        window_.reset(application_.CreateQtMainWindow());
+      }
+    public:
+      SampleQtApplicationRunner(MessageBroker& broker,
+                                SampleApplicationBase& application)
+        : OrthancStone::QtStoneApplicationRunner(broker, application)
+      {
+      }
+
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/SampleApplicationBase.h	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,133 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../../Applications/IStoneApplication.h"
+#include "../../../Framework/Deprecated/Widgets/WorldSceneWidget.h"
+
+#if ORTHANC_ENABLE_WASM==1
+#include "../../../Platforms/Wasm/WasmPlatformApplicationAdapter.h"
+#include "../../../Platforms/Wasm/Defaults.h"
+#endif
+
+#if ORTHANC_ENABLE_QT==1
+#include "Qt/SampleMainWindow.h"
+#include "Qt/SampleMainWindowWithButtons.h"
+#endif
+
+namespace OrthancStone
+{
+  namespace Samples
+  {
+    class SampleApplicationBase : public IStoneApplication
+    {
+    private:
+      boost::shared_ptr<Deprecated::IWidget>  mainWidget_;
+
+    public:
+      virtual void Initialize(StoneApplicationContext* context,
+                              Deprecated::IStatusBar& statusBar,
+                              const boost::program_options::variables_map& parameters) ORTHANC_OVERRIDE
+      {
+      }
+
+      virtual std::string GetTitle() const ORTHANC_OVERRIDE
+      {
+        return "Stone of Orthanc - Sample";
+      }
+
+      /**
+       * In the basic samples, the commands are handled by the platform adapter and NOT
+       * by the application handler
+      */
+      virtual void HandleSerializedMessage(const char* data) ORTHANC_OVERRIDE {};
+
+
+      virtual void Finalize() ORTHANC_OVERRIDE {}
+
+      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
+
+      virtual void InitializeWasm() ORTHANC_OVERRIDE
+      {
+        AttachWidgetToWasmViewport("canvas", mainWidget_);
+      }
+
+      virtual WasmPlatformApplicationAdapter* CreateWasmApplicationAdapter(MessageBroker& broker)
+      {
+        return new WasmPlatformApplicationAdapter(broker, *this);
+      }
+#endif
+
+    };
+
+    // this application actually works in Qt and WASM
+    class SampleSingleCanvasWithButtonsApplicationBase : public SampleApplicationBase
+    {
+public:
+      virtual void OnPushButton1Clicked() {}
+      virtual void OnPushButton2Clicked() {}
+      virtual void OnTool1Clicked() {}
+      virtual void OnTool2Clicked() {}
+
+      virtual void GetButtonNames(std::string& pushButton1,
+                                  std::string& pushButton2,
+                                  std::string& tool1,
+                                  std::string& tool2
+                                  ) {
+        pushButton1 = "action1";
+        pushButton2 = "action2";
+        tool1 = "tool1";
+        tool2 = "tool2";
+      }
+
+#if ORTHANC_ENABLE_QT==1
+      virtual QStoneMainWindow* CreateQtMainWindow() {
+        return new SampleMainWindowWithButtons(dynamic_cast<OrthancStone::NativeStoneApplicationContext&>(*context_), *this);
+      }
+#endif
+
+    };
+
+    // this application actually works in SDL and WASM
+    class SampleSingleCanvasApplicationBase : public SampleApplicationBase
+    {
+public:
+
+#if ORTHANC_ENABLE_QT==1
+      virtual QStoneMainWindow* CreateQtMainWindow() {
+        return new SampleMainWindow(dynamic_cast<OrthancStone::NativeStoneApplicationContext&>(*context_), *this);
+      }
+#endif
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/SampleInteractor.h	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,131 @@
+/**
+ * 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 "SampleApplicationBase.h"
+
+#include "../../../Framework/Widgets/LayeredSceneWidget.h"
+#include "../../../Framework/Widgets/IWorldSceneInteractor.h"
+#include "../../../Framework/Toolbox/ParallelSlicesCursor.h"
+
+namespace OrthancStone
+{
+  namespace Samples
+  {
+    /**
+     * This is a basic mouse interactor for sample applications. It
+     * contains a set of parallel slices in the 3D space. The mouse
+     * wheel events make the widget change the slice that is
+     * displayed.
+     **/
+    class SampleInteractor : public IWorldSceneInteractor
+    {
+    private:
+      ParallelSlicesCursor   cursor_;
+
+    public:
+      SampleInteractor(VolumeImage& volume,
+                       VolumeProjection projection, 
+                       bool reverse)
+      {
+        std::unique_ptr<ParallelSlices> slices(volume.GetGeometry(projection, reverse));
+        cursor_.SetGeometry(*slices);
+      }
+
+      SampleInteractor(ISeriesLoader& series, 
+                       bool reverse)
+      {
+        if (reverse)
+        {
+          std::unique_ptr<ParallelSlices> slices(series.GetGeometry().Reverse());
+          cursor_.SetGeometry(*slices);
+        }
+        else
+        {
+          cursor_.SetGeometry(series.GetGeometry());
+        }
+      }
+
+      SampleInteractor(const ParallelSlices& slices)
+      {
+        cursor_.SetGeometry(slices);
+      }
+
+      ParallelSlicesCursor& GetCursor()
+      {
+        return cursor_;
+      }
+
+      void AddWidget(LayeredSceneWidget& widget)
+      {
+        widget.SetInteractor(*this);
+        widget.SetSlice(cursor_.GetCurrentSlice());
+      }
+
+      virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget,
+                                                          const ViewportGeometry& view,
+                                                          MouseButton button,
+                                                          double x,
+                                                          double y,
+                                                          IStatusBar* statusBar)
+      {
+        return NULL;
+      }
+
+      virtual void MouseOver(CairoContext& context,
+                             WorldSceneWidget& widget,
+                             const ViewportGeometry& view,
+                             double x,
+                             double y,
+                             IStatusBar* statusBar)
+      {
+      }
+
+      virtual void MouseWheel(WorldSceneWidget& widget,
+                              MouseWheelDirection direction,
+                              KeyboardModifiers modifiers,
+                              IStatusBar* statusBar)
+      {
+        if (cursor_.ApplyWheelEvent(direction, modifiers))
+        {
+          dynamic_cast<LayeredSceneWidget&>(widget).SetSlice(cursor_.GetCurrentSlice());
+        }
+      }
+
+      virtual void KeyPressed(WorldSceneWidget& widget,
+                              char key,
+                              KeyboardModifiers modifiers,
+                              IStatusBar* statusBar)
+      {
+      }
+
+      void LookupSliceContainingPoint(LayeredSceneWidget& widget,
+                                      const Vector& p)
+      {
+        if (cursor_.LookupSliceContainingPoint(p))
+        {
+          widget.SetSlice(cursor_.GetCurrentSlice());
+        }
+      }
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/SampleList.h	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,41 @@
+// The macro "ORTHANC_STONE_SAMPLE" must be set by the CMake script
+
+#if ORTHANC_STONE_SAMPLE == 1
+#include "EmptyApplication.h"
+typedef OrthancStone::Samples::EmptyApplication SampleApplication;
+
+#elif ORTHANC_STONE_SAMPLE == 2
+#include "TestPatternApplication.h"
+typedef OrthancStone::Samples::TestPatternApplication SampleApplication;
+
+#elif ORTHANC_STONE_SAMPLE == 3
+#include "SingleFrameApplication.h"
+typedef OrthancStone::Samples::SingleFrameApplication SampleApplication;
+
+#elif ORTHANC_STONE_SAMPLE == 4
+#include "SingleVolumeApplication.h"
+typedef OrthancStone::Samples::SingleVolumeApplication SampleApplication;
+
+#elif ORTHANC_STONE_SAMPLE == 5
+#include "BasicPetCtFusionApplication.h"
+typedef OrthancStone::Samples::BasicPetCtFusionApplication SampleApplication;
+
+#elif ORTHANC_STONE_SAMPLE == 6
+#include "SynchronizedSeriesApplication.h"
+typedef OrthancStone::Samples::SynchronizedSeriesApplication SampleApplication;
+
+#elif ORTHANC_STONE_SAMPLE == 7
+#include "LayoutPetCtFusionApplication.h"
+typedef OrthancStone::Samples::LayoutPetCtFusionApplication SampleApplication;
+
+#elif ORTHANC_STONE_SAMPLE == 8
+#include "SimpleViewerApplicationSingleFile.h"
+typedef OrthancStone::Samples::SimpleViewerApplication SampleApplication;
+
+#elif ORTHANC_STONE_SAMPLE == 9
+#include "SingleFrameEditorApplication.h"
+typedef OrthancStone::Samples::SingleFrameEditorApplication SampleApplication;
+
+#else
+#error Please set the ORTHANC_STONE_SAMPLE macro
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/SampleMainNative.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,44 @@
+/**
+ * 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 "SampleList.h"
+#if ORTHANC_ENABLE_SDL==1
+#include "../../Sdl/SdlStoneApplicationRunner.h"
+#endif
+#if ORTHANC_ENABLE_QT==1
+#include "Qt/SampleQtApplicationRunner.h"
+#endif
+
+int main(int argc, char* argv[]) 
+{
+  boost::shared_ptr<SampleApplication> sampleStoneApplication(new SampleApplication);
+
+#if ORTHANC_ENABLE_SDL==1
+  OrthancStone::SdlStoneApplicationRunner sdlApplicationRunner(sampleStoneApplication);
+  return sdlApplicationRunner.Execute(argc, argv);
+#endif
+  
+#if ORTHANC_ENABLE_QT==1
+  OrthancStone::Samples::SampleQtApplicationRunner qtAppRunner(sampleStoneApplication);
+  return qtAppRunner.Execute(argc, argv);
+#endif
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/SampleMainWasm.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,37 @@
+/**
+ * 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 "Platforms/Wasm/WasmWebService.h"
+#include "Platforms/Wasm/WasmViewport.h"
+
+#include <emscripten/emscripten.h>
+
+#include "SampleList.h"
+
+
+OrthancStone::IStoneApplication* CreateUserApplication(OrthancStone::MessageBroker& broker) 
+{
+  return new SampleApplication(broker);
+}
+
+OrthancStone::WasmPlatformApplicationAdapter* CreateWasmApplicationAdapter(OrthancStone::MessageBroker& broker, OrthancStone::IStoneApplication* application)
+{
+  return dynamic_cast<SampleApplication*>(application)->CreateWasmApplicationAdapter(broker);
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/Samples-status.md	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,103 @@
+Executable versions
+================
+Generic options
+----------------------
+```
+("help", "Display this help and exit")
+("verbose", "Be verbose in logs")
+("orthanc", boost::program_options::value<std::string>()
+  ->default_value("http://localhost:8042/"),
+  "URL to the Orthanc server")
+("username", "Username for the Orthanc server")
+("password", "Password for the Orthanc server")
+("https-verify", boost::program_options::value<bool>()
+  ->default_value(true), "Check HTTPS certificates")
+```
+OrthancStoneSimpleViewer
+-------------------------------------
+- Options:
+    ```
+    - "studyId", std::string, "Orthanc ID of the study"
+    ```
+- study loading works OK
+- Invert does not work:
+```
+void SimpleViewerApplication::ExecuteAction(SimpleViewerApplication::Actions action)
+  {
+    // TODO
+  }
+```
+
+OrthancStoneSimpleViewerSingleFile
+-------------------------------------
+- Options:
+    ```
+    - "studyId", std::string, "Orthanc ID of the study"
+    ```
+
+Study loading works.
+
+The `line` and `circle` buttons work and call this:
+```
+virtual void OnTool1Clicked()
+{
+  currentTool_ = Tools_LineMeasure;
+}
+
+virtual void OnTool2Clicked()
+{
+  currentTool_ = Tools_CircleMeasure;
+}
+```
+The `action1` and `action2` buttons are not connected
+
+The following is displayed in the console at launch time:
+```
+W0313 12:20:12.790449 NativeStoneApplicationRunner.cpp:55] Use the key "s" to reinitialize the layout
+W0313 12:20:12.790449 NativeStoneApplicationRunner.cpp:55] Use the key "n" to go to next image in the main viewport
+```
+However, when looking at `MainWidgetInteractor::KeyPressed` (`SimpleViewerApplicationSingleFile.h:169`), only the following is processed:
+- 's': reset layout
+- 'l': select line tool
+- 'c': select circle tool
+
+OrthancStoneSingleFrame
+-------------------------------------
+```
+generic.add_options()
+("instance", boost::program_options::value<std::string>(), 
+"Orthanc ID of the instance")
+("frame", boost::program_options::value<unsigned int>()
+  ->default_value(0),
+"Number of the frame, for multi-frame DICOM instances")
+("smooth", boost::program_options::value<bool>()
+  ->default_value(true), 
+"Enable bilinear interpolation to smooth the image");
+```
+only key handled in `KeyPressed` is `s` to call `widget.FitContent()`
+
+
+OrthancStoneSingleFrameEditor
+-------------------------------------
+```
+generic.add_options()
+("instance", boost::program_options::value<std::string>(),
+"Orthanc ID of the instance")
+("frame", boost::program_options::value<unsigned int>()
+  ->default_value(0),
+"Number of the frame, for multi-frame DICOM instances");
+```
+Available commands in `KeyPressed` (`SingleFrameEditorApplication.h:280`): 
+- 'a' widget.FitContent()
+- 'c' Crop tool
+- 'm' Mask tool
+- 'd' dump to json and diplay result (?)
+- 'e' export current view to Dicom with dummy tags (?)
+- 'i' wdiget.SwitchInvert
+- 't' Move tool
+- 'n' switch between nearest and bilinear interpolation
+- 'r' Rotate tool
+- 's' Resize tool
+- 'w' Windowing tool
+- 'ctrl+y' redo
+- 'ctrl+z' undo
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/SimpleViewer/AppStatus.h	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,27 @@
+#pragma once
+
+#include <string>
+
+
+namespace SimpleViewer
+{
+  struct AppStatus
+  {
+    std::string patientId;
+    std::string studyDescription;
+    std::string currentInstanceIdInMainViewport;
+    // note: if you add members here, update the serialization code below and deserialization in simple-viewer.ts -> onAppStatusUpdated()
+
+
+    AppStatus()
+    {
+    }
+
+    void ToJson(Json::Value &output) const
+    {
+      output["patientId"] = patientId;
+      output["studyDescription"] = studyDescription;
+      output["currentInstanceIdInMainViewport"] = currentInstanceIdInMainViewport;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/SimpleViewer/MainWidgetInteractor.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,111 @@
+/**
+ * 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 "MainWidgetInteractor.h"
+
+#include "SimpleViewerApplication.h"
+
+namespace SimpleViewer {
+
+  Deprecated::IWorldSceneMouseTracker* MainWidgetInteractor::CreateMouseTracker(Deprecated::WorldSceneWidget& widget,
+                                                                    const Deprecated::ViewportGeometry& view,
+                                                                    MouseButton button,
+                                                                    KeyboardModifiers modifiers,
+                                                                    int viewportX,
+                                                                    int viewportY,
+                                                                    double x,
+                                                                    double y,
+                                                                    Deprecated::IStatusBar* statusBar,
+                                                                    const std::vector<Deprecated::Touch>& displayTouches)
+  {
+    if (button == MouseButton_Left)
+    {
+      if (application_.GetCurrentTool() == Tool_LineMeasure)
+      {
+        return new Deprecated::LineMeasureTracker(statusBar, dynamic_cast<Deprecated::SliceViewerWidget&>(widget).GetSlice(),
+                                      x, y, 255, 0, 0, application_.GetFont());
+      }
+      else if (application_.GetCurrentTool() == Tool_CircleMeasure)
+      {
+        return new Deprecated::CircleMeasureTracker(statusBar, dynamic_cast<Deprecated::SliceViewerWidget&>(widget).GetSlice(),
+                                        x, y, 255, 0, 0, application_.GetFont());
+      }
+      else if (application_.GetCurrentTool() == Tool_Crop)
+      {
+        // TODO
+      }
+      else if (application_.GetCurrentTool() == Tool_Windowing)
+      {
+        // TODO
+      }
+      else if (application_.GetCurrentTool() == Tool_Zoom)
+      {
+        // TODO
+      }
+      else if (application_.GetCurrentTool() == Tool_Pan)
+      {
+        // TODO
+      }
+    }
+    return NULL;
+  }
+
+  void MainWidgetInteractor::MouseOver(CairoContext& context,
+                                       Deprecated::WorldSceneWidget& widget,
+                                       const Deprecated::ViewportGeometry& view,
+                                       double x,
+                                       double y,
+                                       Deprecated::IStatusBar* statusBar)
+  {
+    if (statusBar != NULL)
+    {
+      Vector p = dynamic_cast<Deprecated::SliceViewerWidget&>(widget).GetSlice().MapSliceToWorldCoordinates(x, y);
+
+      char buf[64];
+      sprintf(buf, "X = %.02f Y = %.02f Z = %.02f (in cm)",
+              p[0] / 10.0, p[1] / 10.0, p[2] / 10.0);
+      statusBar->SetMessage(buf);
+    }
+  }
+
+  void MainWidgetInteractor::MouseWheel(Deprecated::WorldSceneWidget& widget,
+                                        MouseWheelDirection direction,
+                                        KeyboardModifiers modifiers,
+                                        Deprecated::IStatusBar* statusBar)
+  {
+  }
+
+  void MainWidgetInteractor::KeyPressed(Deprecated::WorldSceneWidget& widget,
+                                        KeyboardKeys key,
+                                        char keyChar,
+                                        KeyboardModifiers modifiers,
+                                        Deprecated::IStatusBar* statusBar)
+  {
+    switch (keyChar)
+    {
+    case 's':
+      widget.FitContent();
+      break;
+
+    default:
+      break;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/SimpleViewer/MainWidgetInteractor.h	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,76 @@
+/**
+ * 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 "../../../../Framework/Deprecated/Widgets/IWorldSceneInteractor.h"
+
+using namespace OrthancStone;
+
+namespace SimpleViewer {
+
+  class SimpleViewerApplication;
+
+  class MainWidgetInteractor : public Deprecated::IWorldSceneInteractor
+  {
+  private:
+    SimpleViewerApplication&  application_;
+
+  public:
+    MainWidgetInteractor(SimpleViewerApplication&  application) :
+      application_(application)
+    {
+    }
+
+    /**
+        WorldSceneWidget: 
+    */
+    virtual Deprecated::IWorldSceneMouseTracker* CreateMouseTracker(Deprecated::WorldSceneWidget& widget,
+                                                                    const Deprecated::ViewportGeometry& view,
+                                                                    MouseButton button,
+                                                                    KeyboardModifiers modifiers,
+                                                                    int viewportX,
+                                                                    int viewportY,
+                                                                    double x,
+                                                                    double y,
+                                                                    Deprecated::IStatusBar* statusBar,
+                                                                    const std::vector<Deprecated::Touch>& displayTouches);
+
+    virtual void MouseOver(CairoContext& context,
+                           Deprecated::WorldSceneWidget& widget,
+                           const Deprecated::ViewportGeometry& view,
+                           double x,
+                           double y,
+                           Deprecated::IStatusBar* statusBar);
+
+    virtual void MouseWheel(Deprecated::WorldSceneWidget& widget,
+                            MouseWheelDirection direction,
+                            KeyboardModifiers modifiers,
+                            Deprecated::IStatusBar* statusBar);
+
+    virtual void KeyPressed(Deprecated::WorldSceneWidget& widget,
+                            KeyboardKeys key,
+                            char keyChar,
+                            KeyboardModifiers modifiers,
+                            Deprecated::IStatusBar* statusBar);
+  };
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/SimpleViewer/Qt/SimpleViewerMainWindow.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,109 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "SimpleViewerMainWindow.h"
+
+/**
+ * Don't use "ui_MainWindow.h" instead of <ui_MainWindow.h> below, as
+ * this makes CMake unable to detect when the UI file changes.
+ **/
+#include <ui_SimpleViewerMainWindow.h>
+#include "../../SimpleViewerApplication.h"
+
+
+namespace SimpleViewer
+{
+  template<typename T, typename U>
+  bool ExecuteCommand(U* handler, const T& command)
+  {
+    std::string serializedCommand = StoneSerialize(command);
+    StoneDispatchToHandler(serializedCommand, handler);
+  }
+
+  SimpleViewerMainWindow::SimpleViewerMainWindow(
+    OrthancStone::NativeStoneApplicationContext& context,
+    SimpleViewerApplication& stoneApplication,
+    QWidget *parent) :
+    QStoneMainWindow(context, parent),
+    ui_(new Ui::SimpleViewerMainWindow),
+    stoneApplication_(stoneApplication)
+  {
+    ui_->setupUi(this);
+    SetCentralStoneWidget(*ui_->cairoCentralWidget);
+
+#if QT_VERSION >= 0x050000
+    connect(ui_->toolButtonCrop, &QToolButton::clicked, this, &SimpleViewerMainWindow::cropClicked);
+    connect(ui_->pushButtonUndoCrop, &QToolButton::clicked, this, &SimpleViewerMainWindow::undoCropClicked);
+    connect(ui_->toolButtonLine, &QToolButton::clicked, this, &SimpleViewerMainWindow::lineClicked);
+    connect(ui_->toolButtonCircle, &QToolButton::clicked, this, &SimpleViewerMainWindow::circleClicked);
+    connect(ui_->toolButtonWindowing, &QToolButton::clicked, this, &SimpleViewerMainWindow::windowingClicked);
+    connect(ui_->pushButtonRotate, &QPushButton::clicked, this, &SimpleViewerMainWindow::rotateClicked);
+    connect(ui_->pushButtonInvert, &QPushButton::clicked, this, &SimpleViewerMainWindow::invertClicked);
+#else
+    connect(ui_->toolButtonCrop, SIGNAL(clicked()), this, SLOT(cropClicked()));
+    connect(ui_->toolButtonLine, SIGNAL(clicked()), this, SLOT(lineClicked()));
+    connect(ui_->toolButtonCircle, SIGNAL(clicked()), this, SLOT(circleClicked()));
+    connect(ui_->toolButtonWindowing, SIGNAL(clicked()), this, SLOT(windowingClicked()));
+    connect(ui_->pushButtonUndoCrop, SIGNAL(clicked()), this, SLOT(undoCropClicked()));
+    connect(ui_->pushButtonRotate, SIGNAL(clicked()), this, SLOT(rotateClicked()));
+    connect(ui_->pushButtonInvert, SIGNAL(clicked()), this, SLOT(invertClicked()));
+#endif
+  }
+
+  SimpleViewerMainWindow::~SimpleViewerMainWindow()
+  {
+    delete ui_;
+  }
+
+  void SimpleViewerMainWindow::cropClicked()
+  {
+    stoneApplication_.ExecuteCommand(SelectTool(Tool_Crop));
+  }
+
+  void SimpleViewerMainWindow::undoCropClicked()
+  {
+    stoneApplication_.ExecuteCommand(Action(ActionType_UndoCrop));
+  }
+
+  void SimpleViewerMainWindow::lineClicked()
+  {
+    stoneApplication_.ExecuteCommand(SelectTool(Tool_LineMeasure));
+  }
+
+  void SimpleViewerMainWindow::circleClicked()
+  {
+    stoneApplication_.ExecuteCommand(SelectTool(Tool_CircleMeasure));
+  }
+
+  void SimpleViewerMainWindow::windowingClicked()
+  {
+    stoneApplication_.ExecuteCommand(SelectTool(Tool_Windowing));
+  }
+
+  void SimpleViewerMainWindow::rotateClicked()
+  {
+    stoneApplication_.ExecuteCommand(Action(ActionType_Rotate));
+  }
+
+  void SimpleViewerMainWindow::invertClicked()
+  {
+    stoneApplication_.ExecuteCommand(Action(ActionType_Invert));
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/SimpleViewer/Qt/SimpleViewerMainWindow.h	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,57 @@
+/**
+ * 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 <Applications/Qt/QCairoWidget.h>
+#include <Applications/Qt/QStoneMainWindow.h>
+
+namespace Ui 
+{
+  class SimpleViewerMainWindow;
+}
+
+using namespace OrthancStone;
+
+namespace SimpleViewer
+{
+  class SimpleViewerApplication;
+
+  class SimpleViewerMainWindow : public QStoneMainWindow
+  {
+    Q_OBJECT
+
+  private:
+    Ui::SimpleViewerMainWindow*   ui_;
+    SimpleViewerApplication&      stoneApplication_;
+
+  public:
+    explicit SimpleViewerMainWindow(OrthancStone::NativeStoneApplicationContext& context, SimpleViewerApplication& stoneApplication, QWidget *parent = 0);
+    ~SimpleViewerMainWindow();
+
+  private slots:
+    void cropClicked();
+    void undoCropClicked();
+    void rotateClicked();
+    void windowingClicked();
+    void lineClicked();
+    void circleClicked();
+    void invertClicked();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/SimpleViewer/Qt/SimpleViewerMainWindow.ui	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,151 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SimpleViewerMainWindow</class>
+ <widget class="QMainWindow" name="SimpleViewerMainWindow">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>903</width>
+    <height>634</height>
+   </rect>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>500</width>
+    <height>300</height>
+   </size>
+  </property>
+  <property name="baseSize">
+   <size>
+    <width>500</width>
+    <height>300</height>
+   </size>
+  </property>
+  <property name="windowTitle">
+   <string>Stone of Orthanc</string>
+  </property>
+  <property name="layoutDirection">
+   <enum>Qt::LeftToRight</enum>
+  </property>
+  <widget class="QWidget" name="centralwidget">
+   <property name="sizePolicy">
+    <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+     <horstretch>0</horstretch>
+     <verstretch>0</verstretch>
+    </sizepolicy>
+   </property>
+   <property name="layoutDirection">
+    <enum>Qt::LeftToRight</enum>
+   </property>
+   <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0">
+    <property name="sizeConstraint">
+     <enum>QLayout::SetDefaultConstraint</enum>
+    </property>
+    <item>
+     <widget class="QCairoWidget" name="cairoCentralWidget">
+      <property name="minimumSize">
+       <size>
+        <width>0</width>
+        <height>500</height>
+       </size>
+      </property>
+     </widget>
+    </item>
+    <item>
+     <widget class="QGroupBox" name="horizontalGroupBox">
+      <property name="minimumSize">
+       <size>
+        <width>0</width>
+        <height>100</height>
+       </size>
+      </property>
+      <property name="maximumSize">
+       <size>
+        <width>16777215</width>
+        <height>100</height>
+       </size>
+      </property>
+      <layout class="QHBoxLayout" name="horizontalLayout">
+       <item>
+        <widget class="QToolButton" name="toolButtonWindowing">
+         <property name="text">
+          <string>windowing</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QToolButton" name="toolButtonCrop">
+         <property name="text">
+          <string>crop</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QPushButton" name="pushButtonUndoCrop">
+         <property name="text">
+          <string>undo crop</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QToolButton" name="toolButtonLine">
+         <property name="text">
+          <string>line</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QToolButton" name="toolButtonCircle">
+         <property name="text">
+          <string>circle</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QPushButton" name="pushButtonRotate">
+         <property name="text">
+          <string>rotate</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QPushButton" name="pushButtonInvert">
+         <property name="text">
+          <string>invert</string>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+    </item>
+   </layout>
+  </widget>
+  <widget class="QMenuBar" name="menubar">
+   <property name="geometry">
+    <rect>
+     <x>0</x>
+     <y>0</y>
+     <width>903</width>
+     <height>22</height>
+    </rect>
+   </property>
+   <widget class="QMenu" name="menuTest">
+    <property name="title">
+     <string>Test</string>
+    </property>
+   </widget>
+   <addaction name="menuTest"/>
+  </widget>
+  <widget class="QStatusBar" name="statusbar"/>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>QCairoWidget</class>
+   <extends>QGraphicsView</extends>
+   <header location="global">QCairoWidget.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/SimpleViewer/Qt/mainQt.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,14 @@
+#include "Applications/Qt/QtStoneApplicationRunner.h"
+
+#include "../../SimpleViewerApplication.h"
+#include "Framework/Messages/MessageBroker.h"
+
+
+int main(int argc, char* argv[]) 
+{
+  OrthancStone::MessageBroker broker;
+  SimpleViewer::SimpleViewerApplication stoneApplication(broker);
+
+  OrthancStone::QtStoneApplicationRunner qtAppRunner(broker, stoneApplication);
+  return qtAppRunner.Execute(argc, argv);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/SimpleViewer/SimpleViewerApplication.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,225 @@
+/**
+ * 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 "SimpleViewerApplication.h"
+
+#if ORTHANC_ENABLE_QT == 1
+#  include "Qt/SimpleViewerMainWindow.h"
+#endif
+
+#if ORTHANC_ENABLE_WASM == 1
+#  include <Platforms/Wasm/WasmViewport.h>
+#endif
+
+namespace SimpleViewer
+{
+
+  void SimpleViewerApplication::Initialize(StoneApplicationContext* context,
+                                           Deprecated::IStatusBar& statusBar,
+                                           const boost::program_options::variables_map& parameters)
+  {
+    context_ = context;
+    statusBar_ = &statusBar;
+
+    {// initialize viewports and layout
+      mainLayout_ = new Deprecated::LayoutWidget("main-layout");
+      mainLayout_->SetPadding(10);
+      mainLayout_->SetBackgroundCleared(true);
+      mainLayout_->SetBackgroundColor(0, 0, 0);
+      mainLayout_->SetHorizontal();
+
+      thumbnailsLayout_ = new Deprecated::LayoutWidget("thumbnail-layout");
+      thumbnailsLayout_->SetPadding(10);
+      thumbnailsLayout_->SetBackgroundCleared(true);
+      thumbnailsLayout_->SetBackgroundColor(50, 50, 50);
+      thumbnailsLayout_->SetVertical();
+
+      mainWidget_ = new Deprecated::SliceViewerWidget(IObserver::GetBroker(), "main-viewport");
+      //mainWidget_->RegisterObserver(*this);
+
+      // hierarchy
+      mainLayout_->AddWidget(thumbnailsLayout_);
+      mainLayout_->AddWidget(mainWidget_);
+
+      // sources
+      smartLoader_.reset(new Deprecated::SmartLoader(IObserver::GetBroker(), context->GetOrthancApiClient()));
+      smartLoader_->SetImageQuality(Deprecated::SliceImageQuality_FullPam);
+
+      mainLayout_->SetTransmitMouseOver(true);
+      mainWidgetInteractor_.reset(new MainWidgetInteractor(*this));
+      mainWidget_->SetInteractor(*mainWidgetInteractor_);
+      thumbnailInteractor_.reset(new ThumbnailInteractor(*this));
+    }
+
+    statusBar.SetMessage("Use the key \"s\" to reinitialize the layout");
+    statusBar.SetMessage("Use the key \"n\" to go to next image in the main viewport");
+
+
+    if (parameters.count("studyId") < 1)
+    {
+      LOG(WARNING) << "The study ID is missing, will take the first studyId found in Orthanc";
+      context->GetOrthancApiClient().GetJsonAsync("/studies", new Callable<SimpleViewerApplication, Deprecated::OrthancApiClient::JsonResponseReadyMessage>(*this, &SimpleViewerApplication::OnStudyListReceived));
+    }
+    else
+    {
+      SelectStudy(parameters["studyId"].as<std::string>());
+    }
+  }
+
+
+  void SimpleViewerApplication::DeclareStartupOptions(boost::program_options::options_description& options)
+  {
+    boost::program_options::options_description generic("Sample options");
+    generic.add_options()
+        ("studyId", boost::program_options::value<std::string>(),
+         "Orthanc ID of the study")
+        ;
+
+    options.add(generic);
+  }
+
+  void SimpleViewerApplication::OnStudyListReceived(const Deprecated::OrthancApiClient::JsonResponseReadyMessage& message)
+  {
+    const Json::Value& response = message.GetJson();
+
+    if (response.isArray() &&
+        response.size() >= 1)
+    {
+      SelectStudy(response[0].asString());
+    }
+  }
+  void SimpleViewerApplication::OnStudyReceived(const Deprecated::OrthancApiClient::JsonResponseReadyMessage& message)
+  {
+    const Json::Value& response = message.GetJson();
+
+    if (response.isObject() && response["Series"].isArray())
+    {
+      for (size_t i=0; i < response["Series"].size(); i++)
+      {
+        context_->GetOrthancApiClient().GetJsonAsync("/series/" + response["Series"][(int)i].asString(), new Callable<SimpleViewerApplication, Deprecated::OrthancApiClient::JsonResponseReadyMessage>(*this, &SimpleViewerApplication::OnSeriesReceived));
+      }
+    }
+  }
+
+  void SimpleViewerApplication::OnSeriesReceived(const Deprecated::OrthancApiClient::JsonResponseReadyMessage& message)
+  {
+    const Json::Value& response = message.GetJson();
+
+    if (response.isObject() &&
+        response["Instances"].isArray() &&
+        response["Instances"].size() > 0)
+    {
+      // keep track of all instances IDs
+      const std::string& seriesId = response["ID"].asString();
+      seriesTags_[seriesId] = response;
+      instancesIdsPerSeriesId_[seriesId] = std::vector<std::string>();
+      for (size_t i = 0; i < response["Instances"].size(); i++)
+      {
+        const std::string& instanceId = response["Instances"][static_cast<int>(i)].asString();
+        instancesIdsPerSeriesId_[seriesId].push_back(instanceId);
+      }
+
+      // load the first instance in the thumbnail
+      LoadThumbnailForSeries(seriesId, instancesIdsPerSeriesId_[seriesId][0]);
+
+      // if this is the first thumbnail loaded, load the first instance in the mainWidget
+      if (mainWidget_->GetLayerCount() == 0)
+      {
+        smartLoader_->SetFrameInWidget(*mainWidget_, 0, instancesIdsPerSeriesId_[seriesId][0], 0);
+      }
+    }
+  }
+
+  void SimpleViewerApplication::LoadThumbnailForSeries(const std::string& seriesId, const std::string& instanceId)
+  {
+    LOG(INFO) << "Loading thumbnail for series " << seriesId;
+    
+    Deprecated::SliceViewerWidget* thumbnailWidget = 
+      new Deprecated::SliceViewerWidget(IObserver::GetBroker(), "thumbnail-series-" + seriesId);
+    thumbnails_.push_back(thumbnailWidget);
+    thumbnailsLayout_->AddWidget(thumbnailWidget);
+    
+    thumbnailWidget->RegisterObserverCallback(
+      new Callable<SimpleViewerApplication, Deprecated::SliceViewerWidget::GeometryChangedMessage>
+      (*this, &SimpleViewerApplication::OnWidgetGeometryChanged));
+    
+    smartLoader_->SetFrameInWidget(*thumbnailWidget, 0, instanceId, 0);
+    thumbnailWidget->SetInteractor(*thumbnailInteractor_);
+  }
+
+  void SimpleViewerApplication::SelectStudy(const std::string& studyId)
+  {
+    context_->GetOrthancApiClient().GetJsonAsync("/studies/" + studyId, new Callable<SimpleViewerApplication, Deprecated::OrthancApiClient::JsonResponseReadyMessage>(*this, &SimpleViewerApplication::OnStudyReceived));
+  }
+
+  void SimpleViewerApplication::OnWidgetGeometryChanged(const Deprecated::SliceViewerWidget::GeometryChangedMessage& message)
+  {
+    // TODO: The "const_cast" could probably be replaced by "mainWidget_"
+    const_cast<Deprecated::SliceViewerWidget&>(message.GetOrigin()).FitContent();
+  }
+
+  void SimpleViewerApplication::SelectSeriesInMainViewport(const std::string& seriesId)
+  {
+    smartLoader_->SetFrameInWidget(*mainWidget_, 0, instancesIdsPerSeriesId_[seriesId][0], 0);
+  }
+
+  bool SimpleViewerApplication::Handle(const StoneSampleCommands::SelectTool& value)
+  {
+    currentTool_ = value.tool;
+    return true;
+  }
+
+  bool SimpleViewerApplication::Handle(const StoneSampleCommands::Action& value)
+  {
+    switch (value.type)
+    {
+    case ActionType_Invert:
+      // TODO
+      break;
+    case ActionType_UndoCrop:
+      // TODO
+      break;
+    case ActionType_Rotate:
+      // TODO
+      break;
+    default:
+      throw std::runtime_error("Action type not supported");
+    }
+    return true;
+  }
+
+#if ORTHANC_ENABLE_QT==1
+  QStoneMainWindow* SimpleViewerApplication::CreateQtMainWindow()
+  {
+    return new SimpleViewerMainWindow(dynamic_cast<OrthancStone::NativeStoneApplicationContext&>(*context_), *this);
+  }
+#endif
+
+#if ORTHANC_ENABLE_WASM==1
+  void SimpleViewerApplication::InitializeWasm() {
+
+    AttachWidgetToWasmViewport("canvasThumbnails", thumbnailsLayout_);
+    AttachWidgetToWasmViewport("canvasMain", mainWidget_);
+  }
+#endif
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/SimpleViewer/SimpleViewerApplication.h	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,175 @@
+/**
+ * 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
+
+ /*
+ This header contains the command definitions for the sample applications
+ */
+#include "Applications/Samples/StoneSampleCommands_generated.hpp"
+using namespace StoneSampleCommands;
+
+#include "Applications/IStoneApplication.h"
+
+#include "../../../../Framework/Deprecated/Layers/CircleMeasureTracker.h"
+#include "../../../../Framework/Deprecated/Layers/LineMeasureTracker.h"
+#include "../../../../Framework/Deprecated/SmartLoader.h"
+#include "../../../../Framework/Deprecated/Widgets/LayoutWidget.h"
+#include "../../../../Framework/Deprecated/Widgets/SliceViewerWidget.h"
+#include "../../../../Framework/Messages/IObserver.h"
+
+#if ORTHANC_ENABLE_WASM==1
+#include "Platforms/Wasm/WasmPlatformApplicationAdapter.h"
+#include "Platforms/Wasm/Defaults.h"
+#endif
+
+#if ORTHANC_ENABLE_QT==1
+#include "Qt/SimpleViewerMainWindow.h"
+#endif
+
+#include <Core/Images/Font.h>
+#include <Core/Logging.h>
+
+#include "ThumbnailInteractor.h"
+#include "MainWidgetInteractor.h"
+#include "AppStatus.h"
+
+using namespace OrthancStone;
+
+
+namespace SimpleViewer
+{
+
+  class SimpleViewerApplication
+    : public IStoneApplication
+    , public IObserver
+    , public IObservable
+    , public StoneSampleCommands::IHandler
+  {
+  public:
+
+    struct StatusUpdatedMessage : public IMessage
+    {
+      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
+
+      const AppStatus& status_;
+
+      StatusUpdatedMessage(const AppStatus& status)
+        : status_(status)
+      {
+      }
+    };
+
+  private:
+    Tool                                currentTool_;
+
+    std::unique_ptr<MainWidgetInteractor> mainWidgetInteractor_;
+    std::unique_ptr<ThumbnailInteractor>  thumbnailInteractor_;
+    Deprecated::LayoutWidget*                       mainLayout_;
+    Deprecated::LayoutWidget*                       thumbnailsLayout_;
+    Deprecated::SliceViewerWidget*                  mainWidget_;
+    std::vector<Deprecated::SliceViewerWidget*>     thumbnails_;
+    std::map<std::string, std::vector<std::string> > instancesIdsPerSeriesId_;
+    std::map<std::string, Json::Value>  seriesTags_;
+    unsigned int                        currentInstanceIndex_;
+    Deprecated::WidgetViewport*       wasmViewport1_;
+    Deprecated::WidgetViewport*       wasmViewport2_;
+
+    Deprecated::IStatusBar*                         statusBar_;
+    std::unique_ptr<Deprecated::SmartLoader>          smartLoader_;
+
+    Orthanc::Font                       font_;
+
+  public:
+    SimpleViewerApplication(MessageBroker& broker) :
+      IObserver(broker),
+      IObservable(broker),
+      currentTool_(StoneSampleCommands::Tool_LineMeasure),
+      mainLayout_(NULL),
+      currentInstanceIndex_(0),
+      wasmViewport1_(NULL),
+      wasmViewport2_(NULL)
+    {
+      font_.LoadFromResource(Orthanc::EmbeddedResources::FONT_UBUNTU_MONO_BOLD_16);
+    }
+
+    virtual void Finalize() ORTHANC_OVERRIDE {}
+    virtual Deprecated::IWidget* GetCentralWidget() ORTHANC_OVERRIDE {return mainLayout_;}
+
+    virtual void DeclareStartupOptions(boost::program_options::options_description& options) ORTHANC_OVERRIDE;
+    virtual void Initialize(StoneApplicationContext* context,
+                            Deprecated::IStatusBar& statusBar,
+                            const boost::program_options::variables_map& parameters) ORTHANC_OVERRIDE;
+
+    void OnStudyListReceived(const Deprecated::OrthancApiClient::JsonResponseReadyMessage& message);
+
+    void OnStudyReceived(const Deprecated::OrthancApiClient::JsonResponseReadyMessage& message);
+
+    void OnSeriesReceived(const Deprecated::OrthancApiClient::JsonResponseReadyMessage& message);
+
+    void LoadThumbnailForSeries(const std::string& seriesId, const std::string& instanceId);
+
+    void SelectStudy(const std::string& studyId);
+
+    void OnWidgetGeometryChanged(const Deprecated::SliceViewerWidget::GeometryChangedMessage& message);
+
+    void SelectSeriesInMainViewport(const std::string& seriesId);
+
+
+    Tool GetCurrentTool() const
+    {
+      return currentTool_;
+    }
+
+    const Orthanc::Font& GetFont() const
+    {
+      return font_;
+    }
+
+    // ExecuteAction method was empty (its body was a single "TODO" comment)
+    virtual bool Handle(const SelectTool& value) ORTHANC_OVERRIDE;
+    virtual bool Handle(const Action& value) ORTHANC_OVERRIDE;
+
+    template<typename T>
+    bool ExecuteCommand(const T& cmd)
+    {
+      std::string cmdStr = StoneSampleCommands::StoneSerialize(cmd);
+      return StoneSampleCommands::StoneDispatchToHandler(cmdStr, this);
+    }
+
+    virtual void HandleSerializedMessage(const char* data) ORTHANC_OVERRIDE
+    {
+      StoneSampleCommands::StoneDispatchToHandler(data, this);
+    }
+
+    virtual std::string GetTitle() const ORTHANC_OVERRIDE {return "SimpleViewer";}
+
+#if ORTHANC_ENABLE_WASM==1
+    virtual void InitializeWasm() ORTHANC_OVERRIDE;
+#endif
+
+#if ORTHANC_ENABLE_QT==1
+    virtual QStoneMainWindow* CreateQtMainWindow();
+#endif
+  };
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/SimpleViewer/ThumbnailInteractor.cpp	Wed Apr 22 14:05:47 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/>.
+ **/
+
+#include "ThumbnailInteractor.h"
+
+#include "SimpleViewerApplication.h"
+
+namespace SimpleViewer {
+
+  Deprecated::IWorldSceneMouseTracker* ThumbnailInteractor::CreateMouseTracker(Deprecated::WorldSceneWidget& widget,
+                                                                   const Deprecated::ViewportGeometry& view,
+                                                                   MouseButton button,
+                                                                   KeyboardModifiers modifiers,
+                                                                   int viewportX,
+                                                                   int viewportY,
+                                                                   double x,
+                                                                   double y,
+                                                                   Deprecated::IStatusBar* statusBar,
+                                                                   const std::vector<Deprecated::Touch>& displayTouches)
+  {
+    if (button == MouseButton_Left)
+    {
+      statusBar->SetMessage("selected thumbnail " + widget.GetName());
+      std::string seriesId = widget.GetName().substr(strlen("thumbnail-series-"));
+      application_.SelectSeriesInMainViewport(seriesId);
+    }
+    return NULL;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/SimpleViewer/ThumbnailInteractor.h	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,77 @@
+/**
+ * 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 "../../../../Framework/Deprecated/Widgets/IWorldSceneInteractor.h"
+
+using namespace OrthancStone;
+
+namespace SimpleViewer {
+
+  class SimpleViewerApplication;
+
+  class ThumbnailInteractor : public Deprecated::IWorldSceneInteractor
+  {
+  private:
+    SimpleViewerApplication&  application_;
+  public:
+    ThumbnailInteractor(SimpleViewerApplication&  application) :
+      application_(application)
+    {
+    }
+
+    virtual Deprecated::IWorldSceneMouseTracker* CreateMouseTracker(Deprecated::WorldSceneWidget& widget,
+                                                                    const Deprecated::ViewportGeometry& view,
+                                                                    MouseButton button,
+                                                                    KeyboardModifiers modifiers,
+                                                                    int viewportX,
+                                                                    int viewportY,
+                                                                    double x,
+                                                                    double y,
+                                                                    Deprecated::IStatusBar* statusBar,
+                                                                    const std::vector<Deprecated::Touch>& displayTouches);
+
+    virtual void MouseOver(CairoContext& context,
+                           Deprecated::WorldSceneWidget& widget,
+                           const Deprecated::ViewportGeometry& view,
+                           double x,
+                           double y,
+                           Deprecated::IStatusBar* statusBar)
+    {}
+
+    virtual void MouseWheel(Deprecated::WorldSceneWidget& widget,
+                            MouseWheelDirection direction,
+                            KeyboardModifiers modifiers,
+                            Deprecated::IStatusBar* statusBar)
+    {}
+
+    virtual void KeyPressed(Deprecated::WorldSceneWidget& widget,
+                            KeyboardKeys key,
+                            char keyChar,
+                            KeyboardModifiers modifiers,
+                            Deprecated::IStatusBar* statusBar)
+    {}
+
+  };
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/SimpleViewer/Wasm/SimpleViewerWasmApplicationAdapter.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,51 @@
+/**
+ * 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 "SimpleViewerWasmApplicationAdapter.h"
+
+namespace SimpleViewer
+{
+
+  SimpleViewerWasmApplicationAdapter::SimpleViewerWasmApplicationAdapter(MessageBroker &broker, SimpleViewerApplication &application)
+      : WasmPlatformApplicationAdapter(broker, application),
+        viewerApplication_(application)
+  {
+    application.RegisterObserverCallback(new Callable<SimpleViewerWasmApplicationAdapter, SimpleViewerApplication::StatusUpdatedMessage>(*this, &SimpleViewerWasmApplicationAdapter::OnStatusUpdated));
+  }
+
+  void SimpleViewerWasmApplicationAdapter::OnStatusUpdated(const SimpleViewerApplication::StatusUpdatedMessage &message)
+  {
+    Json::Value statusJson;
+    message.status_.ToJson(statusJson);
+
+    Json::Value event;
+    event["event"] = "appStatusUpdated";
+    event["data"] = statusJson;
+
+    Json::StreamWriterBuilder builder;
+    std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
+    std::ostringstream outputStr;
+
+    writer->write(event, &outputStr);
+
+    NotifyStatusUpdateFromCppToWebWithString(outputStr.str());
+  }
+
+} // namespace SimpleViewer
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/SimpleViewer/Wasm/SimpleViewerWasmApplicationAdapter.h	Wed Apr 22 14:05:47 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/>.
+ **/
+
+#pragma once
+
+#include <string>
+#include <Framework/Messages/IObserver.h>
+#include <Platforms/Wasm/WasmPlatformApplicationAdapter.h>
+
+#include "../../SimpleViewerApplication.h"
+
+namespace SimpleViewer {
+
+  class SimpleViewerWasmApplicationAdapter : public WasmPlatformApplicationAdapter
+    {
+      SimpleViewerApplication&  viewerApplication_;
+
+    public:
+      SimpleViewerWasmApplicationAdapter(MessageBroker& broker, SimpleViewerApplication& application);
+
+    private:
+      void OnStatusUpdated(const SimpleViewerApplication::StatusUpdatedMessage& message);
+
+    };
+
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/SimpleViewer/Wasm/mainWasm.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,38 @@
+/**
+ * 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 "Platforms/Wasm/WasmWebService.h"
+#include "Platforms/Wasm/WasmViewport.h"
+
+#include <emscripten/emscripten.h>
+
+#include "../../SimpleViewerApplication.h"
+#include "SimpleViewerWasmApplicationAdapter.h"
+
+
+OrthancStone::IStoneApplication* CreateUserApplication(OrthancStone::MessageBroker& broker) {
+  
+  return new SimpleViewer::SimpleViewerApplication(broker);
+}
+
+OrthancStone::WasmPlatformApplicationAdapter* CreateWasmApplicationAdapter(OrthancStone::MessageBroker& broker, IStoneApplication* application)
+{
+  return new SimpleViewer::SimpleViewerWasmApplicationAdapter(broker, *(dynamic_cast<SimpleViewer::SimpleViewerApplication*>(application)));
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/SimpleViewer/Wasm/simple-viewer.html	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,43 @@
+<!doctype html>
+
+<html lang="us">
+  <head>
+    <meta charset="utf-8" />
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+    <!-- Disable pinch zoom on mobile devices -->
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+    <meta name="HandheldFriendly" content="true" />
+
+    <title>Simple Viewer</title>
+    <link href="styles.css" rel="stylesheet" />
+
+<body>
+  <div id="breadcrumb">
+    <span id="label-patient-id"></span>
+    <span id="label-study-description"></span>
+    <span id="label-series-description"></span>
+  </div>
+  <div style="height: calc(100% - 50px)">
+    <div style="width: 20%; height: 100%; display: inline-block">
+      <canvas id="canvasThumbnails"></canvas>
+    </div>
+    <div style="width: 70%; height: 100%; display: inline-block">
+      <canvas id="canvasMain"></canvas>
+    </div>
+  </div>
+  <div id="toolbox" style="height: 50px">
+    <button tool-selector="line-measure" class="tool-selector">line</button>
+    <button tool-selector="circle-measure" class="tool-selector">circle</button>
+    <button tool-selector="crop" class="tool-selector">crop</button>
+    <button tool-selector="windowing" class="tool-selector">windowing</button>
+    <button tool-selector="zoom" class="tool-selector">zoom</button>
+    <button tool-selector="pan" class="tool-selector">pan</button>
+    <button action-trigger="rotate-left" class="action-trigger">rotate left</button>
+    <button action-trigger="rotate-right" class="action-trigger">rotate right</button>
+    <button action-trigger="invert" class="action-trigger">invert</button>
+  </div>
+  <script type="text/javascript" src="app-simple-viewer.js"></script>
+</body>
+
+</html>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/SimpleViewer/Wasm/simple-viewer.ts	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,81 @@
+import wasmApplicationRunner = require('../../../../Platforms/Wasm/wasm-application-runner');
+
+wasmApplicationRunner.InitializeWasmApplication("OrthancStoneSimpleViewer", "/orthanc");
+
+function SelectTool(toolName: string) {
+  var command = {
+    command: "selectTool:" + toolName,
+    commandType: "generic-no-arg-command",
+    args: {
+    }                                                                                                                       
+  };
+  wasmApplicationRunner.SendSerializedMessageToStoneApplication(JSON.stringify(command));
+}
+
+function PerformAction(actionName: string) {
+  var command = {
+    command: "action:" + actionName,
+    commandType: "generic-no-arg-command",
+    args: {
+    }
+  };
+  wasmApplicationRunner.SendSerializedMessageToStoneApplication(JSON.stringify(command));
+}
+
+class SimpleViewerUI {
+
+  private _labelPatientId: HTMLSpanElement;
+  private _labelStudyDescription: HTMLSpanElement;
+
+  public constructor() {
+    // install "SelectTool" handlers
+    document.querySelectorAll("[tool-selector]").forEach((e) => {
+      (e as HTMLButtonElement).addEventListener("click", () => {
+        SelectTool(e.attributes["tool-selector"].value);
+      });
+    });
+
+    // install "PerformAction" handlers
+    document.querySelectorAll("[action-trigger]").forEach((e) => {
+      (e as HTMLButtonElement).addEventListener("click", () => {
+        PerformAction(e.attributes["action-trigger"].value);
+      });
+    });
+
+    // connect all ui elements to members
+    this._labelPatientId = document.getElementById("label-patient-id") as HTMLSpanElement;
+    this._labelStudyDescription = document.getElementById("label-study-description") as HTMLSpanElement;
+  }
+
+  public onAppStatusUpdated(status: any) {
+    this._labelPatientId.innerText = status["patientId"];
+    this._labelStudyDescription.innerText = status["studyDescription"];
+    // this.highlighThumbnail(status["currentInstanceIdInMainViewport"]);
+  }
+
+}
+
+var ui = new SimpleViewerUI();
+
+// this method is called "from the C++ code" when the StoneApplication is updated.
+// it can be used to update the UI of the application
+function UpdateWebApplicationWithString(statusUpdateMessageString: string) {
+  console.log("updating web application with string: ", statusUpdateMessageString);
+  let statusUpdateMessage = JSON.parse(statusUpdateMessageString);
+
+  if ("event" in statusUpdateMessage) {
+    let eventName = statusUpdateMessage["event"];
+    if (eventName == "appStatusUpdated") {
+      ui.onAppStatusUpdated(statusUpdateMessage["data"]);
+    }
+  }
+}
+
+function UpdateWebApplicationWithSerializedMessage(statusUpdateMessageString: string) {
+  console.log("updating web application with serialized message: ", statusUpdateMessageString);
+  console.log("<not supported in the simple viewer!>");
+}
+
+// make it available to other js scripts in the application
+(<any> window).UpdateWebApplicationWithString = UpdateWebApplicationWithString;
+(<any> window).UpdateWebApplicationWithSerializedMessage = UpdateWebApplicationWithSerializedMessage;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/SimpleViewer/Wasm/styles.css	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,54 @@
+html, body {
+    width: 100%;
+    height: 100%;
+    margin: 0px;
+    border: 0;
+    overflow: hidden; /*  Disable scrollbars */
+    display: block;  /* No floating content on sides */
+    background-color: black;
+    color: white;
+    font-family: Arial, Helvetica, sans-serif;
+}
+
+canvas {
+    left:0px;
+    top:0px;
+}
+
+#canvas-group {
+    padding:5px;
+    background-color: grey;
+}
+
+#status-group {
+    padding:5px;
+}
+
+#worklist-group {
+    padding:5px;
+}
+
+.vsol-button {
+    height: 40px;
+}
+
+#thumbnails-group ul li {
+    display: inline;
+    list-style: none;
+}
+
+.thumbnail {
+    width: 100px;
+    height: 100px;
+    padding: 3px;
+}
+
+.thumbnail-selected {
+    border-width: 1px;
+    border-color: red;
+    border-style: solid;
+}
+
+#template-thumbnail-li {
+    display: none !important;
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/SimpleViewer/Wasm/tsconfig-simple-viewer.json	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,9 @@
+{
+    "extends" : "../../Web/tsconfig-samples",
+    "compilerOptions": {
+    },
+    "include" : [
+        "simple-viewer.ts",
+        "../../build-wasm/ApplicationCommands_generated.ts"
+    ]
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/SimpleViewerApplicationSingleFile.h	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,461 @@
+/**
+ * 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 "SampleApplicationBase.h"
+
+#include "../../../Framework/Deprecated/Layers/CircleMeasureTracker.h"
+#include "../../../Framework/Deprecated/Layers/LineMeasureTracker.h"
+#include "../../../Framework/Deprecated/SmartLoader.h"
+#include "../../../Framework/Deprecated/Widgets/LayoutWidget.h"
+#include "../../../Framework/Deprecated/Widgets/SliceViewerWidget.h"
+#include "../../../Framework/Messages/IObserver.h"
+
+#if ORTHANC_ENABLE_WASM==1
+#include "../../../Platforms/Wasm/WasmPlatformApplicationAdapter.h"
+#include "../../../Platforms/Wasm/Defaults.h"
+#endif
+
+#include <Core/Images/Font.h>
+#include <Core/Logging.h>
+
+namespace OrthancStone
+{
+  namespace Samples
+  {
+    class SimpleViewerApplication :
+      public SampleSingleCanvasWithButtonsApplicationBase,
+      public ObserverBase<SimpleViewerApplication>
+    {
+    private:
+      class ThumbnailInteractor : public Deprecated::IWorldSceneInteractor
+      {
+      private:
+        SimpleViewerApplication&  application_;
+
+      public:
+        ThumbnailInteractor(SimpleViewerApplication&  application) :
+          application_(application)
+        {
+        }
+
+        virtual Deprecated::IWorldSceneMouseTracker* CreateMouseTracker(Deprecated::WorldSceneWidget& widget,
+                                                            const Deprecated::ViewportGeometry& view,
+                                                            MouseButton button,
+                                                            KeyboardModifiers modifiers,
+                                                            int viewportX,
+                                                            int viewportY,
+                                                            double x,
+                                                            double y,
+                                                            Deprecated::IStatusBar* statusBar,
+                                                            const std::vector<Deprecated::Touch>& displayTouches)
+        {
+          if (button == MouseButton_Left)
+          {
+            statusBar->SetMessage("selected thumbnail " + widget.GetName());
+            std::string seriesId = widget.GetName().substr(strlen("thumbnail-series-"));
+            application_.SelectSeriesInMainViewport(seriesId);
+          }
+          return NULL;
+        }
+
+        virtual void MouseOver(CairoContext& context,
+                               Deprecated::WorldSceneWidget& widget,
+                               const Deprecated::ViewportGeometry& view,
+                               double x,
+                               double y,
+                               Deprecated::IStatusBar* statusBar)
+        {
+        }
+
+        virtual void MouseWheel(Deprecated::WorldSceneWidget& widget,
+                                MouseWheelDirection direction,
+                                KeyboardModifiers modifiers,
+                                Deprecated::IStatusBar* statusBar)
+        {
+        }
+
+        virtual void KeyPressed(Deprecated::WorldSceneWidget& widget,
+                                KeyboardKeys key,
+                                char keyChar,
+                                KeyboardModifiers modifiers,
+                                Deprecated::IStatusBar* statusBar)
+        {
+        }
+      };
+
+      class MainWidgetInteractor : public Deprecated::IWorldSceneInteractor
+      {
+      private:
+        SimpleViewerApplication&  application_;
+        
+      public:
+        MainWidgetInteractor(SimpleViewerApplication&  application) :
+          application_(application)
+        {
+        }
+        
+        virtual Deprecated::IWorldSceneMouseTracker* CreateMouseTracker(Deprecated::WorldSceneWidget& widget,
+                                                            const Deprecated::ViewportGeometry& view,
+                                                            MouseButton button,
+                                                            KeyboardModifiers modifiers,
+                                                            int viewportX,
+                                                            int viewportY,
+                                                            double x,
+                                                            double y,
+                                                            Deprecated::IStatusBar* statusBar,
+                                                            const std::vector<Deprecated::Touch>& displayTouches)
+        {
+          if (button == MouseButton_Left)
+          {
+            if (application_.currentTool_ == Tool_LineMeasure)
+            {
+              return new Deprecated::LineMeasureTracker(statusBar, dynamic_cast<Deprecated::SliceViewerWidget&>(widget).GetSlice(),
+                                            x, y, 255, 0, 0, application_.GetFont());
+            }
+            else if (application_.currentTool_ == Tool_CircleMeasure)
+            {
+              return new Deprecated::CircleMeasureTracker(statusBar, dynamic_cast<Deprecated::SliceViewerWidget&>(widget).GetSlice(),
+                                              x, y, 255, 0, 0, application_.GetFont());
+            }
+          }
+          return NULL;
+        }
+
+        virtual void MouseOver(CairoContext& context,
+                               Deprecated::WorldSceneWidget& widget,
+                               const Deprecated::ViewportGeometry& view,
+                               double x,
+                               double y,
+                               Deprecated::IStatusBar* statusBar)
+        {
+          if (statusBar != NULL)
+          {
+            Vector p = dynamic_cast<Deprecated::SliceViewerWidget&>(widget).GetSlice().MapSliceToWorldCoordinates(x, y);
+            
+            char buf[64];
+            sprintf(buf, "X = %.02f Y = %.02f Z = %.02f (in cm)",
+                    p[0] / 10.0, p[1] / 10.0, p[2] / 10.0);
+            statusBar->SetMessage(buf);
+          }
+        }
+
+        virtual void MouseWheel(Deprecated::WorldSceneWidget& widget,
+                                MouseWheelDirection direction,
+                                KeyboardModifiers modifiers,
+                                Deprecated::IStatusBar* statusBar)
+        {
+        }
+
+        virtual void KeyPressed(Deprecated::WorldSceneWidget& widget,
+                                KeyboardKeys key,
+                                char keyChar,
+                                KeyboardModifiers modifiers,
+                                Deprecated::IStatusBar* statusBar)
+        {
+          switch (keyChar)
+          {
+            case 's':
+              widget.FitContent();
+              break;
+
+            case 'l':
+              application_.currentTool_ = Tool_LineMeasure;
+              break;
+
+            case 'c':
+              application_.currentTool_ = Tool_CircleMeasure;
+              break;
+
+            default:
+              break;
+          }
+        }
+      };
+
+
+#if ORTHANC_ENABLE_WASM==1
+      class SimpleViewerApplicationAdapter : public WasmPlatformApplicationAdapter
+      {
+        SimpleViewerApplication&  viewerApplication_;
+
+      public:
+        SimpleViewerApplicationAdapter(SimpleViewerApplication& application)
+          : WasmPlatformApplicationAdapter(application),
+            viewerApplication_(application)
+        {
+        }
+
+        virtual void HandleSerializedMessageFromWeb(std::string& output, const std::string& input) 
+        {
+          if (input == "select-tool:line-measure")
+          {
+            viewerApplication_.currentTool_ = Tool_LineMeasure;
+            NotifyStatusUpdateFromCppToWebWithString("currentTool=line-measure");
+          }
+          else if (input == "select-tool:circle-measure")
+          {
+            viewerApplication_.currentTool_ = Tool_CircleMeasure;
+            NotifyStatusUpdateFromCppToWebWithString("currentTool=circle-measure");
+          }
+
+          output = "ok";
+        }
+
+        virtual void NotifySerializedMessageFromCppToWeb(const std::string& statusUpdateMessage) 
+        {
+          UpdateStoneApplicationStatusFromCppWithSerializedMessage(statusUpdateMessage.c_str());
+        }
+
+        virtual void NotifyStatusUpdateFromCppToWebWithString(const std::string& statusUpdateMessage) 
+        {
+          UpdateStoneApplicationStatusFromCppWithString(statusUpdateMessage.c_str());
+        }
+
+      };
+#endif
+      enum Tool {
+        Tool_LineMeasure,
+        Tool_CircleMeasure
+      };
+
+      Tool                                 currentTool_;
+      std::unique_ptr<MainWidgetInteractor>  mainWidgetInteractor_;
+      std::unique_ptr<ThumbnailInteractor>   thumbnailInteractor_;
+      Deprecated::LayoutWidget*                        mainLayout_;
+      Deprecated::LayoutWidget*                        thumbnailsLayout_;
+      std::vector<boost::shared_ptr<Deprecated::SliceViewerWidget> >      thumbnails_;
+
+      std::map<std::string, std::vector<std::string> > instancesIdsPerSeriesId_;
+      std::map<std::string, Json::Value> seriesTags_;
+
+      unsigned int                         currentInstanceIndex_;
+      Deprecated::WidgetViewport*        wasmViewport1_;
+      Deprecated::WidgetViewport*        wasmViewport2_;
+
+      Deprecated::IStatusBar*                          statusBar_;
+      std::unique_ptr<Deprecated::SmartLoader>           smartLoader_;
+
+      Orthanc::Font                        font_;
+
+    public:
+      SimpleViewerApplication() :
+        currentTool_(Tool_LineMeasure),
+        mainLayout_(NULL),
+        currentInstanceIndex_(0),
+        wasmViewport1_(NULL),
+        wasmViewport2_(NULL)
+      {
+        font_.LoadFromResource(Orthanc::EmbeddedResources::FONT_UBUNTU_MONO_BOLD_16);
+//        DeclareIgnoredMessage(MessageType_Widget_ContentChanged);
+      }
+
+      virtual void DeclareStartupOptions(boost::program_options::options_description& options)
+      {
+        boost::program_options::options_description generic("Sample options");
+        generic.add_options()
+          ("studyId", boost::program_options::value<std::string>(),
+           "Orthanc ID of the study")
+          ;
+
+        options.add(generic);
+      }
+
+      virtual void Initialize(StoneApplicationContext* context,
+                              Deprecated::IStatusBar& statusBar,
+                              const boost::program_options::variables_map& parameters)
+      {
+        using namespace OrthancStone;
+
+        context_ = context;
+        statusBar_ = &statusBar;
+
+        {// initialize viewports and layout
+          mainLayout_ = new Deprecated::LayoutWidget("main-layout");
+          mainLayout_->SetPadding(10);
+          mainLayout_->SetBackgroundCleared(true);
+          mainLayout_->SetBackgroundColor(0, 0, 0);
+          mainLayout_->SetHorizontal();
+
+          boost::shared_ptr<Deprecated::LayoutWidget> thumbnailsLayout_(new Deprecated::LayoutWidget("thumbnail-layout"));
+          thumbnailsLayout_->SetPadding(10);
+          thumbnailsLayout_->SetBackgroundCleared(true);
+          thumbnailsLayout_->SetBackgroundColor(50, 50, 50);
+          thumbnailsLayout_->SetVertical();
+
+          boost::shared_ptr<Deprecated::SliceViewerWidget> widget
+            (new Deprecated::SliceViewerWidget("main-viewport"));
+          SetCentralWidget(widget);
+          //mainWidget_->RegisterObserver(*this);
+
+          // hierarchy
+          mainLayout_->AddWidget(thumbnailsLayout_);
+          mainLayout_->AddWidget(widget);
+
+          // sources
+          smartLoader_.reset(new Deprecated::SmartLoader(context->GetOrthancApiClient()));
+          smartLoader_->SetImageQuality(Deprecated::SliceImageQuality_FullPam);
+
+          mainLayout_->SetTransmitMouseOver(true);
+          mainWidgetInteractor_.reset(new MainWidgetInteractor(*this));
+          widget->SetInteractor(*mainWidgetInteractor_);
+          thumbnailInteractor_.reset(new ThumbnailInteractor(*this));
+        }
+
+        statusBar.SetMessage("Use the key \"s\" to reinitialize the layout");
+        statusBar.SetMessage("Use the key \"n\" to go to next image in the main viewport");
+
+
+        if (parameters.count("studyId") < 1)
+        {
+          LOG(WARNING) << "The study ID is missing, will take the first studyId found in Orthanc";
+          context->GetOrthancApiClient()->GetJsonAsync(
+            "/studies",
+            new Deprecated::DeprecatedCallable<SimpleViewerApplication, Deprecated::OrthancApiClient::JsonResponseReadyMessage>
+            (GetSharedObserver(), &SimpleViewerApplication::OnStudyListReceived));
+        }
+        else
+        {
+          SelectStudy(parameters["studyId"].as<std::string>());
+        }
+      }
+
+      void OnStudyListReceived(const Deprecated::OrthancApiClient::JsonResponseReadyMessage& message)
+      {
+        const Json::Value& response = message.GetJson();
+
+        if (response.isArray() &&
+            response.size() >= 1)
+        {
+          SelectStudy(response[0].asString());
+        }
+      }
+      
+      void OnStudyReceived(const Deprecated::OrthancApiClient::JsonResponseReadyMessage& message)
+      {
+        const Json::Value& response = message.GetJson();
+
+        if (response.isObject() && response["Series"].isArray())
+        {
+          for (size_t i=0; i < response["Series"].size(); i++)
+          {
+            context_->GetOrthancApiClient()->GetJsonAsync(
+              "/series/" + response["Series"][(int)i].asString(),
+              new Deprecated::DeprecatedCallable<SimpleViewerApplication, Deprecated::OrthancApiClient::JsonResponseReadyMessage>
+              (GetSharedObserver(), &SimpleViewerApplication::OnSeriesReceived));
+          }
+        }
+      }
+
+      void OnSeriesReceived(const Deprecated::OrthancApiClient::JsonResponseReadyMessage& message)
+      {
+        const Json::Value& response = message.GetJson();
+
+        if (response.isObject() &&
+            response["Instances"].isArray() &&
+            response["Instances"].size() > 0)
+        {
+          // keep track of all instances IDs
+          const std::string& seriesId = response["ID"].asString();
+          seriesTags_[seriesId] = response;
+          instancesIdsPerSeriesId_[seriesId] = std::vector<std::string>();
+          for (size_t i = 0; i < response["Instances"].size(); i++)
+          {
+            const std::string& instanceId = response["Instances"][static_cast<int>(i)].asString();
+            instancesIdsPerSeriesId_[seriesId].push_back(instanceId);
+          }
+
+          // load the first instance in the thumbnail
+          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&>(*GetCentralWidget());
+          if (widget.GetLayerCount() == 0)
+          {
+            smartLoader_->SetFrameInWidget(widget, 0, instancesIdsPerSeriesId_[seriesId][0], 0);
+          }
+        }
+      }
+
+      void LoadThumbnailForSeries(const std::string& seriesId, const std::string& instanceId)
+      {
+        LOG(INFO) << "Loading thumbnail for series " << seriesId;
+        boost::shared_ptr<Deprecated::SliceViewerWidget> thumbnailWidget(new Deprecated::SliceViewerWidget("thumbnail-series-" + seriesId));
+        thumbnails_.push_back(thumbnailWidget);
+        thumbnailsLayout_->AddWidget(thumbnailWidget);
+        Register<Deprecated::SliceViewerWidget::GeometryChangedMessage>(*thumbnailWidget, &SimpleViewerApplication::OnWidgetGeometryChanged);
+        smartLoader_->SetFrameInWidget(*thumbnailWidget, 0, instanceId, 0);
+        thumbnailWidget->SetInteractor(*thumbnailInteractor_);
+      }
+
+      void SelectStudy(const std::string& studyId)
+      {
+        LOG(INFO) << "Selecting study: " << studyId;
+        context_->GetOrthancApiClient()->GetJsonAsync(
+          "/studies/" + studyId, new Deprecated::DeprecatedCallable<SimpleViewerApplication, Deprecated::OrthancApiClient::JsonResponseReadyMessage>
+          (GetSharedObserver(), &SimpleViewerApplication::OnStudyReceived));
+      }
+
+      void OnWidgetGeometryChanged(const Deprecated::SliceViewerWidget::GeometryChangedMessage& message)
+      {
+        // TODO: The "const_cast" could probably be replaced by "mainWidget"
+        const_cast<Deprecated::SliceViewerWidget&>(message.GetOrigin()).FitContent();
+      }
+
+      void SelectSeriesInMainViewport(const std::string& seriesId)
+      {
+        Deprecated::SliceViewerWidget& widget = dynamic_cast<Deprecated::SliceViewerWidget&>(*GetCentralWidget());
+        smartLoader_->SetFrameInWidget(widget, 0, instancesIdsPerSeriesId_[seriesId][0], 0);
+      }
+
+      const Orthanc::Font& GetFont() const
+      {
+        return font_;
+      }
+      
+      virtual void OnPushButton1Clicked() {}
+      virtual void OnPushButton2Clicked() {}
+      virtual void OnTool1Clicked() { currentTool_ = Tool_LineMeasure;}
+      virtual void OnTool2Clicked() { currentTool_ = Tool_CircleMeasure;}
+
+      virtual void GetButtonNames(std::string& pushButton1,
+                                  std::string& pushButton2,
+                                  std::string& tool1,
+                                  std::string& tool2)
+      {
+        tool1 = "line";
+        tool2 = "circle";
+        pushButton1 = "action1";
+        pushButton2 = "action2";
+      }
+
+#if ORTHANC_ENABLE_WASM==1
+      virtual void InitializeWasm()
+      {
+        AttachWidgetToWasmViewport("canvas", thumbnailsLayout_);
+        AttachWidgetToWasmViewport("canvas2", widget);
+      }
+#endif
+
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/SingleFrameApplication.h	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,268 @@
+/**
+ * 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 "SampleApplicationBase.h"
+
+#include "../../../Framework/Deprecated/Layers/DicomSeriesVolumeSlicer.h"
+#include "../../../Framework/Deprecated/Widgets/SliceViewerWidget.h"
+
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+
+#include <boost/math/constants/constants.hpp>
+
+
+namespace OrthancStone
+{
+  namespace Samples
+  {
+    class SingleFrameApplication :
+      public SampleSingleCanvasApplicationBase,
+      public ObserverBase<SingleFrameApplication>
+    {
+    private:
+      class Interactor : public Deprecated::IWorldSceneInteractor
+      {
+      private:
+        SingleFrameApplication&  application_;
+        
+      public:
+        Interactor(SingleFrameApplication&  application) :
+          application_(application)
+        {
+        }
+        
+        virtual Deprecated::IWorldSceneMouseTracker* CreateMouseTracker(Deprecated::WorldSceneWidget& widget,
+                                                                        const Deprecated::ViewportGeometry& view,
+                                                                        MouseButton button,
+                                                                        KeyboardModifiers modifiers,
+                                                                        int viewportX,
+                                                                        int viewportY,
+                                                                        double x,
+                                                                        double y,
+                                                                        Deprecated::IStatusBar* statusBar,
+                                                                        const std::vector<Deprecated::Touch>& displayTouches)
+        {
+          return NULL;
+        }
+
+        virtual void MouseOver(CairoContext& context,
+                               Deprecated::WorldSceneWidget& widget,
+                               const Deprecated::ViewportGeometry& view,
+                               double x,
+                               double y,
+                               Deprecated::IStatusBar* statusBar)
+        {
+          if (statusBar != NULL)
+          {
+            Vector p = dynamic_cast<Deprecated::SliceViewerWidget&>(widget).GetSlice().MapSliceToWorldCoordinates(x, y);
+            
+            char buf[64];
+            sprintf(buf, "X = %.02f Y = %.02f Z = %.02f (in cm)", 
+                    p[0] / 10.0, p[1] / 10.0, p[2] / 10.0);
+            statusBar->SetMessage(buf);
+          }
+        }
+
+        virtual void MouseWheel(Deprecated::WorldSceneWidget& widget,
+                                MouseWheelDirection direction,
+                                KeyboardModifiers modifiers,
+                                Deprecated::IStatusBar* statusBar)
+        {
+          int scale = (modifiers & KeyboardModifiers_Control ? 10 : 1);
+          
+          switch (direction)
+          {
+            case MouseWheelDirection_Up:
+              application_.OffsetSlice(-scale);
+              break;
+
+            case MouseWheelDirection_Down:
+              application_.OffsetSlice(scale);
+              break;
+
+            default:
+              break;
+          }
+        }
+
+        virtual void KeyPressed(Deprecated::WorldSceneWidget& widget,
+                                KeyboardKeys key,
+                                char keyChar,
+                                KeyboardModifiers modifiers,
+                                Deprecated::IStatusBar* statusBar)
+        {
+          switch (keyChar)
+          {
+            case 's':
+              widget.FitContent();
+              break;
+
+            default:
+              break;
+          }
+        }
+      };
+
+
+      void OffsetSlice(int offset)
+      {
+        if (source_)
+        {
+          int slice = static_cast<int>(slice_) + offset;
+
+          if (slice < 0)
+          {
+            slice = 0;
+          }
+
+          if (slice >= static_cast<int>(source_->GetSlicesCount()))
+          {
+            slice = static_cast<int>(source_->GetSlicesCount()) - 1;
+          }
+
+          if (slice != static_cast<int>(slice_)) 
+          {
+            SetSlice(slice);
+          }   
+        }
+      }
+
+
+      void SetSlice(size_t index)
+      {
+        if (source_ &&
+            index < source_->GetSlicesCount())
+        {
+          slice_ = static_cast<unsigned int>(index);
+          
+#if 1
+          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>();
+
+#if 1
+          Vector x; GeometryToolbox::AssignVector(x, cos(a), sin(a), 0);
+          Vector y; GeometryToolbox::AssignVector(y, -sin(a), cos(a), 0);
+#else
+          // Flip the normal
+          Vector x; GeometryToolbox::AssignVector(x, cos(a), sin(a), 0);
+          Vector y; GeometryToolbox::AssignVector(y, sin(a), -cos(a), 0);
+#endif
+          
+          SliceGeometry s(source_->GetSlice(slice_).GetGeometry().GetOrigin(), x, y);
+          widget_->SetSlice(s);
+#endif
+        }
+      }
+        
+      
+      void OnMainWidgetGeometryReady(const Deprecated::IVolumeSlicer::GeometryReadyMessage& message)
+      {
+        // Once the geometry of the series is downloaded from Orthanc,
+        // display its middle slice, and adapt the viewport to fit this
+        // slice
+        if (source_ &&
+            source_.get() == &message.GetOrigin())
+        {
+          SetSlice(source_->GetSlicesCount() / 2);
+        }
+
+        widget_->FitContent();
+      }
+
+      boost::shared_ptr<Deprecated::SliceViewerWidget>  widget_;
+      std::unique_ptr<Interactor>         mainWidgetInteractor_;
+      boost::shared_ptr<Deprecated::DicomSeriesVolumeSlicer> source_;
+      unsigned int                      slice_;
+
+    public:
+      SingleFrameApplication() :
+        slice_(0)
+      {
+      }
+      
+      virtual void DeclareStartupOptions(boost::program_options::options_description& options)
+      {
+        boost::program_options::options_description generic("Sample options");
+        generic.add_options()
+          ("instance", boost::program_options::value<std::string>(), 
+           "Orthanc ID of the instance")
+          ("frame", boost::program_options::value<unsigned int>()->default_value(0),
+           "Number of the frame, for multi-frame DICOM instances")
+          ("smooth", boost::program_options::value<bool>()->default_value(true), 
+           "Enable bilinear interpolation to smooth the image")
+          ;
+
+        options.add(generic);    
+      }
+
+      virtual void Initialize(StoneApplicationContext* context,
+                              Deprecated::IStatusBar& statusBar,
+                              const boost::program_options::variables_map& parameters)
+      {
+        using namespace OrthancStone;
+
+        context_ = context;
+
+        statusBar.SetMessage("Use the key \"s\" to reinitialize the layout");
+
+        if (parameters.count("instance") != 1)
+        {
+          LOG(ERROR) << "The instance ID is missing";
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+
+        std::string instance = parameters["instance"].as<std::string>();
+        int frame = parameters["frame"].as<unsigned int>();
+
+        widget_.reset(new Deprecated::SliceViewerWidget("main-widget"));
+        SetCentralWidget(widget_);
+
+        boost::shared_ptr<Deprecated::DicomSeriesVolumeSlicer> layer(new Deprecated::DicomSeriesVolumeSlicer);
+        layer->Connect(context->GetOrthancApiClient());
+        source_ = layer;
+
+        layer->LoadFrame(instance, frame);
+        Register<Deprecated::IVolumeSlicer::GeometryReadyMessage>(*layer, &SingleFrameApplication::OnMainWidgetGeometryReady);
+        widget_->AddLayer(layer);
+
+        Deprecated::RenderStyle s;
+
+        if (parameters["smooth"].as<bool>())
+        {
+          s.interpolation_ = ImageInterpolation_Bilinear;
+        }
+
+        widget_->SetLayerStyle(0, s);
+        widget_->SetTransmitMouseOver(true);
+
+        mainWidgetInteractor_.reset(new Interactor(*this));
+        widget_->SetInteractor(*mainWidgetInteractor_);
+      }
+    };
+
+
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/SingleFrameEditorApplication.h	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,531 @@
+/**
+ * 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 "SampleApplicationBase.h"
+
+#include "../../../Framework/Radiography/RadiographyLayerCropTracker.h"
+#include "../../../Framework/Radiography/RadiographyLayerMaskTracker.h"
+#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/Toolbox/TextRenderer.h"
+
+#include <Core/HttpClient.h>
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+#include <Core/Images/PngWriter.h>
+#include <Core/Images/PngReader.h>
+
+
+// Export using PAM is faster than using PNG, but requires Orthanc
+// core >= 1.4.3
+#define EXPORT_USING_PAM  1
+
+
+namespace OrthancStone
+{
+  namespace Samples
+  {
+    class RadiographyEditorInteractor :
+        public Deprecated::IWorldSceneInteractor,
+        public ObserverBase<RadiographyEditorInteractor>
+    {
+    private:
+      enum Tool
+      {
+        Tool_Move,
+        Tool_Rotate,
+        Tool_Crop,
+        Tool_Resize,
+        Tool_Mask,
+        Tool_Windowing
+      };
+
+
+      StoneApplicationContext*  context_;
+      UndoRedoStack             undoRedoStack_;
+      Tool                      tool_;
+      RadiographyMaskLayer*     maskLayer_;
+
+
+      static double GetHandleSize()
+      {
+        return 10.0;
+      }
+
+
+    public:
+      RadiographyEditorInteractor() :
+        context_(NULL),
+        tool_(Tool_Move),
+        maskLayer_(NULL)
+      {
+      }
+
+      void SetContext(StoneApplicationContext& context)
+      {
+        context_ = &context;
+      }
+
+      void SetMaskLayer(RadiographyMaskLayer* maskLayer)
+      {
+        maskLayer_ = maskLayer;
+      }
+      virtual Deprecated::IWorldSceneMouseTracker* CreateMouseTracker(Deprecated::WorldSceneWidget& worldWidget,
+                                                                      const Deprecated::ViewportGeometry& view,
+                                                                      MouseButton button,
+                                                                      KeyboardModifiers modifiers,
+                                                                      int viewportX,
+                                                                      int viewportY,
+                                                                      double x,
+                                                                      double y,
+                                                                      Deprecated::IStatusBar* statusBar,
+                                                                      const std::vector<Deprecated::Touch>& displayTouches)
+      {
+        RadiographyWidget& widget = dynamic_cast<RadiographyWidget&>(worldWidget);
+
+        if (button == MouseButton_Left)
+        {
+          size_t selected;
+
+          if (tool_ == Tool_Windowing)
+          {
+            return new RadiographyWindowingTracker(
+                  undoRedoStack_,
+                  widget.GetScene(),
+                  widget,
+                  OrthancStone::ImageInterpolation_Nearest,
+                  viewportX, viewportY,
+                  RadiographyWindowingTracker::Action_DecreaseWidth,
+                  RadiographyWindowingTracker::Action_IncreaseWidth,
+                  RadiographyWindowingTracker::Action_DecreaseCenter,
+                  RadiographyWindowingTracker::Action_IncreaseCenter);
+          }
+          else if (!widget.LookupSelectedLayer(selected))
+          {
+            // No layer is currently selected
+            size_t layer;
+            if (widget.GetScene().LookupLayer(layer, x, y))
+            {
+              widget.Select(layer);
+            }
+
+            return NULL;
+          }
+          else if (tool_ == Tool_Crop ||
+                   tool_ == Tool_Resize ||
+                   tool_ == Tool_Mask)
+          {
+            RadiographyScene::LayerAccessor accessor(widget.GetScene(), selected);
+            
+            ControlPoint controlPoint;
+            if (accessor.GetLayer().LookupControlPoint(controlPoint, x, y, view.GetZoom(), GetHandleSize()))
+            {
+              switch (tool_)
+              {
+              case Tool_Crop:
+                return new RadiographyLayerCropTracker
+                    (undoRedoStack_, widget.GetScene(), view, selected, controlPoint);
+
+              case Tool_Mask:
+                return new RadiographyLayerMaskTracker
+                    (undoRedoStack_, widget.GetScene(), view, selected, controlPoint);
+
+              case Tool_Resize:
+                return new RadiographyLayerResizeTracker
+                    (undoRedoStack_, widget.GetScene(), selected, controlPoint,
+                     (modifiers & KeyboardModifiers_Shift));
+
+              default:
+                throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+              }
+            }
+            else
+            {
+              size_t layer;
+
+              if (widget.GetScene().LookupLayer(layer, x, y))
+              {
+                widget.Select(layer);
+              }
+              else
+              {
+                widget.Unselect();
+              }
+
+              return NULL;
+            }
+          }
+          else
+          {
+            size_t layer;
+
+            if (widget.GetScene().LookupLayer(layer, x, y))
+            {
+              if (layer == selected)
+              {
+                switch (tool_)
+                {
+                case Tool_Move:
+                  return new RadiographyLayerMoveTracker
+                      (undoRedoStack_, widget.GetScene(), layer, x, y,
+                       (modifiers & KeyboardModifiers_Shift));
+
+                case Tool_Rotate:
+                  return new RadiographyLayerRotateTracker
+                      (undoRedoStack_, widget.GetScene(), view, layer, x, y,
+                       (modifiers & KeyboardModifiers_Shift));
+
+                default:
+                  break;
+                }
+
+                return NULL;
+              }
+              else
+              {
+                widget.Select(layer);
+                return NULL;
+              }
+            }
+            else
+            {
+              widget.Unselect();
+              return NULL;
+            }
+          }
+        }
+        else
+        {
+          return NULL;
+        }
+        return NULL;
+      }
+
+      virtual void MouseOver(CairoContext& context,
+                             Deprecated::WorldSceneWidget& worldWidget,
+                             const Deprecated::ViewportGeometry& view,
+                             double x,
+                             double y,
+                             Deprecated::IStatusBar* statusBar)
+      {
+        RadiographyWidget& widget = dynamic_cast<RadiographyWidget&>(worldWidget);
+
+#if 0
+        if (statusBar != NULL)
+        {
+          char buf[64];
+          sprintf(buf, "X = %.02f Y = %.02f (in cm)", x / 10.0, y / 10.0);
+          statusBar->SetMessage(buf);
+        }
+#endif
+
+        size_t selected;
+
+        if (widget.LookupSelectedLayer(selected) &&
+            (tool_ == Tool_Crop ||
+             tool_ == Tool_Resize ||
+             tool_ == Tool_Mask))
+        {
+          RadiographyScene::LayerAccessor accessor(widget.GetScene(), selected);
+
+          ControlPoint controlPoint;
+          if (accessor.GetLayer().LookupControlPoint(controlPoint, x, y, view.GetZoom(), GetHandleSize()))
+          {
+            double z = 1.0 / view.GetZoom();
+
+            context.SetSourceColor(255, 0, 0);
+            cairo_t* cr = context.GetObject();
+            cairo_set_line_width(cr, 2.0 * z);
+            cairo_move_to(cr, controlPoint.x - GetHandleSize() * z, controlPoint.y - GetHandleSize() * z);
+            cairo_line_to(cr, controlPoint.x + GetHandleSize() * z, controlPoint.y - GetHandleSize() * z);
+            cairo_line_to(cr, controlPoint.x + GetHandleSize() * z, controlPoint.y + GetHandleSize() * z);
+            cairo_line_to(cr, controlPoint.x - GetHandleSize() * z, controlPoint.y + GetHandleSize() * z);
+            cairo_line_to(cr, controlPoint.x - GetHandleSize() * z, controlPoint.y - GetHandleSize() * z);
+            cairo_stroke(cr);
+          }
+        }
+      }
+
+      virtual void MouseWheel(Deprecated::WorldSceneWidget& widget,
+                              MouseWheelDirection direction,
+                              KeyboardModifiers modifiers,
+                              Deprecated::IStatusBar* statusBar)
+      {
+      }
+
+      virtual void KeyPressed(Deprecated::WorldSceneWidget& worldWidget,
+                              KeyboardKeys key,
+                              char keyChar,
+                              KeyboardModifiers modifiers,
+                              Deprecated::IStatusBar* statusBar)
+      {
+        RadiographyWidget& widget = dynamic_cast<RadiographyWidget&>(worldWidget);
+
+        switch (keyChar)
+        {
+        case 'a':
+          widget.FitContent();
+          break;
+
+        case 'c':
+          tool_ = Tool_Crop;
+          break;
+
+        case 'm':
+          tool_ = Tool_Mask;
+          widget.Select(1);
+          break;
+
+        case 'd':
+        {
+          // dump to json and reload
+          Json::Value snapshot;
+          RadiographySceneWriter writer;
+          writer.Write(snapshot, widget.GetScene());
+
+          LOG(INFO) << "JSON export was successful: "
+                    << snapshot.toStyledString();
+
+          boost::shared_ptr<RadiographyScene> scene(new RadiographyScene);
+          RadiographySceneReader reader(*scene, *context_->GetOrthancApiClient());
+          reader.Read(snapshot);
+
+          widget.SetScene(scene);
+        };break;
+
+        case 'e':
+        {
+          Orthanc::DicomMap tags;
+
+          // Minimal set of tags to generate a valid CR image
+          tags.SetValue(Orthanc::DICOM_TAG_ACCESSION_NUMBER, "NOPE", false);
+          tags.SetValue(Orthanc::DICOM_TAG_BODY_PART_EXAMINED, "PELVIS", false);
+          tags.SetValue(Orthanc::DICOM_TAG_INSTANCE_NUMBER, "1", false);
+          //tags.SetValue(Orthanc::DICOM_TAG_LATERALITY, "", false);
+          tags.SetValue(Orthanc::DICOM_TAG_MANUFACTURER, "OSIMIS", false);
+          tags.SetValue(Orthanc::DICOM_TAG_MODALITY, "CR", false);
+          tags.SetValue(Orthanc::DICOM_TAG_PATIENT_BIRTH_DATE, "20000101", false);
+          tags.SetValue(Orthanc::DICOM_TAG_PATIENT_ID, "hello", false);
+          tags.SetValue(Orthanc::DICOM_TAG_PATIENT_NAME, "HELLO^WORLD", false);
+          tags.SetValue(Orthanc::DICOM_TAG_PATIENT_ORIENTATION, "", false);
+          tags.SetValue(Orthanc::DICOM_TAG_PATIENT_SEX, "M", false);
+          tags.SetValue(Orthanc::DICOM_TAG_REFERRING_PHYSICIAN_NAME, "HOUSE^MD", false);
+          tags.SetValue(Orthanc::DICOM_TAG_SERIES_NUMBER, "1", false);
+          tags.SetValue(Orthanc::DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.1", false);
+          tags.SetValue(Orthanc::DICOM_TAG_STUDY_ID, "STUDY", false);
+          tags.SetValue(Orthanc::DICOM_TAG_VIEW_POSITION, "", false);
+
+          if (context_ != NULL)
+          {
+            widget.GetScene().ExportDicom(*context_->GetOrthancApiClient(),
+                                          tags, std::string(), 0.1, 0.1, widget.IsInverted(),
+                                          false /* autoCrop */, widget.GetInterpolation(), EXPORT_USING_PAM);
+          }
+
+          break;
+        }
+
+        case 'i':
+          widget.SwitchInvert();
+          break;
+
+        case 't':
+          tool_ = Tool_Move;
+          break;
+
+        case 'n':
+        {
+          switch (widget.GetInterpolation())
+          {
+          case ImageInterpolation_Nearest:
+            LOG(INFO) << "Switching to bilinear interpolation";
+            widget.SetInterpolation(ImageInterpolation_Bilinear);
+            break;
+
+          case ImageInterpolation_Bilinear:
+            LOG(INFO) << "Switching to nearest neighbor interpolation";
+            widget.SetInterpolation(ImageInterpolation_Nearest);
+            break;
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+          }
+          
+          break;
+        }
+
+        case 'r':
+          tool_ = Tool_Rotate;
+          break;
+
+        case 's':
+          tool_ = Tool_Resize;
+          break;
+
+        case 'w':
+          tool_ = Tool_Windowing;
+          break;
+
+        case 'y':
+          if (modifiers & KeyboardModifiers_Control)
+          {
+            undoRedoStack_.Redo();
+            widget.NotifyContentChanged();
+          }
+          break;
+
+        case 'z':
+          if (modifiers & KeyboardModifiers_Control)
+          {
+            undoRedoStack_.Undo();
+            widget.NotifyContentChanged();
+          }
+          break;
+
+        default:
+          break;
+        }
+      }
+    };
+
+
+
+    class SingleFrameEditorApplication :
+        public SampleSingleCanvasApplicationBase,
+        public IObserver
+    {
+    private:
+      boost::shared_ptr<RadiographyScene>   scene_;
+      RadiographyEditorInteractor           interactor_;
+      RadiographyMaskLayer*                 maskLayer_;
+
+    public:
+      virtual ~SingleFrameEditorApplication()
+      {
+        LOG(WARNING) << "Destroying the application";
+      }
+      
+      virtual void DeclareStartupOptions(boost::program_options::options_description& options)
+      {
+        boost::program_options::options_description generic("Sample options");
+        generic.add_options()
+            ("instance", boost::program_options::value<std::string>(),
+             "Orthanc ID of the instance")
+            ("frame", boost::program_options::value<unsigned int>()->default_value(0),
+             "Number of the frame, for multi-frame DICOM instances")
+            ;
+
+        options.add(generic);
+      }
+
+      virtual void Initialize(StoneApplicationContext* context,
+                              Deprecated::IStatusBar& statusBar,
+                              const boost::program_options::variables_map& parameters)
+      {
+        using namespace OrthancStone;
+
+        context_ = context;
+        interactor_.SetContext(*context);
+
+        statusBar.SetMessage("Use the key \"a\" to reinitialize the layout");
+        statusBar.SetMessage("Use the key \"c\" to crop");
+        statusBar.SetMessage("Use the key \"e\" to export DICOM to the Orthanc server");
+        statusBar.SetMessage("Use the key \"f\" to switch full screen");
+        statusBar.SetMessage("Use the key \"i\" to invert contrast");
+        statusBar.SetMessage("Use the key \"m\" to modify the mask");
+        statusBar.SetMessage("Use the key \"n\" to switch between nearest neighbor and bilinear interpolation");
+        statusBar.SetMessage("Use the key \"r\" to rotate objects");
+        statusBar.SetMessage("Use the key \"s\" to resize objects (not applicable to DICOM layers)");
+        statusBar.SetMessage("Use the key \"t\" to move (translate) objects");
+        statusBar.SetMessage("Use the key \"w\" to change windowing");
+        
+        statusBar.SetMessage("Use the key \"ctrl-z\" to undo action");
+        statusBar.SetMessage("Use the key \"ctrl-y\" to redo action");
+
+        if (parameters.count("instance") != 1)
+        {
+          LOG(ERROR) << "The instance ID is missing";
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+
+        std::string instance = parameters["instance"].as<std::string>();
+        //int frame = parameters["frame"].as<unsigned int>();
+
+        scene_.reset(new RadiographyScene);
+        
+        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);
+
+#if !defined(ORTHANC_ENABLE_WASM) || ORTHANC_ENABLE_WASM != 1
+        Orthanc::HttpClient::ConfigureSsl(true, "/etc/ssl/certs/ca-certificates.crt");
+#endif
+        
+        //scene_->LoadDicomWebFrame(context->GetWebService());
+        
+        std::vector<Orthanc::ImageProcessing::ImagePoint> mask;
+        mask.push_back(Orthanc::ImageProcessing::ImagePoint(1100, 100));
+        mask.push_back(Orthanc::ImageProcessing::ImagePoint(1100, 1000));
+        mask.push_back(Orthanc::ImageProcessing::ImagePoint(2000, 1000));
+        mask.push_back(Orthanc::ImageProcessing::ImagePoint(2200, 150));
+        mask.push_back(Orthanc::ImageProcessing::ImagePoint(1500, 550));
+        maskLayer_ = dynamic_cast<RadiographyMaskLayer*>(&(scene_->LoadMask(mask, dynamic_cast<RadiographyDicomLayer&>(dicomLayer), 128.0f, NULL)));
+        interactor_.SetMaskLayer(maskLayer_);
+
+        {
+          std::unique_ptr<Orthanc::ImageAccessor> renderedTextAlpha(TextRenderer::Render(Orthanc::EmbeddedResources::UBUNTU_FONT, 100,
+                                                                                    "%öÇaA&#"));
+          RadiographyLayer& layer = scene_->LoadAlphaBitmap(renderedTextAlpha.release(), NULL);
+          dynamic_cast<RadiographyAlphaLayer&>(layer).SetForegroundValue(200.0f * 256.0f);
+        }
+
+        {
+          RadiographyTextLayer::RegisterFont("ubuntu", Orthanc::EmbeddedResources::UBUNTU_FONT);
+          RadiographyLayer& layer = scene_->LoadText("Hello\nworld", "ubuntu", 20, 128, NULL, false);
+          layer.SetResizeable(true);
+        }
+        
+        {
+          RadiographyLayer& layer = scene_->LoadTestBlock(100, 50, NULL);
+          layer.SetResizeable(true);
+          layer.SetPan(0, 200);
+        }
+        
+        boost::shared_ptr<RadiographyWidget> widget(new RadiographyWidget(scene_, "main-widget"));
+        widget->SetTransmitMouseOver(true);
+        widget->SetInteractor(interactor_);
+        SetCentralWidget(widget);
+
+        //scene_->SetWindowing(128, 256);
+      }
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/SingleVolumeApplication.h	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,277 @@
+/**
+ * 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 "SampleApplicationBase.h"
+#include "../../../Framework/dev.h"
+#include "../../../Framework/Layers/LineMeasureTracker.h"
+#include "../../../Framework/Layers/CircleMeasureTracker.h"
+
+#include <Core/Toolbox.h>
+#include <Core/Logging.h>
+
+#include <Plugins/Samples/Common/OrthancHttpConnection.h>   // TODO REMOVE
+#include "../../../Framework/Layers/DicomStructureSetSlicer.h"   // TODO REMOVE
+#include "../../../Framework/Toolbox/MessagingToolbox.h"   // TODO REMOVE
+
+namespace OrthancStone
+{
+  namespace Samples
+  {
+    class SingleVolumeApplication : public SampleApplicationBase
+    {
+    private:
+      class Interactor : public VolumeImageInteractor
+      {
+      private:
+        SliceViewerWidget&  widget_;
+        size_t        layer_;
+        
+      protected:
+        virtual void NotifySliceContentChange(const ISlicedVolume& volume,
+                                       const size_t& sliceIndex,
+                                       const Slice& slice)
+        {
+          const OrthancVolumeImage& image = dynamic_cast<const OrthancVolumeImage&>(volume);
+
+          RenderStyle s = widget_.GetLayerStyle(layer_);
+
+          if (image.FitWindowingToRange(s, slice.GetConverter()))
+          {
+            //printf("Windowing: %f => %f\n", s.customWindowCenter_, s.customWindowWidth_);
+            widget_.SetLayerStyle(layer_, s);
+          }
+        }
+
+        virtual void MouseOver(CairoContext& context,
+                               WorldSceneWidget& widget,
+                               const ViewportGeometry& view,
+                               double x,
+                               double y,
+                               IStatusBar* statusBar)
+        {
+          const SliceViewerWidget& w = dynamic_cast<const SliceViewerWidget&>(widget);
+          Vector p = w.GetSlice().MapSliceToWorldCoordinates(x, y);
+          printf("%f %f %f\n", p[0], p[1], p[2]);
+        }
+      
+      public:
+        Interactor(OrthancVolumeImage& volume,
+                   SliceViewerWidget& widget,
+                   VolumeProjection projection,
+                   size_t layer) :
+          VolumeImageInteractor(volume, widget, projection),
+          widget_(widget),
+          layer_(layer)
+        {
+        }
+      };
+
+
+    public:
+      virtual void DeclareStartupOptions(boost::program_options::options_description& options)
+      {
+        boost::program_options::options_description generic("Sample options");
+        generic.add_options()
+          ("series", boost::program_options::value<std::string>(), 
+           "Orthanc ID of the series")
+          ("instance", boost::program_options::value<std::string>(), 
+           "Orthanc ID of a multi-frame instance that describes a 3D volume")
+          ("threads", boost::program_options::value<unsigned int>()->default_value(3), 
+           "Number of download threads")
+          ("projection", boost::program_options::value<std::string>()->default_value("axial"), 
+           "Projection of interest (can be axial, sagittal or coronal)")
+          ("reverse", boost::program_options::value<bool>()->default_value(false), 
+           "Reverse the normal direction of the volume")
+          ;
+
+        options.add(generic);    
+      }
+
+      virtual void Initialize(IStatusBar& statusBar,
+                              const boost::program_options::variables_map& parameters)
+      {
+        using namespace OrthancStone;
+
+        if (parameters.count("series") > 1 ||
+            parameters.count("instance") > 1)
+        {
+          LOG(ERROR) << "Only one series or instance is allowed";
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+
+        if (parameters.count("series") == 1 &&
+            parameters.count("instance") == 1)
+        {
+          LOG(ERROR) << "Cannot specify both a series and an instance";
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+
+        std::string series;
+        if (parameters.count("series") == 1)
+        {
+          series = parameters["series"].as<std::string>();
+        }
+        
+        std::string instance;
+        if (parameters.count("instance") == 1)
+        {
+          instance = parameters["instance"].as<std::string>();
+        }
+        
+        if (series.empty() &&
+            instance.empty())
+        {
+          LOG(ERROR) << "The series ID or instance ID is missing";
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+
+        //unsigned int threads = parameters["threads"].as<unsigned int>();
+        //bool reverse = parameters["reverse"].as<bool>();
+
+        std::string tmp = parameters["projection"].as<std::string>();
+        Orthanc::Toolbox::ToLowerCase(tmp);
+
+        VolumeProjection projection;
+        if (tmp == "axial")
+        {
+          projection = VolumeProjection_Axial;
+        }
+        else if (tmp == "sagittal")
+        {
+          projection = VolumeProjection_Sagittal;
+        }
+        else if (tmp == "coronal")
+        {
+          projection = VolumeProjection_Coronal;
+        }
+        else
+        {
+          LOG(ERROR) << "Unknown projection: " << tmp;
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+
+        std::unique_ptr<SliceViewerWidget> widget(new SliceViewerWidget);
+
+#if 1
+        std::unique_ptr<OrthancVolumeImage> volume(new OrthancVolumeImage(context.GetWebService(), true));
+        if (series.empty())
+        {
+          volume->ScheduleLoadInstance(instance);
+        }
+        else
+        {
+          volume->ScheduleLoadSeries(series);
+        }
+
+        widget->AddLayer(new VolumeImageMPRSlicer(*volume));
+
+        context_->AddInteractor(new Interactor(*volume, *widget, projection, 0));
+        context_->AddSlicedVolume(volume.release());
+
+        if (1)
+        {
+          RenderStyle s;
+          //s.drawGrid_ = true;
+          s.alpha_ = 1;
+          s.windowing_ = ImageWindowing_Bone;
+          widget->SetLayerStyle(0, s);
+        }
+        else
+        {
+          RenderStyle s;
+          s.alpha_ = 1;
+          s.applyLut_ = true;
+          s.lut_ = Orthanc::EmbeddedResources::COLORMAP_JET;
+          s.interpolation_ = ImageInterpolation_Bilinear;
+          widget->SetLayerStyle(0, s);
+        }
+#else
+        std::unique_ptr<OrthancVolumeImage> ct(new OrthancVolumeImage(context_->GetWebService(), false));
+        //ct->ScheduleLoadSeries("15a6f44a-ac7b88fe-19c462d9-dddd918e-b01550d8");  // 0178023P
+        //ct->ScheduleLoadSeries("dd069910-4f090474-7d2bba07-e5c10783-f9e4fb1d");
+        //ct->ScheduleLoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");  // IBA
+        //ct->ScheduleLoadSeries("03677739-1d8bca40-db1daf59-d74ff548-7f6fc9c0");  // 0522c0001 TCIA
+        ct->ScheduleLoadSeries("295e8a13-dfed1320-ba6aebb2-9a13e20f-1b3eb953");  // Captain
+        
+        std::unique_ptr<OrthancVolumeImage> pet(new OrthancVolumeImage(context_->GetWebService(), true));
+        //pet->ScheduleLoadSeries("48d2997f-8e25cd81-dd715b64-bd79cdcc-e8fcee53");  // 0178023P
+        //pet->ScheduleLoadSeries("aabad2e7-80702b5d-e599d26c-4f13398e-38d58a9e");
+        //pet->ScheduleLoadInstance("830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb"); // IBA 1
+        //pet->ScheduleLoadInstance("337876a1-a68a9718-f15abccd-38faafa1-b99b496a"); // IBA 2
+        //pet->ScheduleLoadInstance("830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb");  // IBA 3
+        //pet->ScheduleLoadInstance("269f26f4-0c83eeeb-2e67abbd-5467a40f-f1bec90c");  // 0522c0001 TCIA
+        pet->ScheduleLoadInstance("f080888c-0ab7528a-f7d9c28c-84980eb1-ff3b0ae6");  // Captain 1
+        //pet->ScheduleLoadInstance("4f78055b-6499a2c5-1e089290-394acc05-3ec781c1");  // Captain 2
+
+        std::unique_ptr<StructureSetLoader> rtStruct(new StructureSetLoader(context_->GetWebService()));
+        //rtStruct->ScheduleLoadInstance("c2ebc17b-6b3548db-5e5da170-b8ecab71-ea03add3");  // 0178023P
+        //rtStruct->ScheduleLoadInstance("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9");  // IBA
+        //rtStruct->ScheduleLoadInstance("17cd032b-ad92a438-ca05f06a-f9e96668-7e3e9e20");  // 0522c0001 TCIA
+        rtStruct->ScheduleLoadInstance("96c889ab-29fe5c54-dda6e66c-3949e4da-58f90d75");  // Captain
+        
+        widget->AddLayer(new VolumeImageMPRSlicer(*ct));
+        widget->AddLayer(new VolumeImageMPRSlicer(*pet));
+        widget->AddLayer(new DicomStructureSetSlicer(*rtStruct));
+        
+        context_->AddInteractor(new Interactor(*pet, *widget, projection, 1));
+        //context_->AddInteractor(new VolumeImageInteractor(*ct, *widget, projection));
+
+        context_->AddSlicedVolume(ct.release());
+        context_->AddSlicedVolume(pet.release());
+        context_->AddVolumeLoader(rtStruct.release());
+
+        {
+          RenderStyle s;
+          //s.drawGrid_ = true;
+          s.alpha_ = 1;
+          s.windowing_ = ImageWindowing_Bone;
+          widget->SetLayerStyle(0, s);
+        }
+
+        {
+          RenderStyle s;
+          //s.drawGrid_ = true;
+          s.SetColor(255, 0, 0);  // Draw missing PET layer in red
+          s.alpha_ = 0.5;
+          s.applyLut_ = true;
+          s.lut_ = Orthanc::EmbeddedResources::COLORMAP_JET;
+          s.interpolation_ = ImageInterpolation_Bilinear;
+          s.windowing_ = ImageWindowing_Custom;
+          s.customWindowCenter_ = 0;
+          s.customWindowWidth_ = 128;
+          widget->SetLayerStyle(1, s);
+        }
+#endif
+
+
+        statusBar.SetMessage("Use the keys \"b\", \"l\" and \"d\" to change Hounsfield windowing");
+        statusBar.SetMessage("Use the keys \"t\" to track the (X,Y,Z) mouse coordinates");
+        statusBar.SetMessage("Use the keys \"m\" to measure distances");
+        statusBar.SetMessage("Use the keys \"c\" to draw circles");
+
+        widget->SetTransmitMouseOver(true);
+        context_->SetCentralWidget(widget.release());
+      }
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/StoneSampleCommands.yml	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,35 @@
+#
+#        1         2         3         4         5         6         7         8
+# 345678901234567890123456789012345678901234567890123456789012345678901234567890
+#
+rootName: StoneSampleCommands
+
+# +---------------------------------+
+# | Messages from TypeScript to C++ |
+# +---------------------------------+
+
+enum Tool:
+  - LineMeasure
+  - CircleMeasure
+  - Crop
+  - Windowing
+  - Zoom
+  - Pan
+  - Move
+  - Rotate
+  - Resize
+  - Mask
+
+struct SelectTool:
+  __handler: cpp
+  tool: Tool
+
+enum ActionType:
+  - UndoCrop
+  - Rotate
+  - Invert
+
+struct Action:
+  __handler: cpp
+  type: ActionType
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/StoneSampleCommands_generate.py	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,16 @@
+import sys
+import os
+
+# add the generation script location to the search paths
+sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'Resources', 'CodeGeneration'))
+
+# import the code generation tooling script
+import stonegentool
+
+schemaFile = os.path.join(os.path.dirname(__file__), 'StoneSampleCommands.yml')
+outDir = os.path.dirname(__file__)
+
+# ignition!
+stonegentool.Process(schemaFile, outDir)
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/StoneSampleCommands_generated.hpp	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,703 @@
+/*
+         1         2         3         4         5         6         7
+12345678901234567890123456789012345678901234567890123456789012345678901234567890
+
+Generated on 2019-03-18 12:07:42.696093 by stonegentool
+
+*/
+#pragma once
+
+#include <exception>
+#include <iostream>
+#include <string>
+#include <sstream>
+#include <assert.h>
+#include <memory>
+#include <json/json.h>
+
+//#define STONEGEN_NO_CPP11 1
+
+#ifdef STONEGEN_NO_CPP11
+#define StoneSmartPtr std::unique_ptr
+#else 
+#define StoneSmartPtr std::unique_ptr
+#endif 
+
+namespace StoneSampleCommands
+{
+  /** Throws in case of problem */
+  inline void _StoneDeserializeValue(int32_t& destValue, const Json::Value& jsonValue)
+  {
+    destValue = jsonValue.asInt();
+  }
+
+  inline Json::Value _StoneSerializeValue(int32_t value)
+  {
+    Json::Value result(value);
+    return result;
+  }
+
+  inline void _StoneDeserializeValue(Json::Value& destValue, const Json::Value& jsonValue)
+  {
+    destValue = jsonValue;
+  }
+
+  inline Json::Value _StoneSerializeValue(Json::Value value)
+  {
+    return value;
+  }
+
+  /** Throws in case of problem */
+  inline void _StoneDeserializeValue(double& destValue, const Json::Value& jsonValue)
+  {
+    destValue = jsonValue.asDouble();
+  }
+
+  inline Json::Value _StoneSerializeValue(double value)
+  {
+    Json::Value result(value);
+    return result;
+  }
+
+  /** Throws in case of problem */
+  inline void _StoneDeserializeValue(bool& destValue, const Json::Value& jsonValue)
+  {
+    destValue = jsonValue.asBool();
+  }
+
+  inline Json::Value _StoneSerializeValue(bool value)
+  {
+    Json::Value result(value);
+    return result;
+  }
+
+  /** Throws in case of problem */
+  inline void _StoneDeserializeValue(
+       std::string& destValue
+     , const Json::Value& jsonValue)
+  {
+    destValue = jsonValue.asString();
+  }
+
+  inline Json::Value _StoneSerializeValue(const std::string& value)
+  {
+    // the following is better than 
+    Json::Value result(value.data(),value.data()+value.size());
+    return result;
+  }
+
+  inline std::string MakeIndent(size_t indent)
+  {
+    char* txt = reinterpret_cast<char*>(malloc(indent+1)); // NO EXCEPTION BELOW!!!!!!!!!!!!
+    for(size_t i = 0; i < indent; ++i)
+      txt[i] = ' ';
+    txt[indent] = 0;
+    std::string retVal(txt);
+    free(txt); // NO EXCEPTION ABOVE !!!!!!!!!!
+    return retVal;
+  }
+
+  // generic dumper
+  template<typename T>
+  std::ostream& StoneDumpValue(std::ostream& out, const T& value, size_t indent)
+  {
+    out << MakeIndent(indent) << value;
+    return out;
+  }
+
+  // string dumper
+  inline std::ostream& StoneDumpValue(std::ostream& out, const std::string& value, size_t indent)
+  {
+    out << MakeIndent(indent) << "\"" << value  << "\"";
+    return out;
+  }
+
+  /** Throws in case of problem */
+  template<typename T>
+  void _StoneDeserializeValue(
+    std::map<std::string, T>& destValue, const Json::Value& jsonValue)
+  {
+    destValue.clear();
+    for (
+      Json::Value::const_iterator itr = jsonValue.begin();
+      itr != jsonValue.end();
+      itr++)
+    {
+      std::string key;
+      _StoneDeserializeValue(key, itr.key());
+
+      T innerDestValue;
+      _StoneDeserializeValue(innerDestValue, *itr);
+
+      destValue[key] = innerDestValue;
+    }
+  }
+
+  template<typename T>
+  Json::Value _StoneSerializeValue(const std::map<std::string,T>& value)
+  {
+    Json::Value result(Json::objectValue);
+
+    for (typename std::map<std::string, T>::const_iterator it = value.cbegin();
+      it != value.cend(); ++it)
+    {
+      // it->first it->second
+      result[it->first] = _StoneSerializeValue(it->second);
+    }
+    return result;
+  }
+
+  template<typename T>
+  std::ostream& StoneDumpValue(std::ostream& out, const std::map<std::string,T>& value, size_t indent)
+  {
+    out << MakeIndent(indent) << "{\n";
+    for (typename std::map<std::string, T>::const_iterator it = value.cbegin();
+      it != value.cend(); ++it)
+    {
+      out << MakeIndent(indent+2) << "\"" << it->first << "\" : ";
+      StoneDumpValue(out, it->second, indent+2);
+    }
+    out << MakeIndent(indent) << "}\n";
+    return out;
+  }
+
+  /** Throws in case of problem */
+  template<typename T>
+  void _StoneDeserializeValue(
+    std::vector<T>& destValue, const Json::Value& jsonValue)
+  {
+    destValue.clear();
+    destValue.reserve(jsonValue.size());
+    for (Json::Value::ArrayIndex i = 0; i != jsonValue.size(); i++)
+    {
+      T innerDestValue;
+      _StoneDeserializeValue(innerDestValue, jsonValue[i]);
+      destValue.push_back(innerDestValue);
+    }
+  }
+
+  template<typename T>
+  Json::Value _StoneSerializeValue(const std::vector<T>& value)
+  {
+    Json::Value result(Json::arrayValue);
+    for (size_t i = 0; i < value.size(); ++i)
+    {
+      result.append(_StoneSerializeValue(value[i]));
+    }
+    return result;
+  }
+
+  template<typename T>
+  std::ostream& StoneDumpValue(std::ostream& out, const std::vector<T>& value, size_t indent)
+  {
+    out << MakeIndent(indent) << "[\n";
+    for (size_t i = 0; i < value.size(); ++i)
+    {
+      StoneDumpValue(out, value[i], indent+2);
+    }
+    out << MakeIndent(indent) << "]\n";
+    return out;
+  }
+
+  inline void StoneCheckSerializedValueTypeGeneric(const Json::Value& value)
+  {
+    if ((!value.isMember("type")) || (!value["type"].isString()))
+    {
+      std::stringstream ss;
+      ss << "Cannot deserialize value ('type' key invalid)";
+      throw std::runtime_error(ss.str());
+    }
+  }
+
+  inline void StoneCheckSerializedValueType(
+    const Json::Value& value, std::string typeStr)
+  {
+    StoneCheckSerializedValueTypeGeneric(value);
+
+    std::string actTypeStr = value["type"].asString();
+    if (actTypeStr != typeStr)
+    {
+      std::stringstream ss;
+      ss << "Cannot deserialize type" << actTypeStr
+        << "into " << typeStr;
+      throw std::runtime_error(ss.str());
+    }
+  }
+
+  // end of generic methods
+
+// end of generic methods
+
+  enum Tool {
+    Tool_LineMeasure,
+    Tool_CircleMeasure,
+    Tool_Crop,
+    Tool_Windowing,
+    Tool_Zoom,
+    Tool_Pan,
+    Tool_Move,
+    Tool_Rotate,
+    Tool_Resize,
+    Tool_Mask,
+  };
+
+  inline std::string ToString(const Tool& value)
+  {
+    if( value == Tool_LineMeasure)
+    {
+      return std::string("LineMeasure");
+    }
+    if( value == Tool_CircleMeasure)
+    {
+      return std::string("CircleMeasure");
+    }
+    if( value == Tool_Crop)
+    {
+      return std::string("Crop");
+    }
+    if( value == Tool_Windowing)
+    {
+      return std::string("Windowing");
+    }
+    if( value == Tool_Zoom)
+    {
+      return std::string("Zoom");
+    }
+    if( value == Tool_Pan)
+    {
+      return std::string("Pan");
+    }
+    if( value == Tool_Move)
+    {
+      return std::string("Move");
+    }
+    if( value == Tool_Rotate)
+    {
+      return std::string("Rotate");
+    }
+    if( value == Tool_Resize)
+    {
+      return std::string("Resize");
+    }
+    if( value == Tool_Mask)
+    {
+      return std::string("Mask");
+    }
+    std::stringstream ss;
+    ss << "Value \"" << value << "\" cannot be converted to Tool. Possible values are: "
+        << " LineMeasure = " << static_cast<int64_t>(Tool_LineMeasure)  << ", " 
+        << " CircleMeasure = " << static_cast<int64_t>(Tool_CircleMeasure)  << ", " 
+        << " Crop = " << static_cast<int64_t>(Tool_Crop)  << ", " 
+        << " Windowing = " << static_cast<int64_t>(Tool_Windowing)  << ", " 
+        << " Zoom = " << static_cast<int64_t>(Tool_Zoom)  << ", " 
+        << " Pan = " << static_cast<int64_t>(Tool_Pan)  << ", " 
+        << " Move = " << static_cast<int64_t>(Tool_Move)  << ", " 
+        << " Rotate = " << static_cast<int64_t>(Tool_Rotate)  << ", " 
+        << " Resize = " << static_cast<int64_t>(Tool_Resize)  << ", " 
+        << " Mask = " << static_cast<int64_t>(Tool_Mask)  << ", " 
+        << std::endl;
+    std::string msg = ss.str();
+    throw std::runtime_error(msg);
+  }
+
+  inline void FromString(Tool& value, std::string strValue)
+  {
+    if( strValue == std::string("LineMeasure") )
+    {
+      value = Tool_LineMeasure;
+      return;
+    }
+    if( strValue == std::string("CircleMeasure") )
+    {
+      value = Tool_CircleMeasure;
+      return;
+    }
+    if( strValue == std::string("Crop") )
+    {
+      value = Tool_Crop;
+      return;
+    }
+    if( strValue == std::string("Windowing") )
+    {
+      value = Tool_Windowing;
+      return;
+    }
+    if( strValue == std::string("Zoom") )
+    {
+      value = Tool_Zoom;
+      return;
+    }
+    if( strValue == std::string("Pan") )
+    {
+      value = Tool_Pan;
+      return;
+    }
+    if( strValue == std::string("Move") )
+    {
+      value = Tool_Move;
+      return;
+    }
+    if( strValue == std::string("Rotate") )
+    {
+      value = Tool_Rotate;
+      return;
+    }
+    if( strValue == std::string("Resize") )
+    {
+      value = Tool_Resize;
+      return;
+    }
+    if( strValue == std::string("Mask") )
+    {
+      value = Tool_Mask;
+      return;
+    }
+
+    std::stringstream ss;
+    ss << "String \"" << strValue << "\" cannot be converted to Tool. Possible values are: LineMeasure CircleMeasure Crop Windowing Zoom Pan Move Rotate Resize Mask ";
+    std::string msg = ss.str();
+    throw std::runtime_error(msg);
+  }
+
+
+  inline void _StoneDeserializeValue(
+    Tool& destValue, const Json::Value& jsonValue)
+  {
+    FromString(destValue, jsonValue.asString());
+  }
+
+  inline Json::Value _StoneSerializeValue(const Tool& value)
+  {
+    std::string strValue = ToString(value);
+    return Json::Value(strValue);
+  }
+
+  inline std::ostream& StoneDumpValue(std::ostream& out, const Tool& value, size_t indent = 0)
+  {
+    if( value == Tool_LineMeasure)
+    {
+      out << MakeIndent(indent) << "LineMeasure" << std::endl;
+    }
+    if( value == Tool_CircleMeasure)
+    {
+      out << MakeIndent(indent) << "CircleMeasure" << std::endl;
+    }
+    if( value == Tool_Crop)
+    {
+      out << MakeIndent(indent) << "Crop" << std::endl;
+    }
+    if( value == Tool_Windowing)
+    {
+      out << MakeIndent(indent) << "Windowing" << std::endl;
+    }
+    if( value == Tool_Zoom)
+    {
+      out << MakeIndent(indent) << "Zoom" << std::endl;
+    }
+    if( value == Tool_Pan)
+    {
+      out << MakeIndent(indent) << "Pan" << std::endl;
+    }
+    if( value == Tool_Move)
+    {
+      out << MakeIndent(indent) << "Move" << std::endl;
+    }
+    if( value == Tool_Rotate)
+    {
+      out << MakeIndent(indent) << "Rotate" << std::endl;
+    }
+    if( value == Tool_Resize)
+    {
+      out << MakeIndent(indent) << "Resize" << std::endl;
+    }
+    if( value == Tool_Mask)
+    {
+      out << MakeIndent(indent) << "Mask" << std::endl;
+    }
+    return out;
+  }
+
+
+  enum ActionType {
+    ActionType_UndoCrop,
+    ActionType_Rotate,
+    ActionType_Invert,
+  };
+
+  inline std::string ToString(const ActionType& value)
+  {
+    if( value == ActionType_UndoCrop)
+    {
+      return std::string("UndoCrop");
+    }
+    if( value == ActionType_Rotate)
+    {
+      return std::string("Rotate");
+    }
+    if( value == ActionType_Invert)
+    {
+      return std::string("Invert");
+    }
+    std::stringstream ss;
+    ss << "Value \"" << value << "\" cannot be converted to ActionType. Possible values are: "
+        << " UndoCrop = " << static_cast<int64_t>(ActionType_UndoCrop)  << ", " 
+        << " Rotate = " << static_cast<int64_t>(ActionType_Rotate)  << ", " 
+        << " Invert = " << static_cast<int64_t>(ActionType_Invert)  << ", " 
+        << std::endl;
+    std::string msg = ss.str();
+    throw std::runtime_error(msg);
+  }
+
+  inline void FromString(ActionType& value, std::string strValue)
+  {
+    if( strValue == std::string("UndoCrop") )
+    {
+      value = ActionType_UndoCrop;
+      return;
+    }
+    if( strValue == std::string("Rotate") )
+    {
+      value = ActionType_Rotate;
+      return;
+    }
+    if( strValue == std::string("Invert") )
+    {
+      value = ActionType_Invert;
+      return;
+    }
+
+    std::stringstream ss;
+    ss << "String \"" << strValue << "\" cannot be converted to ActionType. Possible values are: UndoCrop Rotate Invert ";
+    std::string msg = ss.str();
+    throw std::runtime_error(msg);
+  }
+
+
+  inline void _StoneDeserializeValue(
+    ActionType& destValue, const Json::Value& jsonValue)
+  {
+    FromString(destValue, jsonValue.asString());
+  }
+
+  inline Json::Value _StoneSerializeValue(const ActionType& value)
+  {
+    std::string strValue = ToString(value);
+    return Json::Value(strValue);
+  }
+
+  inline std::ostream& StoneDumpValue(std::ostream& out, const ActionType& value, size_t indent = 0)
+  {
+    if( value == ActionType_UndoCrop)
+    {
+      out << MakeIndent(indent) << "UndoCrop" << std::endl;
+    }
+    if( value == ActionType_Rotate)
+    {
+      out << MakeIndent(indent) << "Rotate" << std::endl;
+    }
+    if( value == ActionType_Invert)
+    {
+      out << MakeIndent(indent) << "Invert" << std::endl;
+    }
+    return out;
+  }
+
+
+
+#ifdef _MSC_VER
+#pragma region SelectTool
+#endif //_MSC_VER
+
+  struct SelectTool
+  {
+    Tool tool;
+
+    SelectTool(Tool tool = Tool())
+    {
+      this->tool = tool;
+    }
+  };
+
+  inline void _StoneDeserializeValue(SelectTool& destValue, const Json::Value& value)
+  {
+    _StoneDeserializeValue(destValue.tool, value["tool"]);
+    }
+
+  inline Json::Value _StoneSerializeValue(const SelectTool& value)
+  {
+    Json::Value result(Json::objectValue);
+    result["tool"] = _StoneSerializeValue(value.tool);
+
+    return result;
+  }
+
+  inline std::ostream& StoneDumpValue(std::ostream& out, const SelectTool& value, size_t indent = 0)
+  {
+    out << MakeIndent(indent) << "{\n";
+    out << MakeIndent(indent) << "tool:\n";
+    StoneDumpValue(out, value.tool,indent+2);
+    out << "\n";
+
+    out << MakeIndent(indent) << "}\n";
+    return out;
+  }
+
+  inline void StoneDeserialize(SelectTool& destValue, const Json::Value& value)
+  {
+    StoneCheckSerializedValueType(value, "StoneSampleCommands.SelectTool");
+    _StoneDeserializeValue(destValue, value["value"]);
+  }
+
+  inline Json::Value StoneSerializeToJson(const SelectTool& value)
+  {
+    Json::Value result(Json::objectValue);
+    result["type"] = "StoneSampleCommands.SelectTool";
+    result["value"] = _StoneSerializeValue(value);
+    return result;
+  }
+
+  inline std::string StoneSerialize(const SelectTool& value)
+  {
+    Json::Value resultJson = StoneSerializeToJson(value);
+    std::string resultStr = resultJson.toStyledString();
+    return resultStr;
+  }
+
+#ifdef _MSC_VER
+#pragma endregion SelectTool
+#endif //_MSC_VER
+
+#ifdef _MSC_VER
+#pragma region Action
+#endif //_MSC_VER
+
+  struct Action
+  {
+    ActionType type;
+
+    Action(ActionType type = ActionType())
+    {
+      this->type = type;
+    }
+  };
+
+  inline void _StoneDeserializeValue(Action& destValue, const Json::Value& value)
+  {
+    _StoneDeserializeValue(destValue.type, value["type"]);
+    }
+
+  inline Json::Value _StoneSerializeValue(const Action& value)
+  {
+    Json::Value result(Json::objectValue);
+    result["type"] = _StoneSerializeValue(value.type);
+
+    return result;
+  }
+
+  inline std::ostream& StoneDumpValue(std::ostream& out, const Action& value, size_t indent = 0)
+  {
+    out << MakeIndent(indent) << "{\n";
+    out << MakeIndent(indent) << "type:\n";
+    StoneDumpValue(out, value.type,indent+2);
+    out << "\n";
+
+    out << MakeIndent(indent) << "}\n";
+    return out;
+  }
+
+  inline void StoneDeserialize(Action& destValue, const Json::Value& value)
+  {
+    StoneCheckSerializedValueType(value, "StoneSampleCommands.Action");
+    _StoneDeserializeValue(destValue, value["value"]);
+  }
+
+  inline Json::Value StoneSerializeToJson(const Action& value)
+  {
+    Json::Value result(Json::objectValue);
+    result["type"] = "StoneSampleCommands.Action";
+    result["value"] = _StoneSerializeValue(value);
+    return result;
+  }
+
+  inline std::string StoneSerialize(const Action& value)
+  {
+    Json::Value resultJson = StoneSerializeToJson(value);
+    std::string resultStr = resultJson.toStyledString();
+    return resultStr;
+  }
+
+#ifdef _MSC_VER
+#pragma endregion Action
+#endif //_MSC_VER
+
+#ifdef _MSC_VER
+#pragma region Dispatching code
+#endif //_MSC_VER
+
+  class IHandler
+  {
+  public:
+    virtual bool Handle(const SelectTool& value) = 0;
+    virtual bool Handle(const Action& value) = 0;
+  };
+
+  /** Service function for StoneDispatchToHandler */
+  inline bool StoneDispatchJsonToHandler(
+    const Json::Value& jsonValue, IHandler* handler)
+  {
+    StoneCheckSerializedValueTypeGeneric(jsonValue);
+    std::string type = jsonValue["type"].asString();
+    if (type == "")
+    {
+      // this should never ever happen
+      throw std::runtime_error("Caught empty type while dispatching");
+    }
+    else if (type == "StoneSampleCommands.SelectTool")
+    {
+      SelectTool value;
+      _StoneDeserializeValue(value, jsonValue["value"]);
+      return handler->Handle(value);
+    }
+    else if (type == "StoneSampleCommands.Action")
+    {
+      Action value;
+      _StoneDeserializeValue(value, jsonValue["value"]);
+      return handler->Handle(value);
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  /** Takes a serialized type and passes this to the handler */
+  inline bool StoneDispatchToHandler(std::string strValue, IHandler* handler)
+  {
+    Json::Value readValue;
+
+    Json::CharReaderBuilder builder;
+    Json::CharReader* reader = builder.newCharReader();
+
+    StoneSmartPtr<Json::CharReader> ptr(reader);
+
+    std::string errors;
+
+    bool ok = reader->parse(
+      strValue.c_str(),
+      strValue.c_str() + strValue.size(),
+      &readValue,
+      &errors
+    );
+    if (!ok)
+    {
+      std::stringstream ss;
+      ss << "Jsoncpp parsing error: " << errors;
+      throw std::runtime_error(ss.str());
+    }
+    return StoneDispatchJsonToHandler(readValue, handler);
+  }
+
+#ifdef _MSC_VER
+#pragma endregion Dispatching code
+#endif //_MSC_VER
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/StoneSampleCommands_generated.ts	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,333 @@
+/*
+         1         2         3         4         5         6         7
+12345678901234567890123456789012345678901234567890123456789012345678901234567890
+
+Generated on 2019-03-18 12:07:42.696093 by stonegentool
+
+*/
+
+function StoneCheckSerializedValueType(value: any, typeStr: string)
+{
+  StoneCheckSerializedValueTypeGeneric(value);
+
+  if (value['type'] != typeStr)
+  {
+    throw new Error(
+      `Cannot deserialize type ${value['type']} into ${typeStr}`);
+  }
+}
+
+function isString(val: any) :boolean
+{
+  return ((typeof val === 'string') || (val instanceof String));
+}
+
+function StoneCheckSerializedValueTypeGeneric(value: any)
+{
+  // console.//log("+-------------------------------------------------+");
+  // console.//log("|            StoneCheckSerializedValueTypeGeneric |");
+  // console.//log("+-------------------------------------------------+");
+  // console.//log("value = ");
+  // console.//log(value);
+  if ( (!('type' in value)) || (!isString(value.type)) )
+  {
+    throw new Error(
+      "Cannot deserialize value ('type' key invalid)");
+  }
+}
+
+// end of generic methods
+
+export enum Tool {
+  LineMeasure = "LineMeasure",
+  CircleMeasure = "CircleMeasure",
+  Crop = "Crop",
+  Windowing = "Windowing",
+  Zoom = "Zoom",
+  Pan = "Pan",
+  Move = "Move",
+  Rotate = "Rotate",
+  Resize = "Resize",
+  Mask = "Mask"
+};
+
+export function Tool_FromString(strValue:string) : Tool
+{
+  if( strValue == "LineMeasure" )
+  {
+    return Tool.LineMeasure;
+  }
+  if( strValue == "CircleMeasure" )
+  {
+    return Tool.CircleMeasure;
+  }
+  if( strValue == "Crop" )
+  {
+    return Tool.Crop;
+  }
+  if( strValue == "Windowing" )
+  {
+    return Tool.Windowing;
+  }
+  if( strValue == "Zoom" )
+  {
+    return Tool.Zoom;
+  }
+  if( strValue == "Pan" )
+  {
+    return Tool.Pan;
+  }
+  if( strValue == "Move" )
+  {
+    return Tool.Move;
+  }
+  if( strValue == "Rotate" )
+  {
+    return Tool.Rotate;
+  }
+  if( strValue == "Resize" )
+  {
+    return Tool.Resize;
+  }
+  if( strValue == "Mask" )
+  {
+    return Tool.Mask;
+  }
+
+  let msg : string =  `String ${strValue} cannot be converted to Tool. Possible values are: LineMeasure, CircleMeasure, Crop, Windowing, Zoom, Pan, Move, Rotate, Resize, Mask`;
+  throw new Error(msg);
+}
+
+export function Tool_ToString(value:Tool) : string
+{
+  if( value == Tool.LineMeasure )
+  {
+    return "LineMeasure";
+  }
+  if( value == Tool.CircleMeasure )
+  {
+    return "CircleMeasure";
+  }
+  if( value == Tool.Crop )
+  {
+    return "Crop";
+  }
+  if( value == Tool.Windowing )
+  {
+    return "Windowing";
+  }
+  if( value == Tool.Zoom )
+  {
+    return "Zoom";
+  }
+  if( value == Tool.Pan )
+  {
+    return "Pan";
+  }
+  if( value == Tool.Move )
+  {
+    return "Move";
+  }
+  if( value == Tool.Rotate )
+  {
+    return "Rotate";
+  }
+  if( value == Tool.Resize )
+  {
+    return "Resize";
+  }
+  if( value == Tool.Mask )
+  {
+    return "Mask";
+  }
+
+  let msg : string = `Value ${value} cannot be converted to Tool. Possible values are: `;
+  {
+    let _LineMeasure_enumValue : string = Tool.LineMeasure; // enums are strings in stonecodegen, so this will work.
+    let msg_LineMeasure : string = `LineMeasure (${_LineMeasure_enumValue}), `;
+    msg = msg + msg_LineMeasure;
+  }
+  {
+    let _CircleMeasure_enumValue : string = Tool.CircleMeasure; // enums are strings in stonecodegen, so this will work.
+    let msg_CircleMeasure : string = `CircleMeasure (${_CircleMeasure_enumValue}), `;
+    msg = msg + msg_CircleMeasure;
+  }
+  {
+    let _Crop_enumValue : string = Tool.Crop; // enums are strings in stonecodegen, so this will work.
+    let msg_Crop : string = `Crop (${_Crop_enumValue}), `;
+    msg = msg + msg_Crop;
+  }
+  {
+    let _Windowing_enumValue : string = Tool.Windowing; // enums are strings in stonecodegen, so this will work.
+    let msg_Windowing : string = `Windowing (${_Windowing_enumValue}), `;
+    msg = msg + msg_Windowing;
+  }
+  {
+    let _Zoom_enumValue : string = Tool.Zoom; // enums are strings in stonecodegen, so this will work.
+    let msg_Zoom : string = `Zoom (${_Zoom_enumValue}), `;
+    msg = msg + msg_Zoom;
+  }
+  {
+    let _Pan_enumValue : string = Tool.Pan; // enums are strings in stonecodegen, so this will work.
+    let msg_Pan : string = `Pan (${_Pan_enumValue}), `;
+    msg = msg + msg_Pan;
+  }
+  {
+    let _Move_enumValue : string = Tool.Move; // enums are strings in stonecodegen, so this will work.
+    let msg_Move : string = `Move (${_Move_enumValue}), `;
+    msg = msg + msg_Move;
+  }
+  {
+    let _Rotate_enumValue : string = Tool.Rotate; // enums are strings in stonecodegen, so this will work.
+    let msg_Rotate : string = `Rotate (${_Rotate_enumValue}), `;
+    msg = msg + msg_Rotate;
+  }
+  {
+    let _Resize_enumValue : string = Tool.Resize; // enums are strings in stonecodegen, so this will work.
+    let msg_Resize : string = `Resize (${_Resize_enumValue}), `;
+    msg = msg + msg_Resize;
+  }
+  {
+    let _Mask_enumValue : string = Tool.Mask; // enums are strings in stonecodegen, so this will work.
+    let msg_Mask : string = `Mask (${_Mask_enumValue})`;
+    msg = msg + msg_Mask;
+  }
+  throw new Error(msg);
+}
+
+export enum ActionType {
+  UndoCrop = "UndoCrop",
+  Rotate = "Rotate",
+  Invert = "Invert"
+};
+
+export function ActionType_FromString(strValue:string) : ActionType
+{
+  if( strValue == "UndoCrop" )
+  {
+    return ActionType.UndoCrop;
+  }
+  if( strValue == "Rotate" )
+  {
+    return ActionType.Rotate;
+  }
+  if( strValue == "Invert" )
+  {
+    return ActionType.Invert;
+  }
+
+  let msg : string =  `String ${strValue} cannot be converted to ActionType. Possible values are: UndoCrop, Rotate, Invert`;
+  throw new Error(msg);
+}
+
+export function ActionType_ToString(value:ActionType) : string
+{
+  if( value == ActionType.UndoCrop )
+  {
+    return "UndoCrop";
+  }
+  if( value == ActionType.Rotate )
+  {
+    return "Rotate";
+  }
+  if( value == ActionType.Invert )
+  {
+    return "Invert";
+  }
+
+  let msg : string = `Value ${value} cannot be converted to ActionType. Possible values are: `;
+  {
+    let _UndoCrop_enumValue : string = ActionType.UndoCrop; // enums are strings in stonecodegen, so this will work.
+    let msg_UndoCrop : string = `UndoCrop (${_UndoCrop_enumValue}), `;
+    msg = msg + msg_UndoCrop;
+  }
+  {
+    let _Rotate_enumValue : string = ActionType.Rotate; // enums are strings in stonecodegen, so this will work.
+    let msg_Rotate : string = `Rotate (${_Rotate_enumValue}), `;
+    msg = msg + msg_Rotate;
+  }
+  {
+    let _Invert_enumValue : string = ActionType.Invert; // enums are strings in stonecodegen, so this will work.
+    let msg_Invert : string = `Invert (${_Invert_enumValue})`;
+    msg = msg + msg_Invert;
+  }
+  throw new Error(msg);
+}
+
+
+
+export class SelectTool {
+  tool:Tool;
+
+  constructor() {
+  }
+
+  public StoneSerialize(): string {
+    let container: object = {};
+    container['type'] = 'StoneSampleCommands.SelectTool';
+    container['value'] = this;
+    return JSON.stringify(container);
+  }
+
+  public static StoneDeserialize(valueStr: string) : SelectTool
+  {
+    let value: any = JSON.parse(valueStr);
+    StoneCheckSerializedValueType(value, 'StoneSampleCommands.SelectTool');
+    let result: SelectTool = value['value'] as SelectTool;
+    return result;
+  }
+}
+export class Action {
+  type:ActionType;
+
+  constructor() {
+  }
+
+  public StoneSerialize(): string {
+    let container: object = {};
+    container['type'] = 'StoneSampleCommands.Action';
+    container['value'] = this;
+    return JSON.stringify(container);
+  }
+
+  public static StoneDeserialize(valueStr: string) : Action
+  {
+    let value: any = JSON.parse(valueStr);
+    StoneCheckSerializedValueType(value, 'StoneSampleCommands.Action');
+    let result: Action = value['value'] as Action;
+    return result;
+  }
+}
+
+export interface IHandler {
+};
+
+/** Service function for StoneDispatchToHandler */
+export function StoneDispatchJsonToHandler(
+  jsonValue: any, handler: IHandler): boolean
+{
+  StoneCheckSerializedValueTypeGeneric(jsonValue);
+  let type: string = jsonValue["type"];
+  if (type == "")
+  {
+    // this should never ever happen
+    throw new Error("Caught empty type while dispatching");
+  }
+  else
+  {
+    return false;
+  }
+}
+
+/** Takes a serialized type and passes this to the handler */
+export function StoneDispatchToHandler(
+  strValue: string, handler: IHandler): boolean
+{
+  // console.//log("+------------------------------------------------+");
+  // console.//log("|            StoneDispatchToHandler              |");
+  // console.//log("+------------------------------------------------+");
+  // console.//log("strValue = ");
+  // console.//log(strValue);
+  let jsonValue: any = JSON.parse(strValue)
+  return StoneDispatchJsonToHandler(jsonValue, handler);
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/SynchronizedSeriesApplication.h	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,109 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "SampleInteractor.h"
+
+#include "../../../Framework/Toolbox/OrthancSeriesLoader.h"
+#include "../../../Framework/Layers/SeriesFrameRendererFactory.h"
+#include "../../../Framework/Layers/ReferenceLineFactory.h"
+#include "../../../Framework/Widgets/LayoutWidget.h"
+
+#include <Core/Logging.h>
+
+namespace OrthancStone
+{
+  namespace Samples
+  {
+    class SynchronizedSeriesApplication : public SampleApplicationBase
+    {
+    private:   
+      LayeredSceneWidget* CreateSeriesWidget(BasicApplicationContext& context,
+                                             const std::string& series)
+      {
+        std::unique_ptr<ISeriesLoader> loader
+          (new OrthancSeriesLoader(context.GetWebService().GetConnection(), series));
+
+        std::unique_ptr<SampleInteractor> interactor(new SampleInteractor(*loader, false));
+
+        std::unique_ptr<LayeredSceneWidget> widget(new LayeredSceneWidget);
+        widget->AddLayer(new SeriesFrameRendererFactory(loader.release(), false));
+        widget->SetSlice(interactor->GetCursor().GetCurrentSlice());
+        widget->SetInteractor(*interactor);
+
+        context.AddInteractor(interactor.release());
+
+        return widget.release();
+      }
+
+    public:
+      virtual void DeclareCommandLineOptions(boost::program_options::options_description& options)
+      {
+        boost::program_options::options_description generic("Sample options");
+        generic.add_options()
+          ("a", boost::program_options::value<std::string>(), 
+           "Orthanc ID of the 1st series")
+          ("b", boost::program_options::value<std::string>(), 
+           "Orthanc ID of the 2nd series")
+          ("c", boost::program_options::value<std::string>(), 
+           "Orthanc ID of the 3rd series")
+          ;
+
+        options.add(generic);    
+      }
+
+      virtual void Initialize(BasicApplicationContext& context,
+                              IStatusBar& statusBar,
+                              const boost::program_options::variables_map& parameters)
+      {
+        if (parameters.count("a") != 1 ||
+            parameters.count("b") != 1 ||
+            parameters.count("c") != 1)
+        {
+          LOG(ERROR) << "At least one of the three series IDs is missing";
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+
+        std::unique_ptr<LayeredSceneWidget> a(CreateSeriesWidget(context, parameters["a"].as<std::string>()));
+        std::unique_ptr<LayeredSceneWidget> b(CreateSeriesWidget(context, parameters["b"].as<std::string>()));
+        std::unique_ptr<LayeredSceneWidget> c(CreateSeriesWidget(context, parameters["c"].as<std::string>()));
+
+        ReferenceLineFactory::Configure(*a, *b);
+        ReferenceLineFactory::Configure(*a, *c);
+        ReferenceLineFactory::Configure(*b, *c);
+
+        std::unique_ptr<LayoutWidget> layout(new LayoutWidget);
+        layout->SetPadding(5);
+        layout->AddWidget(a.release());
+
+        std::unique_ptr<LayoutWidget> layoutB(new LayoutWidget);
+        layoutB->SetVertical();
+        layoutB->SetPadding(5);
+        layoutB->AddWidget(b.release());
+        layoutB->AddWidget(c.release());
+        layout->AddWidget(layoutB.release());
+
+        context.SetCentralWidget(layout.release());        
+      }
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/TestPatternApplication.h	Wed Apr 22 14:05:47 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 "SampleApplicationBase.h"
+
+#include "../../../Framework/Widgets/TestCairoWidget.h"
+#include "../../../Framework/Widgets/TestWorldSceneWidget.h"
+#include "../../../Framework/Widgets/LayoutWidget.h"
+
+namespace OrthancStone
+{
+  namespace Samples
+  {
+    class TestPatternApplication : public SampleApplicationBase
+    {
+    public:
+      virtual void DeclareStartupOptions(boost::program_options::options_description& options)
+      {
+        boost::program_options::options_description generic("Sample options");
+        generic.add_options()
+          ("animate", boost::program_options::value<bool>()->default_value(true), "Animate the test pattern")
+          ;
+
+        options.add(generic);    
+      }
+
+      virtual void Initialize(IStatusBar& statusBar,
+                              const boost::program_options::variables_map& parameters)
+      {
+        using namespace OrthancStone;
+
+        std::unique_ptr<LayoutWidget> layout(new LayoutWidget);
+        layout->SetPadding(10);
+        layout->SetBackgroundCleared(true);
+        layout->AddWidget(new TestCairoWidget(parameters["animate"].as<bool>()));
+        layout->AddWidget(new TestWorldSceneWidget(parameters["animate"].as<bool>()));
+
+        context_->SetCentralWidget(layout.release());
+        context_->SetUpdateDelay(25);  // If animation, update the content each 25ms
+      }
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/Web/index.html	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,23 @@
+<!doctype html>
+
+<html lang="us">
+  <head>
+    <meta charset="utf-8" />
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+    <!-- Disable pinch zoom on mobile devices -->
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+    <meta name="HandheldFriendly" content="true" />
+
+    <title>Wasm Samples</title>
+
+<body>
+    <ul>
+      <li><a href="simple-viewer/simple-viewer.html">Simple Viewer Project (you may add ?studyId=XXX in the url)</a></li>
+      <li><a href="single-frame.html?instance=XXX">Single frame application (you must replace XXX by a valid instance id in the url)</a></li>
+      <li><a href="single-frame-editor.html?instance=XXX">Single frame editor application (you must replace XXX by a valid instance id in the url)</a></li>
+      <li><a href="simple-viewer-single-file.html">Simple Viewer Single file (to be replaced by other samples)</a></li>
+    </ul>
+</body>
+
+</html>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/Web/samples-styles.css	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,16 @@
+html, body {
+  width: 100%;
+  height: 100%;
+  margin: 0px;
+  border: 0;
+  overflow: hidden; /*  Disable scrollbars */
+  display: block;  /* No floating content on sides */
+  background-color: black;
+  color: white;
+  font-family: Arial, Helvetica, sans-serif;
+}
+
+canvas {
+  left:0px;
+  top:0px;
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/Web/simple-viewer-single-file.html	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,39 @@
+<!doctype html>
+
+<html lang="us">
+
+<head>
+  <meta charset="utf-8" />
+  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+  <!-- Disable pinch zoom on mobile devices -->
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+  <meta name="HandheldFriendly" content="true" />
+
+  <title>Simple Viewer</title>
+  <link href="samples-styles.css" rel="stylesheet" />
+
+<body>
+  <div id="breadcrumb">
+    <span id="patient-id"></span>
+    <span id="study-description"></span>
+    <span id="series-description"></span>
+  </div>
+  <div style="height: calc(100% - 50px)">
+    <div style="width: 20%; height: 100%; display: inline-block">
+      <canvas id="canvas"></canvas>
+    </div>
+    <div style="width: 70%; height: 100%; display: inline-block">
+      <canvas id="canvas2"></canvas>
+    </div>
+  </div>
+  <div id="toolbox" style="height: 50px">
+    <input tool-selector="line-measure" type="radio" name="radio-tool-selector" class="tool-selector">line
+    <input tool-selector="circle-measure" type="radio" name="radio-tool-selector" class="tool-selector">circle
+    <button action-trigger="action1" class="action-trigger">action1</button>
+    <button action-trigger="action2" class="action-trigger">action2</button>
+  </div>
+  <script type="text/javascript" src="app-simple-viewer-single-file.js"></script>
+</body>
+
+</html>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/Web/simple-viewer-single-file.ts	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,61 @@
+import wasmApplicationRunner = require('../../../Platforms/Wasm/wasm-application-runner');
+
+wasmApplicationRunner.InitializeWasmApplication("OrthancStoneSimpleViewerSingleFile", "/orthanc");
+
+function SelectTool(toolName: string) {
+    var command = {
+        command: "selectTool",
+        args: {
+            toolName: toolName
+        }
+    };
+    wasmApplicationRunner.SendSerializedMessageToStoneApplication(JSON.stringify(command));
+
+}
+
+function PerformAction(commandName: string) {
+    var command = {
+        command: commandName,
+        commandType: "simple",
+        args: {}
+    };
+    wasmApplicationRunner.SendSerializedMessageToStoneApplication(JSON.stringify(command));
+}
+
+//initializes the buttons
+//-----------------------
+// install "SelectTool" handlers
+document.querySelectorAll("[tool-selector]").forEach((e) => {
+    console.log(e);
+    (e as HTMLInputElement).addEventListener("click", () => {
+        console.log(e);
+        SelectTool(e.attributes["tool-selector"].value);
+    });
+});
+
+// install "PerformAction" handlers
+document.querySelectorAll("[action-trigger]").forEach((e) => {
+    (e as HTMLInputElement).addEventListener("click", () => {
+        PerformAction(e.attributes["action-trigger"].value);
+    });
+});
+
+// this method is called "from the C++ code" when the StoneApplication is updated.
+// it can be used to update the UI of the application
+function UpdateWebApplicationWithString(statusUpdateMessage: string) {
+  console.log(statusUpdateMessage);
+  
+  if (statusUpdateMessage.startsWith("series-description=")) {
+      document.getElementById("series-description").innerText = statusUpdateMessage.split("=")[1];
+  }
+}
+
+function UpdateWebApplicationWithSerializedMessage(statusUpdateMessageString: string) {
+  console.log("updating web application with serialized message: ", statusUpdateMessageString);
+  console.log("<not supported in the simple viewer (single file)!>");
+}
+
+// make it available to other js scripts in the application
+(<any> window).UpdateWebApplicationWithString = UpdateWebApplicationWithString;
+
+(<any> window).UpdateWebApplicationWithSerializedMessage = UpdateWebApplicationWithSerializedMessage;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/Web/simple-viewer-single-file.tsconfig.json	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,9 @@
+{
+    "extends" : "./tsconfig-samples",
+    "compilerOptions": {
+        // "outFile": "../build-web/app-simple-viewer-single-file.js"
+    },
+    "include" : [
+        "simple-viewer-single-file.ts"
+    ]
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/Web/single-frame-editor.html	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,22 @@
+<!doctype html>
+
+<html lang="us">
+  <head>
+    <meta charset="utf-8" />
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+    <!-- Disable pinch zoom on mobile devices -->
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+    <meta name="HandheldFriendly" content="true" />
+
+    <title>Simple Viewer</title>
+    <link href="samples-styles.css" rel="stylesheet" />
+
+<body>
+  <div style="width: 100%; height: 100%">
+    <canvas id="canvas"></canvas>
+  </div>
+  <script type="text/javascript" src="app-single-frame-editor.js"></script>
+</body>
+
+</html>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/Web/single-frame-editor.ts	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,3 @@
+import wasmApplicationRunner = require('../../../Platforms/Wasm/wasm-application-runner');
+
+wasmApplicationRunner.InitializeWasmApplication("OrthancStoneSingleFrameEditor", "/orthanc");
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/Web/single-frame-editor.tsconfig.json	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,8 @@
+{
+    "extends" : "./tsconfig-samples",
+    "compilerOptions": {
+    },
+    "include" : [
+        "single-frame-editor.ts"
+    ]
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/Web/single-frame.html	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,22 @@
+<!doctype html>
+
+<html lang="us">
+  <head>
+    <meta charset="utf-8" />
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+    <!-- Disable pinch zoom on mobile devices -->
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+    <meta name="HandheldFriendly" content="true" />
+
+    <title>Simple Viewer</title>
+    <link href="samples-styles.css" rel="stylesheet" />
+
+<body>
+  <div style="width: 100%; height: 100%">
+    <canvas id="canvas"></canvas>
+  </div>
+  <script type="text/javascript" src="app-single-frame.js"></script>
+</body>
+
+</html>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/Web/single-frame.ts	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,4 @@
+import wasmApplicationRunner = require('../../../Platforms/Wasm/wasm-application-runner');
+
+wasmApplicationRunner.InitializeWasmApplication("OrthancStoneSingleFrame", "/orthanc");
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/Web/single-frame.tsconfig.json	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,8 @@
+{
+    "extends" : "./tsconfig-samples",
+    "compilerOptions": {
+    },
+    "include" : [
+        "single-frame.ts"
+    ]
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/Web/tsconfig-samples.json	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,11 @@
+{
+    "extends" : "../../../Platforms/Wasm/tsconfig-stone",
+    "compilerOptions": {
+        "sourceMap": false,
+        "lib" : [
+            "es2017",
+            "dom",
+            "dom.iterable"
+        ]
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/build-wasm.sh	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,27 @@
+#!/bin/bash
+#
+# usage:
+# to build all targets in Debug:
+# ./build-wasm.sh
+#
+# to build a single target in release:
+# ./build-wasm.sh OrthancStoneSingleFrameEditor Release
+
+set -e
+
+target=${1:-all}
+buildType=${2:-Debug}
+
+currentDir=$(pwd)
+samplesRootDir=$(pwd)
+
+mkdir -p $samplesRootDir/build-wasm
+cd $samplesRootDir/build-wasm
+
+source ~/apps/emsdk/emsdk_env.sh
+cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=~/apps/emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_BUILD_TYPE=$buildType -DSTONE_SOURCES_DIR=$currentDir/../../../orthanc-stone -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT=$currentDir/../../../orthanc -DALLOW_DOWNLOADS=ON .. -DENABLE_WASM=ON
+ninja $target
+
+echo "-- building the web application -- "
+cd $currentDir
+./build-web.sh
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/build-wasm.sh.old	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,33 @@
+#!/bin/bash
+#
+# usage:
+# to build all targets:
+# ./build-wasm.sh
+#
+# to build a single target:
+# ./build-wasm.sh OrthancStoneSingleFrameEditor
+
+set -e
+
+target=${1:-all}
+
+currentDir=$(pwd)
+samplesRootDir=$(pwd)
+
+mkdir -p $samplesRootDir/build-wasm
+cd $samplesRootDir/build-wasm
+
+source ~/apps/emsdk/emsdk_env.sh
+cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake \
+  -DCMAKE_BUILD_TYPE=Release \
+  -DSTONE_SOURCES_DIR=$currentDir/../../../orthanc-stone \
+  -DORTHANC_FRAMEWORK_SOURCE=path \
+  -DORTHANC_FRAMEWORK_ROOT=$currentDir/../../../orthanc \
+  -DALLOW_DOWNLOADS=ON .. \
+  -DENABLE_WASM=ON
+
+ninja $target
+
+echo "-- building the web application -- "
+cd $currentDir
+./build-web.sh
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/build-web-ext.sh	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,58 @@
+#!/bin/bash
+
+set -e
+
+target=${1:-all}
+# this script currently assumes that the wasm code has been built on its side and is availabie in build-wasm/
+
+currentDir=$(pwd)
+
+scriptDirRel=$(dirname $0)
+#echo $scriptDirRel
+scriptDirAbs=$(realpath $scriptDirRel)
+echo $scriptDirAbs
+
+samplesRootDir=scriptDirAbs
+
+outputDir=$samplesRootDir/build-web/
+mkdir -p $outputDir
+
+# files used by all single files samples
+cp $samplesRootDir/Web/index.html $outputDir
+cp $samplesRootDir/Web/samples-styles.css $outputDir
+
+# build simple-viewer-single-file (obsolete project)
+if [[ $target == "all" || $target == "OrthancStoneSimpleViewerSingleFile" ]]; then
+  cp $samplesRootDir/Web/simple-viewer-single-file.html $outputDir
+  tsc --allowJs --project $samplesRootDir/Web/simple-viewer-single-file.tsconfig.json
+  cp $currentDir/build-wasm/OrthancStoneSimpleViewerSingleFile.js  $outputDir
+  cp $currentDir/build-wasm/OrthancStoneSimpleViewerSingleFile.wasm  $outputDir
+fi
+
+# build single-frame
+if [[ $target == "all" || $target == "OrthancStoneSingleFrame" ]]; then
+  cp $samplesRootDir/Web/single-frame.html $outputDir
+  tsc --allowJs --project $samplesRootDir/Web/single-frame.tsconfig.json
+  cp $currentDir/build-wasm/OrthancStoneSingleFrame.js  $outputDir
+  cp $currentDir/build-wasm/OrthancStoneSingleFrame.wasm  $outputDir
+fi
+
+# build single-frame-editor
+if [[ $target == "all" || $target == "OrthancStoneSingleFrameEditor" ]]; then
+  cp $samplesRootDir/Web/single-frame-editor.html $outputDir
+  tsc --allowJs --project $samplesRootDir/Web/single-frame-editor.tsconfig.json
+  cp $currentDir/build-wasm/OrthancStoneSingleFrameEditor.js  $outputDir
+  cp $currentDir/build-wasm/OrthancStoneSingleFrameEditor.wasm  $outputDir
+fi
+
+# build simple-viewer project
+if [[ $target == "all" || $target == "OrthancStoneSimpleViewer" ]]; then
+  mkdir -p $outputDir/simple-viewer/
+  cp $samplesRootDir/SimpleViewer/Wasm/simple-viewer.html $outputDir/simple-viewer/
+  cp $samplesRootDir/SimpleViewer/Wasm/styles.css $outputDir/simple-viewer/
+  tsc --allowJs --project $samplesRootDir/SimpleViewer/Wasm/tsconfig-simple-viewer.json
+  cp $currentDir/build-wasm/OrthancStoneSimpleViewer.js  $outputDir/simple-viewer/
+  cp $currentDir/build-wasm/OrthancStoneSimpleViewer.wasm  $outputDir/simple-viewer/
+fi
+
+cd $currentDir
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/build-web.sh	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,74 @@
+#!/bin/bash
+
+set -e
+
+target=${1:-all}
+# this script currently assumes that the wasm code has been built on its side and is availabie in build-wasm/
+
+currentDir=$(pwd)
+samplesRootDir=$(pwd)
+
+echo "*************************************************************************"
+echo "samplesRootDir = $samplesRootDir"
+echo "*************************************************************************"
+
+outputDir=$samplesRootDir/build-web/
+mkdir -p "$outputDir"
+
+# files used by all single files samples
+cp "$samplesRootDir/Web/index.html" "$outputDir"
+cp "$samplesRootDir/Web/samples-styles.css" "$outputDir"
+
+# # build simple-viewer-single-file (obsolete project)
+# if [[ $target == "all" || $target == "OrthancStoneSimpleViewerSingleFile" ]]; then
+#   cp $samplesRootDir/Web/simple-viewer-single-file.html $outputDir
+#   tsc --project $samplesRootDir/Web/simple-viewer-single-file.tsconfig.json --outDir "$outputDir"
+#   browserify \
+#       "$outputDir/Platforms/Wasm/wasm-application-runner.js" \
+#       "$outputDir/Applications/Samples/Web/simple-viewer-single-file.js" \
+#       -o "$outputDir/app-simple-viewer-single-file.js"
+#   cp "$currentDir/build-wasm/OrthancStoneSimpleViewerSingleFile.js"  $outputDir
+#   cp "$currentDir/build-wasm/OrthancStoneSimpleViewerSingleFile.wasm"  $outputDir
+# fi
+
+# # build single-frame
+# if [[ $target == "all" || $target == "OrthancStoneSingleFrame" ]]; then
+#   cp $samplesRootDir/Web/single-frame.html $outputDir
+#   tsc --project $samplesRootDir/Web/single-frame.tsconfig.json --outDir "$outputDir"
+#   browserify \
+#       "$outputDir/Platforms/Wasm/wasm-application-runner.js" \
+#       "$outputDir/Applications/Samples/Web/single-frame.js" \
+#       -o "$outputDir/app-single-frame.js"
+#   cp "$currentDir/build-wasm/OrthancStoneSingleFrame.js"  $outputDir
+#   cp "$currentDir/build-wasm/OrthancStoneSingleFrame.wasm"  $outputDir
+# fi
+
+# build single-frame-editor
+if [[ $target == "all" || $target == "OrthancStoneSingleFrameEditor" ]]; then
+  cp $samplesRootDir/Web/single-frame-editor.html $outputDir
+  tsc --project $samplesRootDir/Web/single-frame-editor.tsconfig.json --outDir "$outputDir"
+  browserify \
+      "$outputDir/Platforms/Wasm/wasm-application-runner.js" \
+      "$outputDir/Applications/Samples/Web/single-frame-editor.js" \
+      -o "$outputDir/app-single-frame-editor.js"
+  cp "$currentDir/build-wasm/OrthancStoneSingleFrameEditor.js"  $outputDir
+  cp "$currentDir/build-wasm/OrthancStoneSingleFrameEditor.wasm"  $outputDir
+fi
+
+# build simple-viewer project
+if [[ $target == "all" || $target == "OrthancStoneSimpleViewer" ]]; then
+  mkdir -p $outputDir/simple-viewer/
+  cp $samplesRootDir/SimpleViewer/Wasm/simple-viewer.html $outputDir/simple-viewer/
+  cp $samplesRootDir/SimpleViewer/Wasm/styles.css $outputDir/simple-viewer/
+  
+  # the root dir must contain all the source files for the whole project
+  tsc --module commonjs --allowJs --project "$samplesRootDir/SimpleViewer/Wasm/tsconfig-simple-viewer.json" --rootDir "$samplesRootDir/../.." --outDir "$outputDir/simple-viewer/"
+  browserify \
+    "$outputDir/simple-viewer/Platforms/Wasm/wasm-application-runner.js" \
+    "$outputDir/simple-viewer/Applications/Samples/SimpleViewer/Wasm/simple-viewer.js" \
+    -o "$outputDir/simple-viewer/app-simple-viewer.js"
+  cp "$currentDir/build-wasm/OrthancStoneSimpleViewer.js"  "$outputDir/simple-viewer/"
+  cp "$currentDir/build-wasm/OrthancStoneSimpleViewer.wasm"  "$outputDir/simple-viewer/"
+fi
+
+cd $currentDir
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/get-requirements-windows.ps1	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,50 @@
+
+if ($true) {
+
+    Write-Error "This script is obsolete. Please work under WSL and run build-wasm.sh"
+
+} else {
+
+    param(
+        [IO.DirectoryInfo] $EmsdkRootDir = "C:\Emscripten",
+        [bool] $Overwrite = $false
+      )
+    
+    if (Test-Path -Path $EmsdkRootDir) {
+        if( $Override) {
+            Remove-Item -Path $EmsdkRootDir -Force -Recurse
+        } else {
+            throw "The `"$EmsdkRootDir`" folder may not exist! Use the Overwrite flag to bypass this check."
+        }
+    }
+    
+    # TODO: detect whether git is installed
+    # choco install -y git
+    
+    Write-Host "Will retrieve the Emscripten SDK to the `"$EmsdkRootDir`" folder"
+    
+    $EmsdkParentDir = split-path -Parent $EmsdkRootDir
+    $EmsdkRootName = split-path -Leaf $EmsdkRootDir
+    
+    Push-Location $EmsdkParentDir
+    
+    git clone https://github.com/juj/emsdk.git $EmsdkRootName
+    cd $EmsdkRootName
+    
+    git pull
+    
+    ./emsdk install latest
+    
+    ./emsdk activate latest
+    
+    echo "INFO: the ~/.emscripten file has been configured for this installation of Emscripten."
+    
+    Write-Host "emsdk is now installed in $EmsdkRootDir"
+    
+    Pop-Location
+
+}
+
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/nginx.local.conf	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,44 @@
+# Local config to serve the WASM samples static files and reverse proxy Orthanc.
+# Uses port 9977 instead of 80.
+
+# `events` section is mandatory
+events {
+  worker_connections 1024; # Default: 1024
+}
+
+http {
+
+  # prevent nginx sync issues on OSX
+  proxy_buffering off;
+
+  server {
+    listen 9977 default_server;
+    client_max_body_size 4G;
+
+    # location may have to be adjusted depending on your OS and nginx install
+    include /etc/nginx/mime.types;
+    # if not in your system mime.types, add this line to support WASM:
+    # types {
+    #    application/wasm                      wasm; 
+    # }
+
+    # serve WASM static files
+    root build-web/;
+    location / {
+	}
+
+    # reverse proxy orthanc
+	location /orthanc/ {
+		rewrite /orthanc(.*) $1 break;
+		proxy_pass http://127.0.0.1:8042;
+		proxy_set_header Host $http_host;
+		proxy_set_header my-auth-header good-token;
+		proxy_request_buffering off;
+		proxy_max_temp_file_size 0;
+		client_max_body_size 0;
+	}
+
+
+  }
+  
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/package-lock.json	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,11 @@
+{
+  "requires": true,
+  "lockfileVersion": 1,
+  "dependencies": {
+    "typescript": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.2.2.tgz",
+      "integrity": "sha512-VCj5UiSyHBjwfYacmDuc/NOk4QQixbE+Wn7MFJuS0nRuPQbof132Pw4u53dm264O8LPc2MVsc7RJNml5szurkg=="
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/rt-viewer-demo/CMakeLists.txt	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,142 @@
+cmake_minimum_required(VERSION 2.8.3)
+project(RtViewerDemo)
+
+if(MSVC)
+  add_definitions(/MP)
+  if (CMAKE_BUILD_TYPE MATCHES DEBUG)
+    add_definitions(/JMC)
+  endif()
+endif()
+
+message("-------------------------------------------------------------------------------------------------------------------")
+message("ORTHANC_FRAMEWORK_ROOT is set to ${ORTHANC_FRAMEWORK_ROOT}")
+message("-------------------------------------------------------------------------------------------------------------------")
+
+if(NOT DEFINED ORTHANC_FRAMEWORK_ROOT)
+  message(FATAL_ERROR "The location of the Orthanc source repository must be set in the ORTHANC_FRAMEWORK_ROOT CMake variable")
+endif()
+
+message("-------------------------------------------------------------------------------------------------------------------")
+message("STONE_SOURCES_DIR is set to ${STONE_SOURCES_DIR}")
+message("-------------------------------------------------------------------------------------------------------------------")
+
+if(NOT DEFINED STONE_SOURCES_DIR)
+  message(FATAL_ERROR "The location of the Stone of Orthanc source repository must be set in the STONE_SOURCES_DIR CMake variable")
+endif()
+
+include(${STONE_SOURCES_DIR}/Resources/CMake/OrthancStoneParameters.cmake)
+
+if (OPENSSL_NO_CAPIENG)
+add_definitions(-DOPENSSL_NO_CAPIENG=1)
+endif()
+
+set(ENABLE_SDL OFF CACHE BOOL "Target SDL Native application")
+set(ENABLE_QT OFF CACHE BOOL "Target Qt Native application")
+set(ENABLE_WASM OFF CACHE BOOL "Target WASM application")
+
+if (ENABLE_WASM)
+  #####################################################################
+  ## Configuration of the Emscripten compiler for WebAssembly target
+  #####################################################################
+
+  set(WASM_FLAGS "-s WASM=1")
+  set(WASM_FLAGS "${WASM_FLAGS} -s STRICT=1") # drops support for all deprecated build options
+  set(WASM_FLAGS "${WASM_FLAGS} -s FILESYSTEM=1") # if we don't include it, gen_uuid.c fails to build because srand, getpid(), ... are not defined
+  set(WASM_FLAGS "${WASM_FLAGS} -s DISABLE_EXCEPTION_CATCHING=0") # actually enable exception catching 
+  set(WASM_FLAGS "${WASM_FLAGS} -s ERROR_ON_MISSING_LIBRARIES=1")
+
+  if (CMAKE_BUILD_TYPE MATCHES DEBUG)
+    set(WASM_FLAGS "${WASM_FLAGS} -g4") # generate debug information
+    set(WASM_FLAGS "${WASM_FLAGS} -s ASSERTIONS=2") # more runtime checks
+  else()
+    set(WASM_FLAGS "${WASM_FLAGS} -Os") # optimize for web (speed and size)
+  endif()
+
+  set(WASM_MODULE_NAME "StoneFrameworkModule" CACHE STRING "Name of the WebAssembly module")
+
+  set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WASM_FLAGS}")
+  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WASM_FLAGS}")
+
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${WASM_FLAGS}")  # not always clear which flags are for the compiler and which one are for the linker -> pass them all to the linker too
+  # set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --js-library ${STONE_SOURCES_DIR}/Applications/Samples/samples-library.js")
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/WasmWebService.js")
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/WasmDelayedCallExecutor.js")
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/default-library.js")
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\"]'")
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s EXPORT_NAME='\"${WASM_MODULE_NAME}\"'")
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1")
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s TOTAL_MEMORY=536870912")
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s TOTAL_STACK=128000000")
+
+  add_definitions(-DORTHANC_ENABLE_WASM=1)
+  set(ORTHANC_SANDBOXED ON)
+
+elseif (ENABLE_QT OR ENABLE_SDL)
+
+  set(ENABLE_NATIVE ON)
+  set(ORTHANC_SANDBOXED OFF)
+  set(ENABLE_CRYPTO_OPTIONS ON)
+  set(ENABLE_GOOGLE_TEST ON)
+  set(ENABLE_WEB_CLIENT ON)
+
+endif()
+
+
+#####################################################################
+## Configuration for Orthanc
+#####################################################################
+
+if (ORTHANC_STONE_VERSION STREQUAL "mainline")
+  set(ORTHANC_FRAMEWORK_VERSION "mainline")
+  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
+else()
+  set(ORTHANC_FRAMEWORK_VERSION "1.4.1")
+  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
+endif()
+
+set(ORTHANC_FRAMEWORK_SOURCE "${ORTHANC_FRAMEWORK_DEFAULT_SOURCE}" CACHE STRING "Source of the Orthanc source code (can be \"hg\", \"archive\", \"web\" or \"path\")")
+set(ORTHANC_FRAMEWORK_ARCHIVE "" CACHE STRING "Path to the Orthanc archive, if ORTHANC_FRAMEWORK_SOURCE is \"archive\"")
+set(ORTHANC_FRAMEWORK_ROOT "" CACHE STRING "Path to the Orthanc source directory, if ORTHANC_FRAMEWORK_SOURCE is \"path\"")
+
+add_definitions(
+  -DORTHANC_ENABLE_LOGGING_PLUGIN=0
+  )
+
+
+#####################################################################
+## Build a static library containing the Orthanc Stone framework
+#####################################################################
+
+LIST(APPEND ORTHANC_BOOST_COMPONENTS program_options)
+
+include(${STONE_SOURCES_DIR}/Resources/CMake/OrthancStoneConfiguration.cmake)
+
+add_library(OrthancStone STATIC
+  ${ORTHANC_STONE_SOURCES}
+  )
+
+#####################################################################
+## Build all the sample applications
+#####################################################################
+
+include_directories(${ORTHANC_STONE_ROOT})
+
+list(APPEND RTVIEWERDEMO_APPLICATION_SOURCES
+  ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleInteractor.h
+  ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleApplicationBase.h
+  )
+
+if (ENABLE_WASM)
+  list(APPEND RTVIEWERDEMO_APPLICATION_SOURCES
+    ${STONE_WASM_SOURCES}
+    )
+endif()
+
+add_executable(RtViewerDemo
+  main.cpp
+  ${RTVIEWERDEMO_APPLICATION_SOURCES}
+)
+set_target_properties(RtViewerDemo PROPERTIES COMPILE_DEFINITIONS ORTHANC_STONE_SAMPLE=3)
+target_include_directories(RtViewerDemo PRIVATE ${ORTHANC_STONE_ROOT})
+target_link_libraries(RtViewerDemo OrthancStone)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/rt-viewer-demo/build-sdl-msvc15.ps1	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,24 @@
+if (-not (Test-Path "build-sdl-msvc15")) {
+  mkdir -p "build-sdl-msvc15"
+}
+
+cd build-sdl-msvc15
+
+cmake -G "Visual Studio 15 2017 Win64" -DSTATIC_BUILD=ON -DOPENSSL_NO_CAPIENG=ON -DORTHANC_FRAMEWORK_SOURCE=path -DSTONE_SOURCES_DIR="$($pwd)\..\..\..\.." -DORTHANC_FRAMEWORK_ROOT="$($pwd)\..\..\..\..\..\orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON .. 
+
+if (!$?) {
+	Write-Error 'cmake configuration failed' -ErrorAction Stop
+}
+
+cmake --build . --target RtViewerDemo --config Debug
+
+if (!$?) {
+	Write-Error 'cmake build failed' -ErrorAction Stop
+}
+
+cd Debug
+
+.\RtViewerDemo.exe --ct-series=a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa --dose-instance=830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb --struct-instance=54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/rt-viewer-demo/build-wasm.sh	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,29 @@
+#!/bin/bash
+#
+# usage:
+# build-wasm BUILD_TYPE
+# where BUILD_TYPE is Debug, RelWithDebInfo or Release
+
+set -e
+
+buildType=${1:-Debug}
+
+currentDir=$(pwd)
+currentDirAbs=$(realpath $currentDir)
+
+mkdir -p build-wasm
+cd build-wasm
+
+source ~/apps/emsdk/emsdk_env.sh
+cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake \
+-DCMAKE_BUILD_TYPE=$buildType -DSTONE_SOURCES_DIR=$currentDirAbs/../../../../orthanc-stone \
+-DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT=$currentDirAbs/../../../../orthanc \
+-DALLOW_DOWNLOADS=ON .. -DENABLE_WASM=ON
+
+ninja $target
+
+echo "-- building the web application -- "
+cd $currentDir
+./build-web.sh
+
+echo "Launch start-serving-files.sh to access the web sample application locally"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/rt-viewer-demo/build-web.sh	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,32 @@
+#!/bin/bash
+
+set -e
+
+target=${1:-all}
+# this script currently assumes that the wasm code has been built on its side and is availabie in build-wasm/
+
+currentDir=$(pwd)
+samplesRootDir=$(pwd)
+
+tscOutput=$samplesRootDir/build-tsc-output/
+outputDir=$samplesRootDir/build-web/
+mkdir -p "$outputDir"
+
+# files used by all single files samples
+cp "$samplesRootDir/index.html" "$outputDir"
+cp "$samplesRootDir/samples-styles.css" "$outputDir"
+
+# build rt-viewer-demo
+cp $samplesRootDir/rt-viewer-demo.html $outputDir
+tsc --project $samplesRootDir/rt-viewer-demo.tsconfig.json --outDir "$tscOutput"
+browserify \
+    "$tscOutput/orthanc-stone/Platforms/Wasm/logger.js" \
+    "$tscOutput/orthanc-stone/Platforms/Wasm/stone-framework-loader.js" \
+    "$tscOutput/orthanc-stone/Platforms/Wasm/wasm-application-runner.js" \
+    "$tscOutput/orthanc-stone/Platforms/Wasm/wasm-viewport.js" \
+    "$tscOutput/rt-viewer-sample/rt-viewer-demo.js" \
+    -o "$outputDir/app-rt-viewer-demo.js"
+cp "$currentDir/build-wasm/RtViewerDemo.js"  $outputDir
+cp "$currentDir/build-wasm/RtViewerDemo.wasm"  $outputDir
+
+cd $currentDir
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/rt-viewer-demo/index.html	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,20 @@
+<!doctype html>
+
+<html lang="us">
+  <head>
+    <meta charset="utf-8" />
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+    <!-- Disable pinch zoom on mobile devices -->
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+    <meta name="HandheldFriendly" content="true" />
+
+    <title>Wasm Samples</title>
+
+<body>
+    <ul>
+      <li><a href="rt-viewer-demo.html?ct-series=a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa&dose-instance=830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb&struct-instance=54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9">RTSTRUCT + CT + RTDOSE viewer demo. Pplease replace the url arguments with suitable IDs (you can find those in the Orthanc Explorer, for instance)</a></li>
+    </ul>
+</body>
+
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/rt-viewer-demo/main.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,893 @@
+/**
+ * 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 "Applications/IStoneApplication.h"
+#include "Framework/Widgets/WorldSceneWidget.h"
+#include "Framework/Widgets/LayoutWidget.h"
+
+#if ORTHANC_ENABLE_WASM==1
+  #include "Platforms/Wasm/WasmPlatformApplicationAdapter.h"
+  #include "Platforms/Wasm/Defaults.h"
+  #include "Platforms/Wasm/WasmViewport.h"
+#endif
+
+#if ORTHANC_ENABLE_QT==1
+  #include "Qt/SampleMainWindow.h"
+  #include "Qt/SampleMainWindowWithButtons.h"
+#endif
+
+#include "Framework/Layers/DicomSeriesVolumeSlicer.h"
+#include "Framework/Widgets/SliceViewerWidget.h"
+#include "Framework/Volumes/StructureSetLoader.h"
+
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+#include <Core/Images/ImageTraits.h>
+
+#include <boost/math/constants/constants.hpp>
+#include "Framework/dev.h"
+#include "Framework/Widgets/LayoutWidget.h"
+#include "Framework/Layers/DicomStructureSetSlicer.h"
+
+namespace OrthancStone
+{
+  namespace Samples
+  {
+    class RtViewerDemoBaseApplication : public IStoneApplication
+    {
+    protected:
+      // ownership is transferred to the application context
+#ifndef RESTORE_NON_RTVIEWERDEMO_BEHAVIOR
+      LayoutWidget*          mainWidget_;
+#else
+      WorldSceneWidget*  mainWidget_;
+#endif
+
+    public:
+      virtual void Initialize(StoneApplicationContext* context,
+                              IStatusBar& statusBar,
+                              const boost::program_options::variables_map& parameters) ORTHANC_OVERRIDE
+      {
+      }
+
+      virtual std::string GetTitle() const ORTHANC_OVERRIDE
+      {
+        return "Stone of Orthanc - Sample";
+      }
+
+      /**
+       * In the basic samples, the commands are handled by the platform adapter and NOT
+       * by the application handler
+      */
+      virtual void HandleSerializedMessage(const char* data) ORTHANC_OVERRIDE {};
+
+
+      virtual void Finalize() ORTHANC_OVERRIDE {}
+      virtual IWidget* GetCentralWidget() ORTHANC_OVERRIDE {return mainWidget_;}
+
+#if ORTHANC_ENABLE_WASM==1
+      // default implementations for a single canvas named "canvas" in the HTML and an empty WasmApplicationAdapter
+
+      virtual void InitializeWasm() ORTHANC_OVERRIDE
+      {
+        AttachWidgetToWasmViewport("canvas", mainWidget_);
+      }
+
+      virtual WasmPlatformApplicationAdapter* CreateWasmApplicationAdapter(MessageBroker& broker)
+      {
+        return new WasmPlatformApplicationAdapter(broker, *this);
+      }
+#endif
+
+    };
+
+    // this application actually works in Qt and WASM
+    class RtViewerDemoBaseSingleCanvasWithButtonsApplication : public RtViewerDemoBaseApplication
+    {
+public:
+      virtual void OnPushButton1Clicked() {}
+      virtual void OnPushButton2Clicked() {}
+      virtual void OnTool1Clicked() {}
+      virtual void OnTool2Clicked() {}
+
+      virtual void GetButtonNames(std::string& pushButton1,
+                                  std::string& pushButton2,
+                                  std::string& tool1,
+                                  std::string& tool2
+                                  ) {
+        pushButton1 = "action1";
+        pushButton2 = "action2";
+        tool1 = "tool1";
+        tool2 = "tool2";
+      }
+
+#if ORTHANC_ENABLE_QT==1
+      virtual QStoneMainWindow* CreateQtMainWindow() {
+        return new SampleMainWindowWithButtons(dynamic_cast<OrthancStone::NativeStoneApplicationContext&>(*context_), *this);
+      }
+#endif
+
+    };
+
+    // this application actually works in SDL and WASM
+    class RtViewerDemoBaseApplicationSingleCanvas : public RtViewerDemoBaseApplication
+    {
+public:
+
+#if ORTHANC_ENABLE_QT==1
+      virtual QStoneMainWindow* CreateQtMainWindow() {
+        return new SampleMainWindow(dynamic_cast<OrthancStone::NativeStoneApplicationContext&>(*context_), *this);
+      }
+#endif
+    };
+  }
+}
+
+
+
+namespace OrthancStone
+{
+  namespace Samples
+  {
+    template <Orthanc::PixelFormat T>
+    void ReadDistributionInternal(std::vector<float>& distribution,
+                                  const Orthanc::ImageAccessor& image)
+    {
+      const unsigned int width = image.GetWidth();
+      const unsigned int height = image.GetHeight();
+      
+      distribution.resize(width * height);
+      size_t pos = 0;
+
+      for (unsigned int y = 0; y < height; y++)
+      {
+        for (unsigned int x = 0; x < width; x++, pos++)
+        {
+          distribution[pos] = Orthanc::ImageTraits<T>::GetFloatPixel(image, x, y);
+        }
+      }
+    }
+
+    void ReadDistribution(std::vector<float>& distribution,
+                          const Orthanc::ImageAccessor& image)
+    {
+      switch (image.GetFormat())
+      {
+        case Orthanc::PixelFormat_Grayscale8:
+          ReadDistributionInternal<Orthanc::PixelFormat_Grayscale8>(distribution, image);
+          break;
+
+        case Orthanc::PixelFormat_Grayscale16:
+          ReadDistributionInternal<Orthanc::PixelFormat_Grayscale16>(distribution, image);
+          break;
+
+        case Orthanc::PixelFormat_SignedGrayscale16:
+          ReadDistributionInternal<Orthanc::PixelFormat_SignedGrayscale16>(distribution, image);
+          break;
+
+        case Orthanc::PixelFormat_Grayscale32:
+          ReadDistributionInternal<Orthanc::PixelFormat_Grayscale32>(distribution, image);
+          break;
+
+        case Orthanc::PixelFormat_Grayscale64:
+          ReadDistributionInternal<Orthanc::PixelFormat_Grayscale64>(distribution, image);
+          break;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+    }
+
+
+    class DoseInteractor : public VolumeImageInteractor
+    {
+    private:
+      SliceViewerWidget&         widget_;
+      size_t               layer_;
+      DicomFrameConverter  converter_;
+
+
+      
+    protected:
+      virtual void NotifySliceChange(const ISlicedVolume& slicedVolume,
+                                    const size_t& sliceIndex,
+                                    const Slice& slice)
+      {
+        converter_ = slice.GetConverter();
+        
+  #if 0
+        const OrthancVolumeImage& volume = dynamic_cast<const OrthancVolumeImage&>(slicedVolume);
+
+        RenderStyle s = widget_.GetLayerStyle(layer_);
+
+        if (volume.FitWindowingToRange(s, slice.GetConverter()))
+        {
+          printf("Windowing: %f => %f\n", s.customWindowCenter_, s.customWindowWidth_);
+          widget_.SetLayerStyle(layer_, s);
+        }
+  #endif
+      }
+
+      virtual void NotifyVolumeReady(const ISlicedVolume& slicedVolume)
+      {
+        const float percentile = 0.01f;
+        const OrthancVolumeImage& volume = dynamic_cast<const OrthancVolumeImage&>(slicedVolume);
+
+        std::vector<float> distribution;
+        ReadDistribution(distribution, volume.GetImage().GetInternalImage());
+        std::sort(distribution.begin(), distribution.end());
+
+        int start = static_cast<int>(std::ceil(distribution.size() * percentile));
+        int end = static_cast<int>(std::floor(distribution.size() * (1.0f - percentile)));
+
+        float a = 0;
+        float b = 0;
+        
+        if (start < end &&
+            start >= 0 &&
+            end < static_cast<int>(distribution.size()))
+        {
+          a = distribution[start];
+          b = distribution[end];
+        }
+        else if (!distribution.empty())
+        {
+          // Too small distribution: Use full range
+          a = distribution.front();
+          b = distribution.back();
+        }
+
+        //printf("%f %f\n", a, b);
+
+        RenderStyle s = widget_.GetLayerStyle(layer_);
+        s.windowing_ = ImageWindowing_Custom;
+        s.customWindowCenter_ = static_cast<float>(converter_.Apply((a + b) / 2.0f));
+        s.customWindowWidth_ = static_cast<float>(converter_.Apply(b - a));
+        
+        // 96.210556 => 192.421112
+        widget_.SetLayerStyle(layer_, s);
+        printf("Windowing: %f => %f\n", s.customWindowCenter_, s.customWindowWidth_);      
+      }
+
+    public:
+      DoseInteractor(MessageBroker& broker, OrthancVolumeImage& volume,
+                    SliceViewerWidget& widget,
+                    VolumeProjection projection,
+                    size_t layer) :
+        VolumeImageInteractor(broker, volume, widget, projection),
+        widget_(widget),
+        layer_(layer)
+      {
+      }
+    };
+
+    class RtViewerDemoApplication :
+      public RtViewerDemoBaseApplicationSingleCanvas,
+      public IObserver
+    {
+    public:
+      std::vector<std::pair<SliceViewerWidget*, size_t> > doseCtWidgetLayerPairs_;
+      std::list<OrthancStone::IWorldSceneInteractor*>    interactors_;
+
+      class Interactor : public IWorldSceneInteractor
+      {
+      private:
+        RtViewerDemoApplication&  application_;
+
+      public:
+        Interactor(RtViewerDemoApplication&  application) :
+          application_(application)
+        {
+        }
+
+        virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget,
+          const ViewportGeometry& view,
+          MouseButton button,
+          KeyboardModifiers modifiers,
+          int viewportX,
+          int viewportY,
+          double x,
+          double y,
+          IStatusBar* statusBar,
+          const std::vector<Touch>& displayTouches)
+        {
+          return NULL;
+        }
+
+        virtual void MouseOver(CairoContext& context,
+          WorldSceneWidget& widget,
+          const ViewportGeometry& view,
+          double x,
+          double y,
+          IStatusBar* statusBar)
+        {
+          if (statusBar != NULL)
+          {
+            Vector p = dynamic_cast<SliceViewerWidget&>(widget).GetSlice().MapSliceToWorldCoordinates(x, y);
+
+            char buf[64];
+            sprintf(buf, "X = %.02f Y = %.02f Z = %.02f (in cm)",
+              p[0] / 10.0, p[1] / 10.0, p[2] / 10.0);
+            statusBar->SetMessage(buf);
+          }
+        }
+
+        virtual void MouseWheel(WorldSceneWidget& widget,
+          MouseWheelDirection direction,
+          KeyboardModifiers modifiers,
+          IStatusBar* statusBar)
+        {
+          int scale = (modifiers & KeyboardModifiers_Control ? 10 : 1);
+
+          switch (direction)
+          {
+          case MouseWheelDirection_Up:
+            application_.OffsetSlice(-scale);
+            break;
+
+          case MouseWheelDirection_Down:
+            application_.OffsetSlice(scale);
+            break;
+
+          default:
+            break;
+          }
+        }
+
+        virtual void KeyPressed(WorldSceneWidget& widget,
+          KeyboardKeys key,
+          char keyChar,
+          KeyboardModifiers modifiers,
+          IStatusBar* statusBar)
+        {
+          switch (keyChar)
+          {
+          case 's':
+            // TODO: recursively traverse children
+            widget.FitContent();
+            break;
+
+          default:
+            break;
+          }
+        }
+      };
+
+      void OffsetSlice(int offset)
+      {
+        if (source_ != NULL)
+        {
+          int slice = static_cast<int>(slice_) + offset;
+
+          if (slice < 0)
+          {
+            slice = 0;
+          }
+
+          if (slice >= static_cast<int>(source_->GetSliceCount()))
+          {
+            slice = static_cast<int>(source_->GetSliceCount()) - 1;
+          }
+
+          if (slice != static_cast<int>(slice_))
+          {
+            SetSlice(slice);
+          }
+        }
+      }
+
+
+      SliceViewerWidget& GetMainWidget()
+      {
+        return *dynamic_cast<SliceViewerWidget*>(mainWidget_);
+      }
+
+
+      void SetSlice(size_t index)
+      {
+        if (source_ != NULL &&
+          index < source_->GetSliceCount())
+        {
+          slice_ = static_cast<unsigned int>(index);
+
+#if 1
+          GetMainWidget().SetSlice(source_->GetSlice(slice_).GetGeometry());
+#else
+          // TEST for scene extents - Rotate the axes
+          double a = 15.0 / 180.0 * boost::math::constants::pi<double>();
+
+#if 1
+          Vector x; GeometryToolbox::AssignVector(x, cos(a), sin(a), 0);
+          Vector y; GeometryToolbox::AssignVector(y, -sin(a), cos(a), 0);
+#else
+          // Flip the normal
+          Vector x; GeometryToolbox::AssignVector(x, cos(a), sin(a), 0);
+          Vector y; GeometryToolbox::AssignVector(y, sin(a), -cos(a), 0);
+#endif
+
+          SliceGeometry s(source_->GetSlice(slice_).GetGeometry().GetOrigin(), x, y);
+          widget_->SetSlice(s);
+#endif
+        }
+      }
+
+
+      void OnMainWidgetGeometryReady(const IVolumeSlicer::GeometryReadyMessage& message)
+      {
+        // 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())
+        {
+          SetSlice(source_->GetSliceCount() / 2);
+        }
+
+        GetMainWidget().FitContent();
+      }
+
+    DicomFrameConverter                                converter_;
+
+    void OnSliceContentChangedMessage(const ISlicedVolume::SliceContentChangedMessage&   message)
+    {
+      converter_ = message.GetSlice().GetConverter();
+    }
+
+    void OnVolumeReadyMessage(const ISlicedVolume::VolumeReadyMessage& message)
+    {
+      const float percentile = 0.01f;
+
+      auto& slicedVolume = message.GetOrigin();
+      const OrthancVolumeImage& volume = dynamic_cast<const OrthancVolumeImage&>(slicedVolume);
+
+      std::vector<float> distribution;
+      ReadDistribution(distribution, volume.GetImage().GetInternalImage());
+      std::sort(distribution.begin(), distribution.end());
+
+      int start = static_cast<int>(std::ceil(distribution.size() * percentile));
+      int end = static_cast<int>(std::floor(distribution.size() * (1.0f - percentile)));
+
+      float a = 0;
+      float b = 0;
+
+      if (start < end &&
+        start >= 0 &&
+        end < static_cast<int>(distribution.size()))
+      {
+        a = distribution[start];
+        b = distribution[end];
+      }
+      else if (!distribution.empty())
+      {
+        // Too small distribution: Use full range
+        a = distribution.front();
+        b = distribution.back();
+      }
+
+      //printf("WINDOWING %f %f\n", a, b);
+
+      for (const auto& pair : doseCtWidgetLayerPairs_)
+      {
+        auto widget = pair.first;
+        auto layer = pair.second;
+        RenderStyle s = widget->GetLayerStyle(layer);
+        s.windowing_ = ImageWindowing_Custom;
+        s.customWindowCenter_ = static_cast<float>(converter_.Apply((a + b) / 2.0f));
+        s.customWindowWidth_ = static_cast<float>(converter_.Apply(b - a));
+
+        // 96.210556 => 192.421112
+        widget->SetLayerStyle(layer, s);
+        printf("Windowing: %f => %f\n", s.customWindowCenter_, s.customWindowWidth_);
+      }
+    }
+      
+
+
+      size_t AddDoseLayer(SliceViewerWidget& widget,
+        OrthancVolumeImage& volume, VolumeProjection projection);
+
+      void AddStructLayer(
+        SliceViewerWidget& widget, StructureSetLoader& loader);
+
+      SliceViewerWidget* CreateDoseCtWidget(
+        std::unique_ptr<OrthancVolumeImage>& ct,
+        std::unique_ptr<OrthancVolumeImage>& dose,
+        std::unique_ptr<StructureSetLoader>& structLoader,
+        VolumeProjection projection);
+
+      void AddCtLayer(SliceViewerWidget& widget, OrthancVolumeImage& volume);
+
+      std::unique_ptr<Interactor>         mainWidgetInteractor_;
+      const DicomSeriesVolumeSlicer*    source_;
+      unsigned int                      slice_;
+
+      std::string                                        ctSeries_;
+      std::string                                        doseInstance_;
+      std::string                                        doseSeries_;
+      std::string                                        structInstance_;
+      std::unique_ptr<OrthancStone::OrthancVolumeImage>    dose_;
+      std::unique_ptr<OrthancStone::OrthancVolumeImage>    ct_;
+      std::unique_ptr<OrthancStone::StructureSetLoader>    struct_;
+
+    public:
+      RtViewerDemoApplication(MessageBroker& broker) :
+        IObserver(broker),
+        source_(NULL),
+        slice_(0)
+      {
+      }
+
+      /*
+      dev options on bgo xps15
+
+      COMMAND LINE
+      --ct-series=a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa --dose-instance=830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb --struct-instance=54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9
+
+      URL PARAMETERS
+      ?ct-series=a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa&dose-instance=830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb&struct-instance=54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9
+
+      */
+
+      void ParseParameters(const boost::program_options::variables_map&  parameters)
+      {
+        // Generic
+        {
+          if (parameters.count("verbose"))
+          {
+            Orthanc::Logging::EnableInfoLevel(true);
+            LOG(INFO) << "Verbose logs (info) are enabled";
+          }
+        }
+
+        {
+          if (parameters.count("trace"))
+          {
+            LOG(INFO) << "parameters.count(\"trace\") != 0";
+            Orthanc::Logging::EnableTraceLevel(true);
+            VLOG(1) << "Trace logs (debug) are enabled";
+          }
+        }
+
+        // CT series
+        {
+
+          if (parameters.count("ct-series") != 1)
+          {
+            LOG(ERROR) << "There must be exactly one CT series specified";
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+          }
+          ctSeries_ = parameters["ct-series"].as<std::string>();
+        }
+
+        // RTDOSE 
+        {
+          if (parameters.count("dose-instance") == 1)
+          {
+            doseInstance_ = parameters["dose-instance"].as<std::string>();
+          }
+          else
+          {
+#ifdef BGO_NOT_IMPLEMENTED_YET
+            // Dose series
+            if (parameters.count("dose-series") != 1)
+            {
+              LOG(ERROR) << "the RTDOSE series is missing";
+              throw Orthanc::OrthancException(
+                Orthanc::ErrorCode_ParameterOutOfRange);
+            }
+            doseSeries_ = parameters["ct"].as<std::string>();
+#endif
+            LOG(ERROR) << "the RTSTRUCT instance is missing";
+            throw Orthanc::OrthancException(
+              Orthanc::ErrorCode_ParameterOutOfRange);
+          }
+        }
+        
+        // RTSTRUCT 
+        {
+          if (parameters.count("struct-instance") == 1)
+          {
+            structInstance_ = parameters["struct-instance"].as<std::string>();
+          }
+          else
+          {
+#ifdef BGO_NOT_IMPLEMENTED_YET
+            // Struct series
+            if (parameters.count("struct-series") != 1)
+            {
+              LOG(ERROR) << "the RTSTRUCT series is missing";
+              throw Orthanc::OrthancException(
+                Orthanc::ErrorCode_ParameterOutOfRange);
+            }
+            structSeries_ = parameters["struct-series"].as<std::string>();
+#endif
+            LOG(ERROR) << "the RTSTRUCT instance is missing";
+            throw Orthanc::OrthancException(
+              Orthanc::ErrorCode_ParameterOutOfRange);
+          }
+        }
+      }
+
+      virtual void DeclareStartupOptions(
+        boost::program_options::options_description& options)
+      {
+        boost::program_options::options_description generic(
+          "RtViewerDemo options. Please note that some of these options "
+          "are mutually exclusive");
+        generic.add_options()
+          ("ct-series", boost::program_options::value<std::string>(),
+            "Orthanc ID of the CT series")
+          ("dose-instance", boost::program_options::value<std::string>(), 
+            "Orthanc ID of the RTDOSE instance (incompatible with dose-series)")
+          ("dose-series", boost::program_options::value<std::string>(), 
+            "NOT IMPLEMENTED YET. Orthanc ID of the RTDOSE series (incompatible"
+            " with dose-instance)")
+          ("struct-instance", boost::program_options::value<std::string>(), 
+            "Orthanc ID of the RTSTRUCT instance (incompatible with struct-"
+            "series)")
+          ("struct-series", boost::program_options::value<std::string>(), 
+            "NOT IMPLEMENTED YET. Orthanc ID of the RTSTRUCT (incompatible with"
+            " struct-instance)")
+          ("smooth", boost::program_options::value<bool>()->default_value(true),
+            "Enable bilinear image smoothing")
+          ;
+
+        options.add(generic);
+      }
+
+      virtual void Initialize(
+        StoneApplicationContext*                      context,
+        IStatusBar&                                   statusBar,
+        const boost::program_options::variables_map&  parameters)
+      {
+        using namespace OrthancStone;
+
+        ParseParameters(parameters);
+
+        context_ = context;
+
+        statusBar.SetMessage("Use the key \"s\" to reinitialize the layout");
+
+        if (!ctSeries_.empty())
+        {
+          printf("CT = [%s]\n", ctSeries_.c_str());
+
+          ct_.reset(new OrthancStone::OrthancVolumeImage(
+            IObserver::GetBroker(), context->GetOrthancApiClient(), false));
+          ct_->ScheduleLoadSeries(ctSeries_);
+          //ct_->ScheduleLoadSeries(
+          //  "a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");
+          //ct_->ScheduleLoadSeries(
+          //  "03677739-1d8bca40-db1daf59-d74ff548-7f6fc9c0");
+        }
+
+        if (!doseSeries_.empty() ||
+          !doseInstance_.empty())
+        {
+          dose_.reset(new OrthancStone::OrthancVolumeImage(
+            IObserver::GetBroker(), context->GetOrthancApiClient(), true));
+
+
+          dose_->RegisterObserverCallback(
+            new Callable<RtViewerDemoApplication, ISlicedVolume::VolumeReadyMessage>
+            (*this, &RtViewerDemoApplication::OnVolumeReadyMessage));
+
+          dose_->RegisterObserverCallback(
+            new Callable<RtViewerDemoApplication, ISlicedVolume::SliceContentChangedMessage>
+            (*this, &RtViewerDemoApplication::OnSliceContentChangedMessage));
+
+          if (doseInstance_.empty())
+          {
+            dose_->ScheduleLoadSeries(doseSeries_);
+          }
+          else
+          {
+            dose_->ScheduleLoadInstance(doseInstance_);
+          }
+
+          //dose_->ScheduleLoadInstance(
+            //"830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb");  // 1
+          //dose_->ScheduleLoadInstance(
+            //"269f26f4-0c83eeeb-2e67abbd-5467a40f-f1bec90c"); //0522c0001 TCIA
+        }
+
+        if (!structInstance_.empty())
+        {
+          struct_.reset(new OrthancStone::StructureSetLoader(
+            IObserver::GetBroker(), context->GetOrthancApiClient()));
+
+          struct_->ScheduleLoadInstance(structInstance_);
+
+          //struct_->ScheduleLoadInstance(
+            //"54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9");
+          //struct_->ScheduleLoadInstance(
+            //"17cd032b-ad92a438-ca05f06a-f9e96668-7e3e9e20"); // 0522c0001 TCIA
+        }
+
+        mainWidget_ = new LayoutWidget("main-layout");
+        mainWidget_->SetBackgroundColor(0, 0, 0);
+        mainWidget_->SetBackgroundCleared(true);
+        mainWidget_->SetPadding(0);
+
+        auto axialWidget = CreateDoseCtWidget
+        (ct_, dose_, struct_, OrthancStone::VolumeProjection_Axial);
+        mainWidget_->AddWidget(axialWidget);
+               
+        std::unique_ptr<OrthancStone::LayoutWidget> subLayout(
+          new OrthancStone::LayoutWidget("main-layout"));
+        subLayout->SetVertical();
+        subLayout->SetPadding(5);
+
+        auto coronalWidget = CreateDoseCtWidget
+        (ct_, dose_, struct_, OrthancStone::VolumeProjection_Coronal);
+        subLayout->AddWidget(coronalWidget);
+
+        auto sagittalWidget = CreateDoseCtWidget
+        (ct_, dose_, struct_, OrthancStone::VolumeProjection_Sagittal);
+        subLayout->AddWidget(sagittalWidget);
+        
+        mainWidget_->AddWidget(subLayout.release());
+      }
+    };
+
+
+    size_t RtViewerDemoApplication::AddDoseLayer(
+      SliceViewerWidget& widget,
+      OrthancVolumeImage& volume, VolumeProjection projection)
+    {
+      size_t layer = widget.AddLayer(
+        new VolumeImageMPRSlicer(IObserver::GetBroker(), volume));
+
+      RenderStyle s;
+      //s.drawGrid_ = true;
+      s.SetColor(255, 0, 0);  // Draw missing PET layer in red
+      s.alpha_ = 0.3f;
+      s.applyLut_ = true;
+      s.lut_ = Orthanc::EmbeddedResources::COLORMAP_JET;
+      s.interpolation_ = ImageInterpolation_Bilinear;
+      widget.SetLayerStyle(layer, s);
+
+      return layer;
+    }
+
+    void RtViewerDemoApplication::AddStructLayer(
+      SliceViewerWidget& widget, StructureSetLoader& loader)
+    {
+      widget.AddLayer(new DicomStructureSetSlicer(
+        IObserver::GetBroker(), loader));
+    }
+
+    SliceViewerWidget* RtViewerDemoApplication::CreateDoseCtWidget(
+      std::unique_ptr<OrthancVolumeImage>& ct,
+      std::unique_ptr<OrthancVolumeImage>& dose,
+      std::unique_ptr<StructureSetLoader>& structLoader,
+      VolumeProjection projection)
+    {
+      std::unique_ptr<OrthancStone::SliceViewerWidget> widget(
+        new OrthancStone::SliceViewerWidget(IObserver::GetBroker(),
+          "ct-dose-widget"));
+
+      if (ct.get() != NULL)
+      {
+        AddCtLayer(*widget, *ct);
+      }
+
+      if (dose.get() != NULL)
+      {
+        size_t layer = AddDoseLayer(*widget, *dose, projection);
+
+        // we need to store the dose rendering widget because we'll update them
+        // according to various asynchronous events
+        doseCtWidgetLayerPairs_.push_back(std::make_pair(widget.get(), layer));
+#if 0
+        interactors_.push_back(new VolumeImageInteractor(
+          IObserver::GetBroker(), *dose, *widget, projection));
+#else
+        interactors_.push_back(new DoseInteractor(
+          IObserver::GetBroker(), *dose, *widget, projection, layer));
+#endif
+      }
+      else if (ct.get() != NULL)
+      {
+        interactors_.push_back(
+          new VolumeImageInteractor(
+            IObserver::GetBroker(), *ct, *widget, projection));
+      }
+
+      if (structLoader.get() != NULL)
+      {
+        AddStructLayer(*widget, *structLoader);
+      }
+
+      return widget.release();
+    }
+
+    void RtViewerDemoApplication::AddCtLayer(
+      SliceViewerWidget& widget,
+      OrthancVolumeImage& volume)
+    {
+      size_t layer = widget.AddLayer(
+        new VolumeImageMPRSlicer(IObserver::GetBroker(), volume));
+
+      RenderStyle s;
+      //s.drawGrid_ = true;
+      s.alpha_ = 1;
+      s.windowing_ = ImageWindowing_Bone;
+      widget.SetLayerStyle(layer, s);
+    }
+  }
+}
+
+
+
+#if ORTHANC_ENABLE_WASM==1
+
+#include "Platforms/Wasm/WasmWebService.h"
+#include "Platforms/Wasm/WasmViewport.h"
+
+#include <emscripten/emscripten.h>
+
+//#include "SampleList.h"
+
+
+OrthancStone::IStoneApplication* CreateUserApplication(OrthancStone::MessageBroker& broker)
+{
+  return new OrthancStone::Samples::RtViewerDemoApplication(broker);
+}
+
+OrthancStone::WasmPlatformApplicationAdapter* CreateWasmApplicationAdapter(OrthancStone::MessageBroker& broker, OrthancStone::IStoneApplication* application)
+{
+  return dynamic_cast<OrthancStone::Samples::RtViewerDemoApplication*>(application)->CreateWasmApplicationAdapter(broker);
+}
+
+#else
+
+//#include "SampleList.h"
+#if ORTHANC_ENABLE_SDL==1
+#include "Applications/Sdl/SdlStoneApplicationRunner.h"
+#endif
+#if ORTHANC_ENABLE_QT==1
+#include "Applications/Qt/SampleQtApplicationRunner.h"
+#endif
+#include "Framework/Messages/MessageBroker.h"
+
+int main(int argc, char* argv[])
+{
+  OrthancStone::MessageBroker broker;
+  OrthancStone::Samples::RtViewerDemoApplication sampleStoneApplication(broker);
+
+#if ORTHANC_ENABLE_SDL==1
+  OrthancStone::SdlStoneApplicationRunner sdlApplicationRunner(broker, sampleStoneApplication);
+  return sdlApplicationRunner.Execute(argc, argv);
+#endif
+#if ORTHANC_ENABLE_QT==1
+  OrthancStone::Samples::SampleQtApplicationRunner qtAppRunner(broker, sampleStoneApplication);
+  return qtAppRunner.Execute(argc, argv);
+#endif
+}
+
+
+#endif
+
+
+
+
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/rt-viewer-demo/nginx.local.conf	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,44 @@
+# Local config to serve the WASM samples static files and reverse proxy Orthanc.
+# Uses port 9977 instead of 80.
+
+# `events` section is mandatory
+events {
+  worker_connections 1024; # Default: 1024
+}
+
+http {
+
+  # prevent nginx sync issues on OSX
+  proxy_buffering off;
+
+  server {
+    listen 9977 default_server;
+    client_max_body_size 4G;
+
+    # location may have to be adjusted depending on your OS and nginx install
+    include /etc/nginx/mime.types;
+    # if not in your system mime.types, add this line to support WASM:
+    # types {
+    #    application/wasm                      wasm; 
+    # }
+
+    # serve WASM static files
+    root build-web/;
+    location / {
+	}
+
+    # reverse proxy orthanc
+	location /orthanc/ {
+		rewrite /orthanc(.*) $1 break;
+		proxy_pass http://127.0.0.1:8042;
+		proxy_set_header Host $http_host;
+		proxy_set_header my-auth-header good-token;
+		proxy_request_buffering off;
+		proxy_max_temp_file_size 0;
+		client_max_body_size 0;
+	}
+
+
+  }
+  
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/rt-viewer-demo/rt-viewer-demo.html	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,25 @@
+<!doctype html>
+
+<html lang="us">
+  <head>
+    <meta charset="utf-8" />
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+    <!-- Disable pinch zoom on mobile devices -->
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+    <meta name="HandheldFriendly" content="true" />
+
+    <title>Simple Viewer</title>
+    <link href="samples-styles.css" rel="stylesheet" />
+
+<body>
+  <div style="width: 100%; height: 5%">
+    <p>RTSTRUCT viewer demonstration</p>
+  </div>
+  <div style="width: 100%; height: 95%">
+    <canvas id="canvas"></canvas>
+  </div>
+  <script type="text/javascript" src="app-rt-viewer-demo.js"></script>
+</body>
+
+</html>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/rt-viewer-demo/rt-viewer-demo.ts	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,5 @@
+import { InitializeWasmApplication } from '../../../Platforms/Wasm/wasm-application-runner';
+
+
+InitializeWasmApplication("RtViewerDemo", "/orthanc");
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/rt-viewer-demo/rt-viewer-demo.tsconfig.json	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,8 @@
+{
+    "extends" : "./tsconfig-samples",
+    "compilerOptions": {
+    },
+    "include" : [
+        "rt-viewer-demo.ts"
+    ]
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/rt-viewer-demo/samples-styles.css	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,16 @@
+html, body {
+  width: 100%;
+  height: 100%;
+  margin: 0px;
+  border: 0;
+  overflow: hidden; /*  Disable scrollbars */
+  display: block;  /* No floating content on sides */
+  background-color: black;
+  color: white;
+  font-family: Arial, Helvetica, sans-serif;
+}
+
+canvas {
+  left:0px;
+  top:0px;
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/rt-viewer-demo/start-serving-files.sh	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+sudo nginx -p $(pwd) -c nginx.local.conf
+
+echo "Please browse to :"
+
+echo "http://localhost:9977/rt-viewer-demo.html?ct-series=a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa&dose-instance=830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb&struct-instance=54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9"
+
+echo "(This requires you have uploaded the correct files to your local Orthanc instance)"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/rt-viewer-demo/stop-serving-files.sh	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+sudo nginx -s stop
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/rt-viewer-demo/tsconfig-samples.json	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,11 @@
+{
+    "extends" : "../../../Platforms/Wasm/tsconfig-stone.json",
+    "compilerOptions": {
+        "sourceMap": false,
+        "lib" : [
+            "es2017",
+            "dom",
+            "dom.iterable"
+        ]
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Samples/Deprecated/tsconfig-stone.json	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,7 @@
+{
+    "include" : [
+        "../../Platforms/Wasm/stone-framework-loader.ts",
+        "../../Platforms/Wasm/wasm-application-runner.ts",
+        "../../Platforms/Wasm/wasm-viewport.ts"
+    ]
+}
--- a/Applications/Samples/EmptyApplication.h	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,58 +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 "SampleApplicationBase.h"
-
-#include "../../Framework/Widgets/EmptyWidget.h"
-
-namespace OrthancStone
-{
-  namespace Samples
-  {
-    class EmptyApplication : public SampleApplicationBase
-    {
-    public:
-      virtual void DeclareStartupOptions(boost::program_options::options_description& options)
-      {
-        boost::program_options::options_description generic("Sample options");
-        generic.add_options()
-          ("red", boost::program_options::value<int>()->default_value(255), "Background color: red channel")
-          ("green", boost::program_options::value<int>()->default_value(0), "Background color: green channel")
-          ("blue", boost::program_options::value<int>()->default_value(0), "Background color: blue channel")
-          ;
-
-        options.add(generic);    
-      }
-
-      virtual void Initialize(IStatusBar& statusBar,
-                              const boost::program_options::variables_map& parameters)
-      {
-        int red = parameters["red"].as<int>();
-        int green = parameters["green"].as<int>();
-        int blue = parameters["blue"].as<int>();
-
-        context_->SetCentralWidget(new EmptyWidget(red, green, blue));
-      }
-    };
-  }
-}
--- a/Applications/Samples/LayoutPetCtFusionApplication.h	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,398 +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 "SampleInteractor.h"
-
-#include "../../Framework/Layers/ReferenceLineFactory.h"
-#include "../../Framework/Layers/DicomStructureSetSlicer.h"
-#include "../../Framework/Widgets/LayoutWidget.h"
-
-#include <Core/Logging.h>
-
-namespace OrthancStone
-{
-  namespace Samples
-  {
-    class LayoutPetCtFusionApplication : 
-      public SampleApplicationBase,
-      public LayeredSceneWidget::ISliceObserver,
-      public WorldSceneWidget::IWorldObserver
-    {
-    private:
-      class Interactor : public SampleInteractor
-      {
-      private:
-        LayoutPetCtFusionApplication& that_;
-
-      public:
-        Interactor(LayoutPetCtFusionApplication& that,
-                   VolumeImage& volume,
-                   VolumeProjection projection, 
-                   bool reverse) :
-          SampleInteractor(volume, projection, reverse),
-          that_(that)
-        {
-        }
-
-        virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget,
-                                                            const SliceGeometry& slice,
-                                                            const ViewportGeometry& view,
-                                                            MouseButton button,
-                                                            double x,
-                                                            double y,
-                                                            IStatusBar* statusBar)
-        {
-          if (button == MouseButton_Left)
-          {
-            // Center the sibling views over the clicked point
-            Vector p = slice.MapSliceToWorldCoordinates(x, y);
-
-            if (statusBar != NULL)
-            {
-              char buf[64];
-              sprintf(buf, "Click on coordinates (%.02f,%.02f,%.02f) in cm", p[0] / 10.0, p[1] / 10.0, p[2] / 10.0);
-              statusBar->SetMessage(buf);
-            }
-
-            that_.interactorAxial_->LookupSliceContainingPoint(*that_.ctAxial_, p);
-            that_.interactorCoronal_->LookupSliceContainingPoint(*that_.ctCoronal_, p);
-            that_.interactorSagittal_->LookupSliceContainingPoint(*that_.ctSagittal_, p);
-          }
-
-          return NULL;
-        }
-
-        virtual void KeyPressed(WorldSceneWidget& widget,
-                                char key,
-                                KeyboardModifiers modifiers,
-                                IStatusBar* statusBar)
-        {
-          if (key == 's')
-          {
-            that_.FitContent();
-          }
-        }
-      };
-
-      bool                 processingEvent_;
-      Interactor*          interactorAxial_;
-      Interactor*          interactorCoronal_;
-      Interactor*          interactorSagittal_;
-      LayeredSceneWidget*  ctAxial_;
-      LayeredSceneWidget*  ctCoronal_;
-      LayeredSceneWidget*  ctSagittal_;
-      LayeredSceneWidget*  petAxial_;
-      LayeredSceneWidget*  petCoronal_;
-      LayeredSceneWidget*  petSagittal_;
-      LayeredSceneWidget*  fusionAxial_;
-      LayeredSceneWidget*  fusionCoronal_;
-      LayeredSceneWidget*  fusionSagittal_;
-
-
-      void FitContent()
-      {
-        petAxial_->FitContent();
-        petCoronal_->FitContent();
-        petSagittal_->FitContent();
-      }
-
-
-      void AddLayer(LayeredSceneWidget& widget,
-                    VolumeImage& volume,
-                    bool isCt)
-      {
-        size_t layer;
-        widget.AddLayer(layer, new VolumeImage::LayerFactory(volume));
-
-        if (isCt)
-        {
-          RenderStyle style; 
-          style.windowing_ = ImageWindowing_Bone;
-          widget.SetLayerStyle(layer, style);
-        }
-        else
-        {
-          RenderStyle style; 
-          style.applyLut_ = true;
-          style.alpha_ = (layer == 0 ? 1.0f : 0.5f);
-          widget.SetLayerStyle(layer, style);
-        }
-      }
-
-
-      void ConnectSiblingLocations(LayeredSceneWidget& axial,
-                                   LayeredSceneWidget& coronal,
-                                   LayeredSceneWidget& sagittal)
-      {
-        ReferenceLineFactory::Configure(axial, coronal);
-        ReferenceLineFactory::Configure(axial, sagittal);
-        ReferenceLineFactory::Configure(coronal, sagittal);
-      }
-
-
-      void SynchronizeView(const WorldSceneWidget& source,
-                           const ViewportGeometry& view,
-                           LayeredSceneWidget& widget1,
-                           LayeredSceneWidget& widget2,
-                           LayeredSceneWidget& widget3)
-      {
-        if (&source == &widget1 ||
-            &source == &widget2 ||
-            &source == &widget3)
-        {
-          if (&source != &widget1)
-          {
-            widget1.SetView(view);
-          }
-
-          if (&source != &widget2)
-          {
-            widget2.SetView(view);
-          }
-
-          if (&source != &widget3)
-          {
-            widget3.SetView(view);
-          }
-        }
-      }
-
-
-      void SynchronizeSlice(const LayeredSceneWidget& source,
-                            const SliceGeometry& slice,
-                            LayeredSceneWidget& widget1,
-                            LayeredSceneWidget& widget2,
-                            LayeredSceneWidget& widget3)
-      {
-        if (&source == &widget1 ||
-            &source == &widget2 ||
-            &source == &widget3)
-        {
-          if (&source != &widget1)
-          {
-            widget1.SetSlice(slice);
-          }
-
-          if (&source != &widget2)
-          {
-            widget2.SetSlice(slice);
-          }
-
-          if (&source != &widget3)
-          {
-            widget3.SetSlice(slice);
-          }
-        }
-      }
-
-
-      LayeredSceneWidget* CreateWidget()
-      {
-        std::unique_ptr<LayeredSceneWidget> widget(new LayeredSceneWidget);
-        widget->Register(dynamic_cast<WorldSceneWidget::IWorldObserver&>(*this));
-        widget->Register(dynamic_cast<LayeredSceneWidget::ISliceObserver&>(*this));
-        return widget.release();
-      }
-
-
-      void CreateLayout(BasicApplicationContext& context)
-      {
-        std::unique_ptr<OrthancStone::LayoutWidget> layout(new OrthancStone::LayoutWidget);
-        layout->SetBackgroundCleared(true);
-        //layout->SetBackgroundColor(255,0,0);
-        layout->SetPadding(5);
-
-        OrthancStone::LayoutWidget& layoutA = dynamic_cast<OrthancStone::LayoutWidget&>
-          (layout->AddWidget(new OrthancStone::LayoutWidget));
-        layoutA.SetPadding(0, 0, 0, 0, 5);
-        layoutA.SetVertical();
-        petAxial_ = &dynamic_cast<LayeredSceneWidget&>(layoutA.AddWidget(CreateWidget()));
-        OrthancStone::LayoutWidget& layoutA2 = dynamic_cast<OrthancStone::LayoutWidget&>
-          (layoutA.AddWidget(new OrthancStone::LayoutWidget));
-        layoutA2.SetPadding(0, 0, 0, 0, 5);
-        petSagittal_ = &dynamic_cast<LayeredSceneWidget&>(layoutA2.AddWidget(CreateWidget()));
-        petCoronal_ = &dynamic_cast<LayeredSceneWidget&>(layoutA2.AddWidget(CreateWidget()));
-
-        OrthancStone::LayoutWidget& layoutB = dynamic_cast<OrthancStone::LayoutWidget&>
-          (layout->AddWidget(new OrthancStone::LayoutWidget));
-        layoutB.SetPadding(0, 0, 0, 0, 5);
-        layoutB.SetVertical();
-        ctAxial_ = &dynamic_cast<LayeredSceneWidget&>(layoutB.AddWidget(CreateWidget()));
-        OrthancStone::LayoutWidget& layoutB2 = dynamic_cast<OrthancStone::LayoutWidget&>
-          (layoutB.AddWidget(new OrthancStone::LayoutWidget));
-        layoutB2.SetPadding(0, 0, 0, 0, 5);
-        ctSagittal_ = &dynamic_cast<LayeredSceneWidget&>(layoutB2.AddWidget(CreateWidget()));
-        ctCoronal_ = &dynamic_cast<LayeredSceneWidget&>(layoutB2.AddWidget(CreateWidget()));
-
-        OrthancStone::LayoutWidget& layoutC = dynamic_cast<OrthancStone::LayoutWidget&>
-          (layout->AddWidget(new OrthancStone::LayoutWidget));
-        layoutC.SetPadding(0, 0, 0, 0, 5);
-        layoutC.SetVertical();
-        fusionAxial_ = &dynamic_cast<LayeredSceneWidget&>(layoutC.AddWidget(CreateWidget()));
-        OrthancStone::LayoutWidget& layoutC2 = dynamic_cast<OrthancStone::LayoutWidget&>
-          (layoutC.AddWidget(new OrthancStone::LayoutWidget));
-        layoutC2.SetPadding(0, 0, 0, 0, 5);
-        fusionSagittal_ = &dynamic_cast<LayeredSceneWidget&>(layoutC2.AddWidget(CreateWidget()));
-        fusionCoronal_ = &dynamic_cast<LayeredSceneWidget&>(layoutC2.AddWidget(CreateWidget()));
-  
-        context.SetCentralWidget(layout.release());
-      }
-
-
-    public:
-      virtual void DeclareCommandLineOptions(boost::program_options::options_description& options)
-      {
-        boost::program_options::options_description generic("Sample options");
-        generic.add_options()
-          ("ct", boost::program_options::value<std::string>(), 
-           "Orthanc ID of the CT series")
-          ("pet", boost::program_options::value<std::string>(), 
-           "Orthanc ID of the PET series")
-          ("rt", boost::program_options::value<std::string>(), 
-           "Orthanc ID of the DICOM RT-STRUCT series (optional)")
-          ("threads", boost::program_options::value<unsigned int>()->default_value(3), 
-           "Number of download threads for the CT series")
-          ;
-
-        options.add(generic);    
-      }
-
-      virtual void Initialize(BasicApplicationContext& context,
-                              IStatusBar& statusBar,
-                              const boost::program_options::variables_map& parameters)
-      {
-        using namespace OrthancStone;
-
-        processingEvent_ = true;
-
-        if (parameters.count("ct") != 1 ||
-            parameters.count("pet") != 1)
-        {
-          LOG(ERROR) << "The series ID is missing";
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-        }
-
-        std::string ct = parameters["ct"].as<std::string>();
-        std::string pet = parameters["pet"].as<std::string>();
-        unsigned int threads = parameters["threads"].as<unsigned int>();
-
-        VolumeImage& ctVolume = context.AddSeriesVolume(ct, true /* progressive download */, threads);
-        VolumeImage& petVolume = context.AddSeriesVolume(pet, true /* progressive download */, 1);
-
-        // Take the PET volume as the reference for the slices
-        interactorAxial_ = &dynamic_cast<Interactor&>
-          (context.AddInteractor(new Interactor(*this, petVolume, VolumeProjection_Axial, false)));
-        interactorCoronal_ = &dynamic_cast<Interactor&>
-          (context.AddInteractor(new Interactor(*this, petVolume, VolumeProjection_Coronal, false)));
-        interactorSagittal_ = &dynamic_cast<Interactor&>
-          (context.AddInteractor(new Interactor(*this, petVolume, VolumeProjection_Sagittal, true)));
-
-        CreateLayout(context);
-
-        AddLayer(*ctAxial_, ctVolume, true);
-        AddLayer(*ctCoronal_, ctVolume, true);
-        AddLayer(*ctSagittal_, ctVolume, true);
-
-        AddLayer(*petAxial_, petVolume, false);
-        AddLayer(*petCoronal_, petVolume, false);
-        AddLayer(*petSagittal_, petVolume, false);
-
-        AddLayer(*fusionAxial_, ctVolume, true);
-        AddLayer(*fusionAxial_, petVolume, false);
-        AddLayer(*fusionCoronal_, ctVolume, true);
-        AddLayer(*fusionCoronal_, petVolume, false);
-        AddLayer(*fusionSagittal_, ctVolume, true);
-        AddLayer(*fusionSagittal_, petVolume, false);
-
-        if (parameters.count("rt") == 1)
-        {
-          DicomStructureSet& rtStruct = context.AddStructureSet(parameters["rt"].as<std::string>());
-
-          Vector p = rtStruct.GetStructureCenter(0);
-          interactorAxial_->GetCursor().LookupSliceContainingPoint(p);
-
-          ctAxial_->AddLayer(new DicomStructureSetSlicer(rtStruct));
-          petAxial_->AddLayer(new DicomStructureSetSlicer(rtStruct));
-          fusionAxial_->AddLayer(new DicomStructureSetSlicer(rtStruct));
-        }        
-
-        ConnectSiblingLocations(*ctAxial_, *ctCoronal_, *ctSagittal_); 
-        ConnectSiblingLocations(*petAxial_, *petCoronal_, *petSagittal_); 
-        ConnectSiblingLocations(*fusionAxial_, *fusionCoronal_, *fusionSagittal_); 
-
-        interactorAxial_->AddWidget(*ctAxial_);
-        interactorAxial_->AddWidget(*petAxial_);
-        interactorAxial_->AddWidget(*fusionAxial_);
-        
-        interactorCoronal_->AddWidget(*ctCoronal_);
-        interactorCoronal_->AddWidget(*petCoronal_);
-        interactorCoronal_->AddWidget(*fusionCoronal_);
-        
-        interactorSagittal_->AddWidget(*ctSagittal_);
-        interactorSagittal_->AddWidget(*petSagittal_);
-        interactorSagittal_->AddWidget(*fusionSagittal_);
-
-        processingEvent_ = false;
-
-        statusBar.SetMessage("Use the key \"t\" to toggle the fullscreen mode");
-        statusBar.SetMessage("Use the key \"s\" to reinitialize the layout");
-      }
-
-      virtual void NotifySizeChange(const WorldSceneWidget& source,
-                                    ViewportGeometry& view)
-      {
-        view.FitContent();
-      }
-
-      virtual void NotifyViewChange(const WorldSceneWidget& source,
-                                    const ViewportGeometry& view)
-      {
-        if (!processingEvent_)  // Avoid reentrant calls
-        {
-          processingEvent_ = true;
-
-          SynchronizeView(source, view, *ctAxial_, *petAxial_, *fusionAxial_);
-          SynchronizeView(source, view, *ctCoronal_, *petCoronal_, *fusionCoronal_);
-          SynchronizeView(source, view, *ctSagittal_, *petSagittal_, *fusionSagittal_);
-          
-          processingEvent_ = false;
-        }
-      }
-
-      virtual void NotifySliceContentChange(const LayeredSceneWidget& source,
-                                     const SliceGeometry& slice)
-      {
-        if (!processingEvent_)  // Avoid reentrant calls
-        {
-          processingEvent_ = true;
-
-          SynchronizeSlice(source, slice, *ctAxial_, *petAxial_, *fusionAxial_);
-          SynchronizeSlice(source, slice, *ctCoronal_, *petCoronal_, *fusionCoronal_);
-          SynchronizeSlice(source, slice, *ctSagittal_, *petSagittal_, *fusionSagittal_);
-          
-          processingEvent_ = false;
-        }
-      }
-    };
-  }
-}
--- a/Applications/Samples/Qt/SampleMainWindow.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +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 "SampleMainWindow.h"
-
-/**
- * Don't use "ui_MainWindow.h" instead of <ui_MainWindow.h> below, as
- * this makes CMake unable to detect when the UI file changes.
- **/
-#include <ui_SampleMainWindow.h>
-#include "../../Applications/Samples/SampleApplicationBase.h"
-
-namespace OrthancStone
-{
-  namespace Samples
-  {
-
-    SampleMainWindow::SampleMainWindow(
-      OrthancStone::NativeStoneApplicationContext& context,
-      OrthancStone::Samples::SampleSingleCanvasApplicationBase& stoneSampleApplication,
-      QWidget *parent) :
-      QStoneMainWindow(context, parent),
-      ui_(new Ui::SampleMainWindow),
-      stoneSampleApplication_(stoneSampleApplication)
-    {
-      ui_->setupUi(this);
-      SetCentralStoneWidget(*ui_->cairoCentralWidget);
-    }
-
-    SampleMainWindow::~SampleMainWindow()
-    {
-      delete ui_;
-    }
-
-  }
-}
--- a/Applications/Samples/Qt/SampleMainWindow.h	Mon Apr 20 18:26:32 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 "../../Qt/QCairoWidget.h"
-#include "../../Qt/QStoneMainWindow.h"
-
-namespace Ui 
-{
-  class SampleMainWindow;
-}
-
-namespace OrthancStone
-{
-  namespace Samples
-  {
-
-    class SampleSingleCanvasApplicationBase;
-
-    class SampleMainWindow : public QStoneMainWindow
-    {
-      Q_OBJECT
-
-    private:
-      Ui::SampleMainWindow*   ui_;
-      SampleSingleCanvasApplicationBase&  stoneSampleApplication_;
-
-    public:
-      explicit SampleMainWindow(OrthancStone::NativeStoneApplicationContext& context, SampleSingleCanvasApplicationBase& stoneSampleApplication, QWidget *parent = 0);
-      ~SampleMainWindow();
-    };
-  }
-}
--- a/Applications/Samples/Qt/SampleMainWindow.ui	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,84 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>SampleMainWindow</class>
- <widget class="QMainWindow" name="SampleMainWindow">
-  <property name="geometry">
-   <rect>
-    <x>0</x>
-    <y>0</y>
-    <width>903</width>
-    <height>634</height>
-   </rect>
-  </property>
-  <property name="minimumSize">
-   <size>
-    <width>500</width>
-    <height>300</height>
-   </size>
-  </property>
-  <property name="baseSize">
-   <size>
-    <width>500</width>
-    <height>300</height>
-   </size>
-  </property>
-  <property name="windowTitle">
-   <string>Stone of Orthanc</string>
-  </property>
-  <property name="layoutDirection">
-   <enum>Qt::LeftToRight</enum>
-  </property>
-  <widget class="QWidget" name="centralwidget">
-   <property name="sizePolicy">
-    <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
-     <horstretch>0</horstretch>
-     <verstretch>0</verstretch>
-    </sizepolicy>
-   </property>
-   <property name="layoutDirection">
-    <enum>Qt::LeftToRight</enum>
-   </property>
-   <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0">
-    <property name="sizeConstraint">
-     <enum>QLayout::SetDefaultConstraint</enum>
-    </property>
-    <item>
-     <widget class="QCairoWidget" name="cairoCentralWidget">
-      <property name="minimumSize">
-       <size>
-        <width>0</width>
-        <height>500</height>
-       </size>
-      </property>
-     </widget>
-    </item>
-   </layout>
-  </widget>
-  <widget class="QMenuBar" name="menubar">
-   <property name="geometry">
-    <rect>
-     <x>0</x>
-     <y>0</y>
-     <width>903</width>
-     <height>22</height>
-    </rect>
-   </property>
-   <widget class="QMenu" name="menuTest">
-    <property name="title">
-     <string>Test</string>
-    </property>
-   </widget>
-   <addaction name="menuTest"/>
-  </widget>
-  <widget class="QStatusBar" name="statusbar"/>
- </widget>
- <customwidgets>
-  <customwidget>
-   <class>QCairoWidget</class>
-   <extends>QGraphicsView</extends>
-   <header location="global">QCairoWidget.h</header>
-  </customwidget>
- </customwidgets>
- <resources/>
- <connections/>
-</ui>
--- a/Applications/Samples/Qt/SampleMainWindowWithButtons.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,96 +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 "SampleMainWindow.h"
-
-/**
- * Don't use "ui_MainWindow.h" instead of <ui_MainWindow.h> below, as
- * this makes CMake unable to detect when the UI file changes.
- **/
-#include <ui_SampleMainWindowWithButtons.h>
-#include "../../Applications/Samples/SampleApplicationBase.h"
-
-namespace OrthancStone
-{
-  namespace Samples
-  {
-
-    SampleMainWindowWithButtons::SampleMainWindowWithButtons(
-      OrthancStone::NativeStoneApplicationContext& context,
-      OrthancStone::Samples::SampleSingleCanvasWithButtonsApplicationBase& stoneSampleApplication,
-      QWidget *parent) :
-      QStoneMainWindow(context, parent),
-      ui_(new Ui::SampleMainWindowWithButtons),
-      stoneSampleApplication_(stoneSampleApplication)
-    {
-      ui_->setupUi(this);
-      SetCentralStoneWidget(*ui_->cairoCentralWidget);
-
-#if QT_VERSION >= 0x050000
-      connect(ui_->toolButton1, &QToolButton::clicked, this, &SampleMainWindowWithButtons::tool1Clicked);
-      connect(ui_->toolButton2, &QToolButton::clicked, this, &SampleMainWindowWithButtons::tool2Clicked);
-      connect(ui_->pushButton1, &QPushButton::clicked, this, &SampleMainWindowWithButtons::pushButton1Clicked);
-      connect(ui_->pushButton1, &QPushButton::clicked, this, &SampleMainWindowWithButtons::pushButton2Clicked);
-#else
-      connect(ui_->toolButton1, SIGNAL(clicked()), this, SLOT(tool1Clicked()));
-      connect(ui_->toolButton2, SIGNAL(clicked()), this, SLOT(tool2Clicked()));
-      connect(ui_->pushButton1, SIGNAL(clicked()), this, SLOT(pushButton1Clicked()));
-      connect(ui_->pushButton1, SIGNAL(clicked()), this, SLOT(pushButton2Clicked()));
-#endif
-
-      std::string pushButton1Name;
-      std::string pushButton2Name;
-      std::string tool1Name;
-      std::string tool2Name;
-      stoneSampleApplication_.GetButtonNames(pushButton1Name, pushButton2Name, tool1Name, tool2Name);
-
-      ui_->toolButton1->setText(QString::fromStdString(tool1Name));
-      ui_->toolButton2->setText(QString::fromStdString(tool2Name));
-      ui_->pushButton1->setText(QString::fromStdString(pushButton1Name));
-      ui_->pushButton2->setText(QString::fromStdString(pushButton2Name));
-    }
-
-    SampleMainWindowWithButtons::~SampleMainWindowWithButtons()
-    {
-      delete ui_;
-    }
-
-    void SampleMainWindowWithButtons::tool1Clicked()
-    {
-      stoneSampleApplication_.OnTool1Clicked();
-    }
-
-    void SampleMainWindowWithButtons::tool2Clicked()
-    {
-      stoneSampleApplication_.OnTool2Clicked();
-    }
-
-    void SampleMainWindowWithButtons::pushButton1Clicked()
-    {
-      stoneSampleApplication_.OnPushButton1Clicked();
-    }
-
-    void SampleMainWindowWithButtons::pushButton2Clicked()
-    {
-      stoneSampleApplication_.OnPushButton2Clicked();
-    }
-
-  }
-}
--- a/Applications/Samples/Qt/SampleMainWindowWithButtons.h	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,56 +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 "../../Qt/QCairoWidget.h"
-#include "../../Qt/QStoneMainWindow.h"
-
-namespace Ui 
-{
-  class SampleMainWindowWithButtons;
-}
-
-namespace OrthancStone
-{
-  namespace Samples
-  {
-
-    class SampleSingleCanvasWithButtonsApplicationBase;
-
-    class SampleMainWindowWithButtons : public QStoneMainWindow
-    {
-      Q_OBJECT
-
-    private:
-      Ui::SampleMainWindowWithButtons*   ui_;
-      SampleSingleCanvasWithButtonsApplicationBase&  stoneSampleApplication_;
-
-    public:
-      explicit SampleMainWindowWithButtons(OrthancStone::NativeStoneApplicationContext& context, SampleSingleCanvasWithButtonsApplicationBase& stoneSampleApplication, QWidget *parent = 0);
-      ~SampleMainWindowWithButtons();
-
-    private slots:
-      void tool1Clicked();
-      void tool2Clicked();
-      void pushButton1Clicked();
-      void pushButton2Clicked();
-    };
-  }
-}
--- a/Applications/Samples/Qt/SampleMainWindowWithButtons.ui	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,130 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>SampleMainWindowWithButtons</class>
- <widget class="QMainWindow" name="SampleMainWindowWithButtons">
-  <property name="geometry">
-   <rect>
-    <x>0</x>
-    <y>0</y>
-    <width>903</width>
-    <height>634</height>
-   </rect>
-  </property>
-  <property name="minimumSize">
-   <size>
-    <width>500</width>
-    <height>300</height>
-   </size>
-  </property>
-  <property name="baseSize">
-   <size>
-    <width>500</width>
-    <height>300</height>
-   </size>
-  </property>
-  <property name="windowTitle">
-   <string>Stone of Orthanc</string>
-  </property>
-  <property name="layoutDirection">
-   <enum>Qt::LeftToRight</enum>
-  </property>
-  <widget class="QWidget" name="centralwidget">
-   <property name="sizePolicy">
-    <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
-     <horstretch>0</horstretch>
-     <verstretch>0</verstretch>
-    </sizepolicy>
-   </property>
-   <property name="layoutDirection">
-    <enum>Qt::LeftToRight</enum>
-   </property>
-   <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0">
-    <property name="sizeConstraint">
-     <enum>QLayout::SetDefaultConstraint</enum>
-    </property>
-    <item>
-     <widget class="QCairoWidget" name="cairoCentralWidget">
-      <property name="minimumSize">
-       <size>
-        <width>0</width>
-        <height>500</height>
-       </size>
-      </property>
-     </widget>
-    </item>
-    <item>
-     <widget class="QGroupBox" name="horizontalGroupBox">
-      <property name="minimumSize">
-       <size>
-        <width>0</width>
-        <height>100</height>
-       </size>
-      </property>
-      <property name="maximumSize">
-       <size>
-        <width>16777215</width>
-        <height>100</height>
-       </size>
-      </property>
-      <layout class="QHBoxLayout" name="horizontalLayout">
-       <item>
-        <widget class="QToolButton" name="toolButton1">
-         <property name="text">
-          <string>tool1</string>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <widget class="QToolButton" name="toolButton2">
-         <property name="text">
-          <string>tool2</string>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <widget class="QPushButton" name="pushButton1">
-         <property name="text">
-          <string>action1</string>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <widget class="QPushButton" name="pushButton2">
-         <property name="text">
-          <string>action2</string>
-         </property>
-        </widget>
-       </item>
-      </layout>
-     </widget>
-    </item>
-   </layout>
-  </widget>
-  <widget class="QMenuBar" name="menubar">
-   <property name="geometry">
-    <rect>
-     <x>0</x>
-     <y>0</y>
-     <width>903</width>
-     <height>22</height>
-    </rect>
-   </property>
-   <widget class="QMenu" name="menuTest">
-    <property name="title">
-     <string>Test</string>
-    </property>
-   </widget>
-   <addaction name="menuTest"/>
-  </widget>
-  <widget class="QStatusBar" name="statusbar"/>
- </widget>
- <customwidgets>
-  <customwidget>
-   <class>QCairoWidget</class>
-   <extends>QGraphicsView</extends>
-   <header location="global">QCairoWidget.h</header>
-  </customwidget>
- </customwidgets>
- <resources/>
- <connections/>
-</ui>
--- a/Applications/Samples/Qt/SampleQtApplicationRunner.h	Mon Apr 20 18:26:32 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 "../../Qt/QtStoneApplicationRunner.h"
-
-#if ORTHANC_ENABLE_QT != 1
-#error this file shall be included only with the ORTHANC_ENABLE_QT set to 1
-#endif
-
-namespace OrthancStone
-{
-  namespace Samples
-  {
-    class SampleQtApplicationRunner : public OrthancStone::QtStoneApplicationRunner
-    {
-    protected:
-      virtual void InitializeMainWindow(OrthancStone::NativeStoneApplicationContext& context)
-      {
-        window_.reset(application_.CreateQtMainWindow());
-      }
-    public:
-      SampleQtApplicationRunner(MessageBroker& broker,
-                                SampleApplicationBase& application)
-        : OrthancStone::QtStoneApplicationRunner(broker, application)
-      {
-      }
-
-    };
-  }
-}
--- a/Applications/Samples/SampleApplicationBase.h	Mon Apr 20 18:26:32 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/>.
- **/
-
-
-#pragma once
-
-#include "../../Applications/IStoneApplication.h"
-#include "../../Framework/Deprecated/Widgets/WorldSceneWidget.h"
-
-#if ORTHANC_ENABLE_WASM==1
-#include "../../Platforms/Wasm/WasmPlatformApplicationAdapter.h"
-#include "../../Platforms/Wasm/Defaults.h"
-#endif
-
-#if ORTHANC_ENABLE_QT==1
-#include "Qt/SampleMainWindow.h"
-#include "Qt/SampleMainWindowWithButtons.h"
-#endif
-
-namespace OrthancStone
-{
-  namespace Samples
-  {
-    class SampleApplicationBase : public IStoneApplication
-    {
-    protected:
-      // ownership is transferred to the application context
-      Deprecated::WorldSceneWidget*  mainWidget_;
-
-    public:
-      virtual void Initialize(StoneApplicationContext* context,
-                              Deprecated::IStatusBar& statusBar,
-                              const boost::program_options::variables_map& parameters) ORTHANC_OVERRIDE
-      {
-      }
-
-      virtual std::string GetTitle() const ORTHANC_OVERRIDE
-      {
-        return "Stone of Orthanc - Sample";
-      }
-
-      /**
-       * In the basic samples, the commands are handled by the platform adapter and NOT
-       * by the application handler
-      */
-      virtual void HandleSerializedMessage(const char* data) ORTHANC_OVERRIDE {};
-
-
-      virtual void Finalize() ORTHANC_OVERRIDE {}
-      virtual 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
-
-      virtual void InitializeWasm() ORTHANC_OVERRIDE
-      {
-        AttachWidgetToWasmViewport("canvas", mainWidget_);
-      }
-
-      virtual WasmPlatformApplicationAdapter* CreateWasmApplicationAdapter(MessageBroker& broker)
-      {
-        return new WasmPlatformApplicationAdapter(broker, *this);
-      }
-#endif
-
-    };
-
-    // this application actually works in Qt and WASM
-    class SampleSingleCanvasWithButtonsApplicationBase : public SampleApplicationBase
-    {
-public:
-      virtual void OnPushButton1Clicked() {}
-      virtual void OnPushButton2Clicked() {}
-      virtual void OnTool1Clicked() {}
-      virtual void OnTool2Clicked() {}
-
-      virtual void GetButtonNames(std::string& pushButton1,
-                                  std::string& pushButton2,
-                                  std::string& tool1,
-                                  std::string& tool2
-                                  ) {
-        pushButton1 = "action1";
-        pushButton2 = "action2";
-        tool1 = "tool1";
-        tool2 = "tool2";
-      }
-
-#if ORTHANC_ENABLE_QT==1
-      virtual QStoneMainWindow* CreateQtMainWindow() {
-        return new SampleMainWindowWithButtons(dynamic_cast<OrthancStone::NativeStoneApplicationContext&>(*context_), *this);
-      }
-#endif
-
-    };
-
-    // this application actually works in SDL and WASM
-    class SampleSingleCanvasApplicationBase : public SampleApplicationBase
-    {
-public:
-
-#if ORTHANC_ENABLE_QT==1
-      virtual QStoneMainWindow* CreateQtMainWindow() {
-        return new SampleMainWindow(dynamic_cast<OrthancStone::NativeStoneApplicationContext&>(*context_), *this);
-      }
-#endif
-    };
-  }
-}
--- a/Applications/Samples/SampleInteractor.h	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,131 +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 "SampleApplicationBase.h"
-
-#include "../../Framework/Widgets/LayeredSceneWidget.h"
-#include "../../Framework/Widgets/IWorldSceneInteractor.h"
-#include "../../Framework/Toolbox/ParallelSlicesCursor.h"
-
-namespace OrthancStone
-{
-  namespace Samples
-  {
-    /**
-     * This is a basic mouse interactor for sample applications. It
-     * contains a set of parallel slices in the 3D space. The mouse
-     * wheel events make the widget change the slice that is
-     * displayed.
-     **/
-    class SampleInteractor : public IWorldSceneInteractor
-    {
-    private:
-      ParallelSlicesCursor   cursor_;
-
-    public:
-      SampleInteractor(VolumeImage& volume,
-                       VolumeProjection projection, 
-                       bool reverse)
-      {
-        std::unique_ptr<ParallelSlices> slices(volume.GetGeometry(projection, reverse));
-        cursor_.SetGeometry(*slices);
-      }
-
-      SampleInteractor(ISeriesLoader& series, 
-                       bool reverse)
-      {
-        if (reverse)
-        {
-          std::unique_ptr<ParallelSlices> slices(series.GetGeometry().Reverse());
-          cursor_.SetGeometry(*slices);
-        }
-        else
-        {
-          cursor_.SetGeometry(series.GetGeometry());
-        }
-      }
-
-      SampleInteractor(const ParallelSlices& slices)
-      {
-        cursor_.SetGeometry(slices);
-      }
-
-      ParallelSlicesCursor& GetCursor()
-      {
-        return cursor_;
-      }
-
-      void AddWidget(LayeredSceneWidget& widget)
-      {
-        widget.SetInteractor(*this);
-        widget.SetSlice(cursor_.GetCurrentSlice());
-      }
-
-      virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget,
-                                                          const ViewportGeometry& view,
-                                                          MouseButton button,
-                                                          double x,
-                                                          double y,
-                                                          IStatusBar* statusBar)
-      {
-        return NULL;
-      }
-
-      virtual void MouseOver(CairoContext& context,
-                             WorldSceneWidget& widget,
-                             const ViewportGeometry& view,
-                             double x,
-                             double y,
-                             IStatusBar* statusBar)
-      {
-      }
-
-      virtual void MouseWheel(WorldSceneWidget& widget,
-                              MouseWheelDirection direction,
-                              KeyboardModifiers modifiers,
-                              IStatusBar* statusBar)
-      {
-        if (cursor_.ApplyWheelEvent(direction, modifiers))
-        {
-          dynamic_cast<LayeredSceneWidget&>(widget).SetSlice(cursor_.GetCurrentSlice());
-        }
-      }
-
-      virtual void KeyPressed(WorldSceneWidget& widget,
-                              char key,
-                              KeyboardModifiers modifiers,
-                              IStatusBar* statusBar)
-      {
-      }
-
-      void LookupSliceContainingPoint(LayeredSceneWidget& widget,
-                                      const Vector& p)
-      {
-        if (cursor_.LookupSliceContainingPoint(p))
-        {
-          widget.SetSlice(cursor_.GetCurrentSlice());
-        }
-      }
-    };
-  }
-}
--- a/Applications/Samples/SampleList.h	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,41 +0,0 @@
-// The macro "ORTHANC_STONE_SAMPLE" must be set by the CMake script
-
-#if ORTHANC_STONE_SAMPLE == 1
-#include "EmptyApplication.h"
-typedef OrthancStone::Samples::EmptyApplication SampleApplication;
-
-#elif ORTHANC_STONE_SAMPLE == 2
-#include "TestPatternApplication.h"
-typedef OrthancStone::Samples::TestPatternApplication SampleApplication;
-
-#elif ORTHANC_STONE_SAMPLE == 3
-#include "SingleFrameApplication.h"
-typedef OrthancStone::Samples::SingleFrameApplication SampleApplication;
-
-#elif ORTHANC_STONE_SAMPLE == 4
-#include "SingleVolumeApplication.h"
-typedef OrthancStone::Samples::SingleVolumeApplication SampleApplication;
-
-#elif ORTHANC_STONE_SAMPLE == 5
-#include "BasicPetCtFusionApplication.h"
-typedef OrthancStone::Samples::BasicPetCtFusionApplication SampleApplication;
-
-#elif ORTHANC_STONE_SAMPLE == 6
-#include "SynchronizedSeriesApplication.h"
-typedef OrthancStone::Samples::SynchronizedSeriesApplication SampleApplication;
-
-#elif ORTHANC_STONE_SAMPLE == 7
-#include "LayoutPetCtFusionApplication.h"
-typedef OrthancStone::Samples::LayoutPetCtFusionApplication SampleApplication;
-
-#elif ORTHANC_STONE_SAMPLE == 8
-#include "SimpleViewerApplicationSingleFile.h"
-typedef OrthancStone::Samples::SimpleViewerApplication SampleApplication;
-
-#elif ORTHANC_STONE_SAMPLE == 9
-#include "SingleFrameEditorApplication.h"
-typedef OrthancStone::Samples::SingleFrameEditorApplication SampleApplication;
-
-#else
-#error Please set the ORTHANC_STONE_SAMPLE macro
-#endif
--- a/Applications/Samples/SampleMainNative.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,45 +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 "SampleList.h"
-#if ORTHANC_ENABLE_SDL==1
-#include "../Sdl/SdlStoneApplicationRunner.h"
-#endif
-#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);
-
-#if ORTHANC_ENABLE_SDL==1
-  OrthancStone::SdlStoneApplicationRunner sdlApplicationRunner(broker, sampleStoneApplication);
-  return sdlApplicationRunner.Execute(argc, argv);
-#endif
-#if ORTHANC_ENABLE_QT==1
-  OrthancStone::Samples::SampleQtApplicationRunner qtAppRunner(broker, sampleStoneApplication);
-  return qtAppRunner.Execute(argc, argv);
-#endif
-}
-
--- a/Applications/Samples/SampleMainWasm.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,37 +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 "Platforms/Wasm/WasmWebService.h"
-#include "Platforms/Wasm/WasmViewport.h"
-
-#include <emscripten/emscripten.h>
-
-#include "SampleList.h"
-
-
-OrthancStone::IStoneApplication* CreateUserApplication(OrthancStone::MessageBroker& broker) 
-{
-  return new SampleApplication(broker);
-}
-
-OrthancStone::WasmPlatformApplicationAdapter* CreateWasmApplicationAdapter(OrthancStone::MessageBroker& broker, OrthancStone::IStoneApplication* application)
-{
-  return dynamic_cast<SampleApplication*>(application)->CreateWasmApplicationAdapter(broker);
-}
\ No newline at end of file
--- a/Applications/Samples/Samples-status.md	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,103 +0,0 @@
-Executable versions
-================
-Generic options
-----------------------
-```
-("help", "Display this help and exit")
-("verbose", "Be verbose in logs")
-("orthanc", boost::program_options::value<std::string>()
-  ->default_value("http://localhost:8042/"),
-  "URL to the Orthanc server")
-("username", "Username for the Orthanc server")
-("password", "Password for the Orthanc server")
-("https-verify", boost::program_options::value<bool>()
-  ->default_value(true), "Check HTTPS certificates")
-```
-OrthancStoneSimpleViewer
--------------------------------------
-- Options:
-    ```
-    - "studyId", std::string, "Orthanc ID of the study"
-    ```
-- study loading works OK
-- Invert does not work:
-```
-void SimpleViewerApplication::ExecuteAction(SimpleViewerApplication::Actions action)
-  {
-    // TODO
-  }
-```
-
-OrthancStoneSimpleViewerSingleFile
--------------------------------------
-- Options:
-    ```
-    - "studyId", std::string, "Orthanc ID of the study"
-    ```
-
-Study loading works.
-
-The `line` and `circle` buttons work and call this:
-```
-virtual void OnTool1Clicked()
-{
-  currentTool_ = Tools_LineMeasure;
-}
-
-virtual void OnTool2Clicked()
-{
-  currentTool_ = Tools_CircleMeasure;
-}
-```
-The `action1` and `action2` buttons are not connected
-
-The following is displayed in the console at launch time:
-```
-W0313 12:20:12.790449 NativeStoneApplicationRunner.cpp:55] Use the key "s" to reinitialize the layout
-W0313 12:20:12.790449 NativeStoneApplicationRunner.cpp:55] Use the key "n" to go to next image in the main viewport
-```
-However, when looking at `MainWidgetInteractor::KeyPressed` (`SimpleViewerApplicationSingleFile.h:169`), only the following is processed:
-- 's': reset layout
-- 'l': select line tool
-- 'c': select circle tool
-
-OrthancStoneSingleFrame
--------------------------------------
-```
-generic.add_options()
-("instance", boost::program_options::value<std::string>(), 
-"Orthanc ID of the instance")
-("frame", boost::program_options::value<unsigned int>()
-  ->default_value(0),
-"Number of the frame, for multi-frame DICOM instances")
-("smooth", boost::program_options::value<bool>()
-  ->default_value(true), 
-"Enable bilinear interpolation to smooth the image");
-```
-only key handled in `KeyPressed` is `s` to call `widget.FitContent()`
-
-
-OrthancStoneSingleFrameEditor
--------------------------------------
-```
-generic.add_options()
-("instance", boost::program_options::value<std::string>(),
-"Orthanc ID of the instance")
-("frame", boost::program_options::value<unsigned int>()
-  ->default_value(0),
-"Number of the frame, for multi-frame DICOM instances");
-```
-Available commands in `KeyPressed` (`SingleFrameEditorApplication.h:280`): 
-- 'a' widget.FitContent()
-- 'c' Crop tool
-- 'm' Mask tool
-- 'd' dump to json and diplay result (?)
-- 'e' export current view to Dicom with dummy tags (?)
-- 'i' wdiget.SwitchInvert
-- 't' Move tool
-- 'n' switch between nearest and bilinear interpolation
-- 'r' Rotate tool
-- 's' Resize tool
-- 'w' Windowing tool
-- 'ctrl+y' redo
-- 'ctrl+z' undo
--- a/Applications/Samples/SimpleViewer/AppStatus.h	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,27 +0,0 @@
-#pragma once
-
-#include <string>
-
-
-namespace SimpleViewer
-{
-  struct AppStatus
-  {
-    std::string patientId;
-    std::string studyDescription;
-    std::string currentInstanceIdInMainViewport;
-    // note: if you add members here, update the serialization code below and deserialization in simple-viewer.ts -> onAppStatusUpdated()
-
-
-    AppStatus()
-    {
-    }
-
-    void ToJson(Json::Value &output) const
-    {
-      output["patientId"] = patientId;
-      output["studyDescription"] = studyDescription;
-      output["currentInstanceIdInMainViewport"] = currentInstanceIdInMainViewport;
-    }
-  };
-}
--- a/Applications/Samples/SimpleViewer/MainWidgetInteractor.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,111 +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 "MainWidgetInteractor.h"
-
-#include "SimpleViewerApplication.h"
-
-namespace SimpleViewer {
-
-  Deprecated::IWorldSceneMouseTracker* MainWidgetInteractor::CreateMouseTracker(Deprecated::WorldSceneWidget& widget,
-                                                                    const Deprecated::ViewportGeometry& view,
-                                                                    MouseButton button,
-                                                                    KeyboardModifiers modifiers,
-                                                                    int viewportX,
-                                                                    int viewportY,
-                                                                    double x,
-                                                                    double y,
-                                                                    Deprecated::IStatusBar* statusBar,
-                                                                    const std::vector<Deprecated::Touch>& displayTouches)
-  {
-    if (button == MouseButton_Left)
-    {
-      if (application_.GetCurrentTool() == Tool_LineMeasure)
-      {
-        return new Deprecated::LineMeasureTracker(statusBar, dynamic_cast<Deprecated::SliceViewerWidget&>(widget).GetSlice(),
-                                      x, y, 255, 0, 0, application_.GetFont());
-      }
-      else if (application_.GetCurrentTool() == Tool_CircleMeasure)
-      {
-        return new Deprecated::CircleMeasureTracker(statusBar, dynamic_cast<Deprecated::SliceViewerWidget&>(widget).GetSlice(),
-                                        x, y, 255, 0, 0, application_.GetFont());
-      }
-      else if (application_.GetCurrentTool() == Tool_Crop)
-      {
-        // TODO
-      }
-      else if (application_.GetCurrentTool() == Tool_Windowing)
-      {
-        // TODO
-      }
-      else if (application_.GetCurrentTool() == Tool_Zoom)
-      {
-        // TODO
-      }
-      else if (application_.GetCurrentTool() == Tool_Pan)
-      {
-        // TODO
-      }
-    }
-    return NULL;
-  }
-
-  void MainWidgetInteractor::MouseOver(CairoContext& context,
-                                       Deprecated::WorldSceneWidget& widget,
-                                       const Deprecated::ViewportGeometry& view,
-                                       double x,
-                                       double y,
-                                       Deprecated::IStatusBar* statusBar)
-  {
-    if (statusBar != NULL)
-    {
-      Vector p = dynamic_cast<Deprecated::SliceViewerWidget&>(widget).GetSlice().MapSliceToWorldCoordinates(x, y);
-
-      char buf[64];
-      sprintf(buf, "X = %.02f Y = %.02f Z = %.02f (in cm)",
-              p[0] / 10.0, p[1] / 10.0, p[2] / 10.0);
-      statusBar->SetMessage(buf);
-    }
-  }
-
-  void MainWidgetInteractor::MouseWheel(Deprecated::WorldSceneWidget& widget,
-                                        MouseWheelDirection direction,
-                                        KeyboardModifiers modifiers,
-                                        Deprecated::IStatusBar* statusBar)
-  {
-  }
-
-  void MainWidgetInteractor::KeyPressed(Deprecated::WorldSceneWidget& widget,
-                                        KeyboardKeys key,
-                                        char keyChar,
-                                        KeyboardModifiers modifiers,
-                                        Deprecated::IStatusBar* statusBar)
-  {
-    switch (keyChar)
-    {
-    case 's':
-      widget.FitContent();
-      break;
-
-    default:
-      break;
-    }
-  }
-}
--- a/Applications/Samples/SimpleViewer/MainWidgetInteractor.h	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,76 +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 "../../../Framework/Deprecated/Widgets/IWorldSceneInteractor.h"
-
-using namespace OrthancStone;
-
-namespace SimpleViewer {
-
-  class SimpleViewerApplication;
-
-  class MainWidgetInteractor : public Deprecated::IWorldSceneInteractor
-  {
-  private:
-    SimpleViewerApplication&  application_;
-
-  public:
-    MainWidgetInteractor(SimpleViewerApplication&  application) :
-      application_(application)
-    {
-    }
-
-    /**
-        WorldSceneWidget: 
-    */
-    virtual Deprecated::IWorldSceneMouseTracker* CreateMouseTracker(Deprecated::WorldSceneWidget& widget,
-                                                                    const Deprecated::ViewportGeometry& view,
-                                                                    MouseButton button,
-                                                                    KeyboardModifiers modifiers,
-                                                                    int viewportX,
-                                                                    int viewportY,
-                                                                    double x,
-                                                                    double y,
-                                                                    Deprecated::IStatusBar* statusBar,
-                                                                    const std::vector<Deprecated::Touch>& displayTouches);
-
-    virtual void MouseOver(CairoContext& context,
-                           Deprecated::WorldSceneWidget& widget,
-                           const Deprecated::ViewportGeometry& view,
-                           double x,
-                           double y,
-                           Deprecated::IStatusBar* statusBar);
-
-    virtual void MouseWheel(Deprecated::WorldSceneWidget& widget,
-                            MouseWheelDirection direction,
-                            KeyboardModifiers modifiers,
-                            Deprecated::IStatusBar* statusBar);
-
-    virtual void KeyPressed(Deprecated::WorldSceneWidget& widget,
-                            KeyboardKeys key,
-                            char keyChar,
-                            KeyboardModifiers modifiers,
-                            Deprecated::IStatusBar* statusBar);
-  };
-
-
-}
--- a/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,109 +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 "SimpleViewerMainWindow.h"
-
-/**
- * Don't use "ui_MainWindow.h" instead of <ui_MainWindow.h> below, as
- * this makes CMake unable to detect when the UI file changes.
- **/
-#include <ui_SimpleViewerMainWindow.h>
-#include "../SimpleViewerApplication.h"
-
-
-namespace SimpleViewer
-{
-  template<typename T, typename U>
-  bool ExecuteCommand(U* handler, const T& command)
-  {
-    std::string serializedCommand = StoneSerialize(command);
-    StoneDispatchToHandler(serializedCommand, handler);
-  }
-
-  SimpleViewerMainWindow::SimpleViewerMainWindow(
-    OrthancStone::NativeStoneApplicationContext& context,
-    SimpleViewerApplication& stoneApplication,
-    QWidget *parent) :
-    QStoneMainWindow(context, parent),
-    ui_(new Ui::SimpleViewerMainWindow),
-    stoneApplication_(stoneApplication)
-  {
-    ui_->setupUi(this);
-    SetCentralStoneWidget(*ui_->cairoCentralWidget);
-
-#if QT_VERSION >= 0x050000
-    connect(ui_->toolButtonCrop, &QToolButton::clicked, this, &SimpleViewerMainWindow::cropClicked);
-    connect(ui_->pushButtonUndoCrop, &QToolButton::clicked, this, &SimpleViewerMainWindow::undoCropClicked);
-    connect(ui_->toolButtonLine, &QToolButton::clicked, this, &SimpleViewerMainWindow::lineClicked);
-    connect(ui_->toolButtonCircle, &QToolButton::clicked, this, &SimpleViewerMainWindow::circleClicked);
-    connect(ui_->toolButtonWindowing, &QToolButton::clicked, this, &SimpleViewerMainWindow::windowingClicked);
-    connect(ui_->pushButtonRotate, &QPushButton::clicked, this, &SimpleViewerMainWindow::rotateClicked);
-    connect(ui_->pushButtonInvert, &QPushButton::clicked, this, &SimpleViewerMainWindow::invertClicked);
-#else
-    connect(ui_->toolButtonCrop, SIGNAL(clicked()), this, SLOT(cropClicked()));
-    connect(ui_->toolButtonLine, SIGNAL(clicked()), this, SLOT(lineClicked()));
-    connect(ui_->toolButtonCircle, SIGNAL(clicked()), this, SLOT(circleClicked()));
-    connect(ui_->toolButtonWindowing, SIGNAL(clicked()), this, SLOT(windowingClicked()));
-    connect(ui_->pushButtonUndoCrop, SIGNAL(clicked()), this, SLOT(undoCropClicked()));
-    connect(ui_->pushButtonRotate, SIGNAL(clicked()), this, SLOT(rotateClicked()));
-    connect(ui_->pushButtonInvert, SIGNAL(clicked()), this, SLOT(invertClicked()));
-#endif
-  }
-
-  SimpleViewerMainWindow::~SimpleViewerMainWindow()
-  {
-    delete ui_;
-  }
-
-  void SimpleViewerMainWindow::cropClicked()
-  {
-    stoneApplication_.ExecuteCommand(SelectTool(Tool_Crop));
-  }
-
-  void SimpleViewerMainWindow::undoCropClicked()
-  {
-    stoneApplication_.ExecuteCommand(Action(ActionType_UndoCrop));
-  }
-
-  void SimpleViewerMainWindow::lineClicked()
-  {
-    stoneApplication_.ExecuteCommand(SelectTool(Tool_LineMeasure));
-  }
-
-  void SimpleViewerMainWindow::circleClicked()
-  {
-    stoneApplication_.ExecuteCommand(SelectTool(Tool_CircleMeasure));
-  }
-
-  void SimpleViewerMainWindow::windowingClicked()
-  {
-    stoneApplication_.ExecuteCommand(SelectTool(Tool_Windowing));
-  }
-
-  void SimpleViewerMainWindow::rotateClicked()
-  {
-    stoneApplication_.ExecuteCommand(Action(ActionType_Rotate));
-  }
-
-  void SimpleViewerMainWindow::invertClicked()
-  {
-    stoneApplication_.ExecuteCommand(Action(ActionType_Invert));
-  }
-}
--- a/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.h	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,57 +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 <Applications/Qt/QCairoWidget.h>
-#include <Applications/Qt/QStoneMainWindow.h>
-
-namespace Ui 
-{
-  class SimpleViewerMainWindow;
-}
-
-using namespace OrthancStone;
-
-namespace SimpleViewer
-{
-  class SimpleViewerApplication;
-
-  class SimpleViewerMainWindow : public QStoneMainWindow
-  {
-    Q_OBJECT
-
-  private:
-    Ui::SimpleViewerMainWindow*   ui_;
-    SimpleViewerApplication&      stoneApplication_;
-
-  public:
-    explicit SimpleViewerMainWindow(OrthancStone::NativeStoneApplicationContext& context, SimpleViewerApplication& stoneApplication, QWidget *parent = 0);
-    ~SimpleViewerMainWindow();
-
-  private slots:
-    void cropClicked();
-    void undoCropClicked();
-    void rotateClicked();
-    void windowingClicked();
-    void lineClicked();
-    void circleClicked();
-    void invertClicked();
-  };
-}
--- a/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.ui	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,151 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>SimpleViewerMainWindow</class>
- <widget class="QMainWindow" name="SimpleViewerMainWindow">
-  <property name="geometry">
-   <rect>
-    <x>0</x>
-    <y>0</y>
-    <width>903</width>
-    <height>634</height>
-   </rect>
-  </property>
-  <property name="minimumSize">
-   <size>
-    <width>500</width>
-    <height>300</height>
-   </size>
-  </property>
-  <property name="baseSize">
-   <size>
-    <width>500</width>
-    <height>300</height>
-   </size>
-  </property>
-  <property name="windowTitle">
-   <string>Stone of Orthanc</string>
-  </property>
-  <property name="layoutDirection">
-   <enum>Qt::LeftToRight</enum>
-  </property>
-  <widget class="QWidget" name="centralwidget">
-   <property name="sizePolicy">
-    <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
-     <horstretch>0</horstretch>
-     <verstretch>0</verstretch>
-    </sizepolicy>
-   </property>
-   <property name="layoutDirection">
-    <enum>Qt::LeftToRight</enum>
-   </property>
-   <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0">
-    <property name="sizeConstraint">
-     <enum>QLayout::SetDefaultConstraint</enum>
-    </property>
-    <item>
-     <widget class="QCairoWidget" name="cairoCentralWidget">
-      <property name="minimumSize">
-       <size>
-        <width>0</width>
-        <height>500</height>
-       </size>
-      </property>
-     </widget>
-    </item>
-    <item>
-     <widget class="QGroupBox" name="horizontalGroupBox">
-      <property name="minimumSize">
-       <size>
-        <width>0</width>
-        <height>100</height>
-       </size>
-      </property>
-      <property name="maximumSize">
-       <size>
-        <width>16777215</width>
-        <height>100</height>
-       </size>
-      </property>
-      <layout class="QHBoxLayout" name="horizontalLayout">
-       <item>
-        <widget class="QToolButton" name="toolButtonWindowing">
-         <property name="text">
-          <string>windowing</string>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <widget class="QToolButton" name="toolButtonCrop">
-         <property name="text">
-          <string>crop</string>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <widget class="QPushButton" name="pushButtonUndoCrop">
-         <property name="text">
-          <string>undo crop</string>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <widget class="QToolButton" name="toolButtonLine">
-         <property name="text">
-          <string>line</string>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <widget class="QToolButton" name="toolButtonCircle">
-         <property name="text">
-          <string>circle</string>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <widget class="QPushButton" name="pushButtonRotate">
-         <property name="text">
-          <string>rotate</string>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <widget class="QPushButton" name="pushButtonInvert">
-         <property name="text">
-          <string>invert</string>
-         </property>
-        </widget>
-       </item>
-      </layout>
-     </widget>
-    </item>
-   </layout>
-  </widget>
-  <widget class="QMenuBar" name="menubar">
-   <property name="geometry">
-    <rect>
-     <x>0</x>
-     <y>0</y>
-     <width>903</width>
-     <height>22</height>
-    </rect>
-   </property>
-   <widget class="QMenu" name="menuTest">
-    <property name="title">
-     <string>Test</string>
-    </property>
-   </widget>
-   <addaction name="menuTest"/>
-  </widget>
-  <widget class="QStatusBar" name="statusbar"/>
- </widget>
- <customwidgets>
-  <customwidget>
-   <class>QCairoWidget</class>
-   <extends>QGraphicsView</extends>
-   <header location="global">QCairoWidget.h</header>
-  </customwidget>
- </customwidgets>
- <resources/>
- <connections/>
-</ui>
--- a/Applications/Samples/SimpleViewer/Qt/mainQt.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-#include "Applications/Qt/QtStoneApplicationRunner.h"
-
-#include "../SimpleViewerApplication.h"
-#include "Framework/Messages/MessageBroker.h"
-
-
-int main(int argc, char* argv[]) 
-{
-  OrthancStone::MessageBroker broker;
-  SimpleViewer::SimpleViewerApplication stoneApplication(broker);
-
-  OrthancStone::QtStoneApplicationRunner qtAppRunner(broker, stoneApplication);
-  return qtAppRunner.Execute(argc, argv);
-}
--- a/Applications/Samples/SimpleViewer/SimpleViewerApplication.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,225 +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 "SimpleViewerApplication.h"
-
-#if ORTHANC_ENABLE_QT == 1
-#  include "Qt/SimpleViewerMainWindow.h"
-#endif
-
-#if ORTHANC_ENABLE_WASM == 1
-#  include <Platforms/Wasm/WasmViewport.h>
-#endif
-
-namespace SimpleViewer
-{
-
-  void SimpleViewerApplication::Initialize(StoneApplicationContext* context,
-                                           Deprecated::IStatusBar& statusBar,
-                                           const boost::program_options::variables_map& parameters)
-  {
-    context_ = context;
-    statusBar_ = &statusBar;
-
-    {// initialize viewports and layout
-      mainLayout_ = new Deprecated::LayoutWidget("main-layout");
-      mainLayout_->SetPadding(10);
-      mainLayout_->SetBackgroundCleared(true);
-      mainLayout_->SetBackgroundColor(0, 0, 0);
-      mainLayout_->SetHorizontal();
-
-      thumbnailsLayout_ = new Deprecated::LayoutWidget("thumbnail-layout");
-      thumbnailsLayout_->SetPadding(10);
-      thumbnailsLayout_->SetBackgroundCleared(true);
-      thumbnailsLayout_->SetBackgroundColor(50, 50, 50);
-      thumbnailsLayout_->SetVertical();
-
-      mainWidget_ = new Deprecated::SliceViewerWidget(IObserver::GetBroker(), "main-viewport");
-      //mainWidget_->RegisterObserver(*this);
-
-      // hierarchy
-      mainLayout_->AddWidget(thumbnailsLayout_);
-      mainLayout_->AddWidget(mainWidget_);
-
-      // sources
-      smartLoader_.reset(new Deprecated::SmartLoader(IObserver::GetBroker(), context->GetOrthancApiClient()));
-      smartLoader_->SetImageQuality(Deprecated::SliceImageQuality_FullPam);
-
-      mainLayout_->SetTransmitMouseOver(true);
-      mainWidgetInteractor_.reset(new MainWidgetInteractor(*this));
-      mainWidget_->SetInteractor(*mainWidgetInteractor_);
-      thumbnailInteractor_.reset(new ThumbnailInteractor(*this));
-    }
-
-    statusBar.SetMessage("Use the key \"s\" to reinitialize the layout");
-    statusBar.SetMessage("Use the key \"n\" to go to next image in the main viewport");
-
-
-    if (parameters.count("studyId") < 1)
-    {
-      LOG(WARNING) << "The study ID is missing, will take the first studyId found in Orthanc";
-      context->GetOrthancApiClient().GetJsonAsync("/studies", new Callable<SimpleViewerApplication, Deprecated::OrthancApiClient::JsonResponseReadyMessage>(*this, &SimpleViewerApplication::OnStudyListReceived));
-    }
-    else
-    {
-      SelectStudy(parameters["studyId"].as<std::string>());
-    }
-  }
-
-
-  void SimpleViewerApplication::DeclareStartupOptions(boost::program_options::options_description& options)
-  {
-    boost::program_options::options_description generic("Sample options");
-    generic.add_options()
-        ("studyId", boost::program_options::value<std::string>(),
-         "Orthanc ID of the study")
-        ;
-
-    options.add(generic);
-  }
-
-  void SimpleViewerApplication::OnStudyListReceived(const Deprecated::OrthancApiClient::JsonResponseReadyMessage& message)
-  {
-    const Json::Value& response = message.GetJson();
-
-    if (response.isArray() &&
-        response.size() >= 1)
-    {
-      SelectStudy(response[0].asString());
-    }
-  }
-  void SimpleViewerApplication::OnStudyReceived(const Deprecated::OrthancApiClient::JsonResponseReadyMessage& message)
-  {
-    const Json::Value& response = message.GetJson();
-
-    if (response.isObject() && response["Series"].isArray())
-    {
-      for (size_t i=0; i < response["Series"].size(); i++)
-      {
-        context_->GetOrthancApiClient().GetJsonAsync("/series/" + response["Series"][(int)i].asString(), new Callable<SimpleViewerApplication, Deprecated::OrthancApiClient::JsonResponseReadyMessage>(*this, &SimpleViewerApplication::OnSeriesReceived));
-      }
-    }
-  }
-
-  void SimpleViewerApplication::OnSeriesReceived(const Deprecated::OrthancApiClient::JsonResponseReadyMessage& message)
-  {
-    const Json::Value& response = message.GetJson();
-
-    if (response.isObject() &&
-        response["Instances"].isArray() &&
-        response["Instances"].size() > 0)
-    {
-      // keep track of all instances IDs
-      const std::string& seriesId = response["ID"].asString();
-      seriesTags_[seriesId] = response;
-      instancesIdsPerSeriesId_[seriesId] = std::vector<std::string>();
-      for (size_t i = 0; i < response["Instances"].size(); i++)
-      {
-        const std::string& instanceId = response["Instances"][static_cast<int>(i)].asString();
-        instancesIdsPerSeriesId_[seriesId].push_back(instanceId);
-      }
-
-      // load the first instance in the thumbnail
-      LoadThumbnailForSeries(seriesId, instancesIdsPerSeriesId_[seriesId][0]);
-
-      // if this is the first thumbnail loaded, load the first instance in the mainWidget
-      if (mainWidget_->GetLayerCount() == 0)
-      {
-        smartLoader_->SetFrameInWidget(*mainWidget_, 0, instancesIdsPerSeriesId_[seriesId][0], 0);
-      }
-    }
-  }
-
-  void SimpleViewerApplication::LoadThumbnailForSeries(const std::string& seriesId, const std::string& instanceId)
-  {
-    LOG(INFO) << "Loading thumbnail for series " << seriesId;
-    
-    Deprecated::SliceViewerWidget* thumbnailWidget = 
-      new Deprecated::SliceViewerWidget(IObserver::GetBroker(), "thumbnail-series-" + seriesId);
-    thumbnails_.push_back(thumbnailWidget);
-    thumbnailsLayout_->AddWidget(thumbnailWidget);
-    
-    thumbnailWidget->RegisterObserverCallback(
-      new Callable<SimpleViewerApplication, Deprecated::SliceViewerWidget::GeometryChangedMessage>
-      (*this, &SimpleViewerApplication::OnWidgetGeometryChanged));
-    
-    smartLoader_->SetFrameInWidget(*thumbnailWidget, 0, instanceId, 0);
-    thumbnailWidget->SetInteractor(*thumbnailInteractor_);
-  }
-
-  void SimpleViewerApplication::SelectStudy(const std::string& studyId)
-  {
-    context_->GetOrthancApiClient().GetJsonAsync("/studies/" + studyId, new Callable<SimpleViewerApplication, Deprecated::OrthancApiClient::JsonResponseReadyMessage>(*this, &SimpleViewerApplication::OnStudyReceived));
-  }
-
-  void SimpleViewerApplication::OnWidgetGeometryChanged(const Deprecated::SliceViewerWidget::GeometryChangedMessage& message)
-  {
-    // TODO: The "const_cast" could probably be replaced by "mainWidget_"
-    const_cast<Deprecated::SliceViewerWidget&>(message.GetOrigin()).FitContent();
-  }
-
-  void SimpleViewerApplication::SelectSeriesInMainViewport(const std::string& seriesId)
-  {
-    smartLoader_->SetFrameInWidget(*mainWidget_, 0, instancesIdsPerSeriesId_[seriesId][0], 0);
-  }
-
-  bool SimpleViewerApplication::Handle(const StoneSampleCommands::SelectTool& value)
-  {
-    currentTool_ = value.tool;
-    return true;
-  }
-
-  bool SimpleViewerApplication::Handle(const StoneSampleCommands::Action& value)
-  {
-    switch (value.type)
-    {
-    case ActionType_Invert:
-      // TODO
-      break;
-    case ActionType_UndoCrop:
-      // TODO
-      break;
-    case ActionType_Rotate:
-      // TODO
-      break;
-    default:
-      throw std::runtime_error("Action type not supported");
-    }
-    return true;
-  }
-
-#if ORTHANC_ENABLE_QT==1
-  QStoneMainWindow* SimpleViewerApplication::CreateQtMainWindow()
-  {
-    return new SimpleViewerMainWindow(dynamic_cast<OrthancStone::NativeStoneApplicationContext&>(*context_), *this);
-  }
-#endif
-
-#if ORTHANC_ENABLE_WASM==1
-  void SimpleViewerApplication::InitializeWasm() {
-
-    AttachWidgetToWasmViewport("canvasThumbnails", thumbnailsLayout_);
-    AttachWidgetToWasmViewport("canvasMain", mainWidget_);
-  }
-#endif
-
-
-}
--- a/Applications/Samples/SimpleViewer/SimpleViewerApplication.h	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,175 +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
-
- /*
- This header contains the command definitions for the sample applications
- */
-#include "Applications/Samples/StoneSampleCommands_generated.hpp"
-using namespace StoneSampleCommands;
-
-#include "Applications/IStoneApplication.h"
-
-#include "../../../Framework/Deprecated/Layers/CircleMeasureTracker.h"
-#include "../../../Framework/Deprecated/Layers/LineMeasureTracker.h"
-#include "../../../Framework/Deprecated/SmartLoader.h"
-#include "../../../Framework/Deprecated/Widgets/LayoutWidget.h"
-#include "../../../Framework/Deprecated/Widgets/SliceViewerWidget.h"
-#include "../../../Framework/Messages/IObserver.h"
-
-#if ORTHANC_ENABLE_WASM==1
-#include "Platforms/Wasm/WasmPlatformApplicationAdapter.h"
-#include "Platforms/Wasm/Defaults.h"
-#endif
-
-#if ORTHANC_ENABLE_QT==1
-#include "Qt/SimpleViewerMainWindow.h"
-#endif
-
-#include <Core/Images/Font.h>
-#include <Core/Logging.h>
-
-#include "ThumbnailInteractor.h"
-#include "MainWidgetInteractor.h"
-#include "AppStatus.h"
-
-using namespace OrthancStone;
-
-
-namespace SimpleViewer
-{
-
-  class SimpleViewerApplication
-    : public IStoneApplication
-    , public IObserver
-    , public IObservable
-    , public StoneSampleCommands::IHandler
-  {
-  public:
-
-    struct StatusUpdatedMessage : public IMessage
-    {
-      ORTHANC_STONE_MESSAGE(__FILE__, __LINE__);
-
-      const AppStatus& status_;
-
-      StatusUpdatedMessage(const AppStatus& status)
-        : status_(status)
-      {
-      }
-    };
-
-  private:
-    Tool                                currentTool_;
-
-    std::unique_ptr<MainWidgetInteractor> mainWidgetInteractor_;
-    std::unique_ptr<ThumbnailInteractor>  thumbnailInteractor_;
-    Deprecated::LayoutWidget*                       mainLayout_;
-    Deprecated::LayoutWidget*                       thumbnailsLayout_;
-    Deprecated::SliceViewerWidget*                  mainWidget_;
-    std::vector<Deprecated::SliceViewerWidget*>     thumbnails_;
-    std::map<std::string, std::vector<std::string> > instancesIdsPerSeriesId_;
-    std::map<std::string, Json::Value>  seriesTags_;
-    unsigned int                        currentInstanceIndex_;
-    Deprecated::WidgetViewport*       wasmViewport1_;
-    Deprecated::WidgetViewport*       wasmViewport2_;
-
-    Deprecated::IStatusBar*                         statusBar_;
-    std::unique_ptr<Deprecated::SmartLoader>          smartLoader_;
-
-    Orthanc::Font                       font_;
-
-  public:
-    SimpleViewerApplication(MessageBroker& broker) :
-      IObserver(broker),
-      IObservable(broker),
-      currentTool_(StoneSampleCommands::Tool_LineMeasure),
-      mainLayout_(NULL),
-      currentInstanceIndex_(0),
-      wasmViewport1_(NULL),
-      wasmViewport2_(NULL)
-    {
-      font_.LoadFromResource(Orthanc::EmbeddedResources::FONT_UBUNTU_MONO_BOLD_16);
-    }
-
-    virtual void Finalize() ORTHANC_OVERRIDE {}
-    virtual Deprecated::IWidget* GetCentralWidget() ORTHANC_OVERRIDE {return mainLayout_;}
-
-    virtual void DeclareStartupOptions(boost::program_options::options_description& options) ORTHANC_OVERRIDE;
-    virtual void Initialize(StoneApplicationContext* context,
-                            Deprecated::IStatusBar& statusBar,
-                            const boost::program_options::variables_map& parameters) ORTHANC_OVERRIDE;
-
-    void OnStudyListReceived(const Deprecated::OrthancApiClient::JsonResponseReadyMessage& message);
-
-    void OnStudyReceived(const Deprecated::OrthancApiClient::JsonResponseReadyMessage& message);
-
-    void OnSeriesReceived(const Deprecated::OrthancApiClient::JsonResponseReadyMessage& message);
-
-    void LoadThumbnailForSeries(const std::string& seriesId, const std::string& instanceId);
-
-    void SelectStudy(const std::string& studyId);
-
-    void OnWidgetGeometryChanged(const Deprecated::SliceViewerWidget::GeometryChangedMessage& message);
-
-    void SelectSeriesInMainViewport(const std::string& seriesId);
-
-
-    Tool GetCurrentTool() const
-    {
-      return currentTool_;
-    }
-
-    const Orthanc::Font& GetFont() const
-    {
-      return font_;
-    }
-
-    // ExecuteAction method was empty (its body was a single "TODO" comment)
-    virtual bool Handle(const SelectTool& value) ORTHANC_OVERRIDE;
-    virtual bool Handle(const Action& value) ORTHANC_OVERRIDE;
-
-    template<typename T>
-    bool ExecuteCommand(const T& cmd)
-    {
-      std::string cmdStr = StoneSampleCommands::StoneSerialize(cmd);
-      return StoneSampleCommands::StoneDispatchToHandler(cmdStr, this);
-    }
-
-    virtual void HandleSerializedMessage(const char* data) ORTHANC_OVERRIDE
-    {
-      StoneSampleCommands::StoneDispatchToHandler(data, this);
-    }
-
-    virtual std::string GetTitle() const ORTHANC_OVERRIDE {return "SimpleViewer";}
-
-#if ORTHANC_ENABLE_WASM==1
-    virtual void InitializeWasm() ORTHANC_OVERRIDE;
-#endif
-
-#if ORTHANC_ENABLE_QT==1
-    virtual QStoneMainWindow* CreateQtMainWindow();
-#endif
-  };
-
-
-}
--- a/Applications/Samples/SimpleViewer/ThumbnailInteractor.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,46 +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 "ThumbnailInteractor.h"
-
-#include "SimpleViewerApplication.h"
-
-namespace SimpleViewer {
-
-  Deprecated::IWorldSceneMouseTracker* ThumbnailInteractor::CreateMouseTracker(Deprecated::WorldSceneWidget& widget,
-                                                                   const Deprecated::ViewportGeometry& view,
-                                                                   MouseButton button,
-                                                                   KeyboardModifiers modifiers,
-                                                                   int viewportX,
-                                                                   int viewportY,
-                                                                   double x,
-                                                                   double y,
-                                                                   Deprecated::IStatusBar* statusBar,
-                                                                   const std::vector<Deprecated::Touch>& displayTouches)
-  {
-    if (button == MouseButton_Left)
-    {
-      statusBar->SetMessage("selected thumbnail " + widget.GetName());
-      std::string seriesId = widget.GetName().substr(strlen("thumbnail-series-"));
-      application_.SelectSeriesInMainViewport(seriesId);
-    }
-    return NULL;
-  }
-}
--- a/Applications/Samples/SimpleViewer/ThumbnailInteractor.h	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,77 +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 "../../../Framework/Deprecated/Widgets/IWorldSceneInteractor.h"
-
-using namespace OrthancStone;
-
-namespace SimpleViewer {
-
-  class SimpleViewerApplication;
-
-  class ThumbnailInteractor : public Deprecated::IWorldSceneInteractor
-  {
-  private:
-    SimpleViewerApplication&  application_;
-  public:
-    ThumbnailInteractor(SimpleViewerApplication&  application) :
-      application_(application)
-    {
-    }
-
-    virtual Deprecated::IWorldSceneMouseTracker* CreateMouseTracker(Deprecated::WorldSceneWidget& widget,
-                                                                    const Deprecated::ViewportGeometry& view,
-                                                                    MouseButton button,
-                                                                    KeyboardModifiers modifiers,
-                                                                    int viewportX,
-                                                                    int viewportY,
-                                                                    double x,
-                                                                    double y,
-                                                                    Deprecated::IStatusBar* statusBar,
-                                                                    const std::vector<Deprecated::Touch>& displayTouches);
-
-    virtual void MouseOver(CairoContext& context,
-                           Deprecated::WorldSceneWidget& widget,
-                           const Deprecated::ViewportGeometry& view,
-                           double x,
-                           double y,
-                           Deprecated::IStatusBar* statusBar)
-    {}
-
-    virtual void MouseWheel(Deprecated::WorldSceneWidget& widget,
-                            MouseWheelDirection direction,
-                            KeyboardModifiers modifiers,
-                            Deprecated::IStatusBar* statusBar)
-    {}
-
-    virtual void KeyPressed(Deprecated::WorldSceneWidget& widget,
-                            KeyboardKeys key,
-                            char keyChar,
-                            KeyboardModifiers modifiers,
-                            Deprecated::IStatusBar* statusBar)
-    {}
-
-  };
-
-
-}
--- a/Applications/Samples/SimpleViewer/Wasm/SimpleViewerWasmApplicationAdapter.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,51 +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 "SimpleViewerWasmApplicationAdapter.h"
-
-namespace SimpleViewer
-{
-
-  SimpleViewerWasmApplicationAdapter::SimpleViewerWasmApplicationAdapter(MessageBroker &broker, SimpleViewerApplication &application)
-      : WasmPlatformApplicationAdapter(broker, application),
-        viewerApplication_(application)
-  {
-    application.RegisterObserverCallback(new Callable<SimpleViewerWasmApplicationAdapter, SimpleViewerApplication::StatusUpdatedMessage>(*this, &SimpleViewerWasmApplicationAdapter::OnStatusUpdated));
-  }
-
-  void SimpleViewerWasmApplicationAdapter::OnStatusUpdated(const SimpleViewerApplication::StatusUpdatedMessage &message)
-  {
-    Json::Value statusJson;
-    message.status_.ToJson(statusJson);
-
-    Json::Value event;
-    event["event"] = "appStatusUpdated";
-    event["data"] = statusJson;
-
-    Json::StreamWriterBuilder builder;
-    std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
-    std::ostringstream outputStr;
-
-    writer->write(event, &outputStr);
-
-    NotifyStatusUpdateFromCppToWebWithString(outputStr.str());
-  }
-
-} // namespace SimpleViewer
\ No newline at end of file
--- a/Applications/Samples/SimpleViewer/Wasm/SimpleViewerWasmApplicationAdapter.h	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,43 +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 <string>
-#include <Framework/Messages/IObserver.h>
-#include <Platforms/Wasm/WasmPlatformApplicationAdapter.h>
-
-#include "../SimpleViewerApplication.h"
-
-namespace SimpleViewer {
-
-  class SimpleViewerWasmApplicationAdapter : public WasmPlatformApplicationAdapter
-    {
-      SimpleViewerApplication&  viewerApplication_;
-
-    public:
-      SimpleViewerWasmApplicationAdapter(MessageBroker& broker, SimpleViewerApplication& application);
-
-    private:
-      void OnStatusUpdated(const SimpleViewerApplication::StatusUpdatedMessage& message);
-
-    };
-
-}
\ No newline at end of file
--- a/Applications/Samples/SimpleViewer/Wasm/mainWasm.cpp	Mon Apr 20 18:26:32 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 "Platforms/Wasm/WasmWebService.h"
-#include "Platforms/Wasm/WasmViewport.h"
-
-#include <emscripten/emscripten.h>
-
-#include "../SimpleViewerApplication.h"
-#include "SimpleViewerWasmApplicationAdapter.h"
-
-
-OrthancStone::IStoneApplication* CreateUserApplication(OrthancStone::MessageBroker& broker) {
-  
-  return new SimpleViewer::SimpleViewerApplication(broker);
-}
-
-OrthancStone::WasmPlatformApplicationAdapter* CreateWasmApplicationAdapter(OrthancStone::MessageBroker& broker, IStoneApplication* application)
-{
-  return new SimpleViewer::SimpleViewerWasmApplicationAdapter(broker, *(dynamic_cast<SimpleViewer::SimpleViewerApplication*>(application)));
-}
--- a/Applications/Samples/SimpleViewer/Wasm/simple-viewer.html	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,43 +0,0 @@
-<!doctype html>
-
-<html lang="us">
-  <head>
-    <meta charset="utf-8" />
-    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-
-    <!-- Disable pinch zoom on mobile devices -->
-    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
-    <meta name="HandheldFriendly" content="true" />
-
-    <title>Simple Viewer</title>
-    <link href="styles.css" rel="stylesheet" />
-
-<body>
-  <div id="breadcrumb">
-    <span id="label-patient-id"></span>
-    <span id="label-study-description"></span>
-    <span id="label-series-description"></span>
-  </div>
-  <div style="height: calc(100% - 50px)">
-    <div style="width: 20%; height: 100%; display: inline-block">
-      <canvas id="canvasThumbnails"></canvas>
-    </div>
-    <div style="width: 70%; height: 100%; display: inline-block">
-      <canvas id="canvasMain"></canvas>
-    </div>
-  </div>
-  <div id="toolbox" style="height: 50px">
-    <button tool-selector="line-measure" class="tool-selector">line</button>
-    <button tool-selector="circle-measure" class="tool-selector">circle</button>
-    <button tool-selector="crop" class="tool-selector">crop</button>
-    <button tool-selector="windowing" class="tool-selector">windowing</button>
-    <button tool-selector="zoom" class="tool-selector">zoom</button>
-    <button tool-selector="pan" class="tool-selector">pan</button>
-    <button action-trigger="rotate-left" class="action-trigger">rotate left</button>
-    <button action-trigger="rotate-right" class="action-trigger">rotate right</button>
-    <button action-trigger="invert" class="action-trigger">invert</button>
-  </div>
-  <script type="text/javascript" src="app-simple-viewer.js"></script>
-</body>
-
-</html>
\ No newline at end of file
--- a/Applications/Samples/SimpleViewer/Wasm/simple-viewer.ts	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,81 +0,0 @@
-import wasmApplicationRunner = require('../../../../Platforms/Wasm/wasm-application-runner');
-
-wasmApplicationRunner.InitializeWasmApplication("OrthancStoneSimpleViewer", "/orthanc");
-
-function SelectTool(toolName: string) {
-  var command = {
-    command: "selectTool:" + toolName,
-    commandType: "generic-no-arg-command",
-    args: {
-    }                                                                                                                       
-  };
-  wasmApplicationRunner.SendSerializedMessageToStoneApplication(JSON.stringify(command));
-}
-
-function PerformAction(actionName: string) {
-  var command = {
-    command: "action:" + actionName,
-    commandType: "generic-no-arg-command",
-    args: {
-    }
-  };
-  wasmApplicationRunner.SendSerializedMessageToStoneApplication(JSON.stringify(command));
-}
-
-class SimpleViewerUI {
-
-  private _labelPatientId: HTMLSpanElement;
-  private _labelStudyDescription: HTMLSpanElement;
-
-  public constructor() {
-    // install "SelectTool" handlers
-    document.querySelectorAll("[tool-selector]").forEach((e) => {
-      (e as HTMLButtonElement).addEventListener("click", () => {
-        SelectTool(e.attributes["tool-selector"].value);
-      });
-    });
-
-    // install "PerformAction" handlers
-    document.querySelectorAll("[action-trigger]").forEach((e) => {
-      (e as HTMLButtonElement).addEventListener("click", () => {
-        PerformAction(e.attributes["action-trigger"].value);
-      });
-    });
-
-    // connect all ui elements to members
-    this._labelPatientId = document.getElementById("label-patient-id") as HTMLSpanElement;
-    this._labelStudyDescription = document.getElementById("label-study-description") as HTMLSpanElement;
-  }
-
-  public onAppStatusUpdated(status: any) {
-    this._labelPatientId.innerText = status["patientId"];
-    this._labelStudyDescription.innerText = status["studyDescription"];
-    // this.highlighThumbnail(status["currentInstanceIdInMainViewport"]);
-  }
-
-}
-
-var ui = new SimpleViewerUI();
-
-// this method is called "from the C++ code" when the StoneApplication is updated.
-// it can be used to update the UI of the application
-function UpdateWebApplicationWithString(statusUpdateMessageString: string) {
-  console.log("updating web application with string: ", statusUpdateMessageString);
-  let statusUpdateMessage = JSON.parse(statusUpdateMessageString);
-
-  if ("event" in statusUpdateMessage) {
-    let eventName = statusUpdateMessage["event"];
-    if (eventName == "appStatusUpdated") {
-      ui.onAppStatusUpdated(statusUpdateMessage["data"]);
-    }
-  }
-}
-
-function UpdateWebApplicationWithSerializedMessage(statusUpdateMessageString: string) {
-  console.log("updating web application with serialized message: ", statusUpdateMessageString);
-  console.log("<not supported in the simple viewer!>");
-}
-
-// make it available to other js scripts in the application
-(<any> window).UpdateWebApplicationWithString = UpdateWebApplicationWithString;
-(<any> window).UpdateWebApplicationWithSerializedMessage = UpdateWebApplicationWithSerializedMessage;
--- a/Applications/Samples/SimpleViewer/Wasm/styles.css	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,54 +0,0 @@
-html, body {
-    width: 100%;
-    height: 100%;
-    margin: 0px;
-    border: 0;
-    overflow: hidden; /*  Disable scrollbars */
-    display: block;  /* No floating content on sides */
-    background-color: black;
-    color: white;
-    font-family: Arial, Helvetica, sans-serif;
-}
-
-canvas {
-    left:0px;
-    top:0px;
-}
-
-#canvas-group {
-    padding:5px;
-    background-color: grey;
-}
-
-#status-group {
-    padding:5px;
-}
-
-#worklist-group {
-    padding:5px;
-}
-
-.vsol-button {
-    height: 40px;
-}
-
-#thumbnails-group ul li {
-    display: inline;
-    list-style: none;
-}
-
-.thumbnail {
-    width: 100px;
-    height: 100px;
-    padding: 3px;
-}
-
-.thumbnail-selected {
-    border-width: 1px;
-    border-color: red;
-    border-style: solid;
-}
-
-#template-thumbnail-li {
-    display: none !important;
-}
\ No newline at end of file
--- a/Applications/Samples/SimpleViewer/Wasm/tsconfig-simple-viewer.json	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-{
-    "extends" : "../../Web/tsconfig-samples",
-    "compilerOptions": {
-    },
-    "include" : [
-        "simple-viewer.ts",
-        "../../build-wasm/ApplicationCommands_generated.ts"
-    ]
-}
--- a/Applications/Samples/SimpleViewerApplicationSingleFile.h	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,460 +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 "SampleApplicationBase.h"
-
-#include "../../Framework/Deprecated/Layers/CircleMeasureTracker.h"
-#include "../../Framework/Deprecated/Layers/LineMeasureTracker.h"
-#include "../../Framework/Deprecated/SmartLoader.h"
-#include "../../Framework/Deprecated/Widgets/LayoutWidget.h"
-#include "../../Framework/Deprecated/Widgets/SliceViewerWidget.h"
-#include "../../Framework/Messages/IObserver.h"
-
-#if ORTHANC_ENABLE_WASM==1
-#include "../../Platforms/Wasm/WasmPlatformApplicationAdapter.h"
-#include "../../Platforms/Wasm/Defaults.h"
-#endif
-
-#include <Core/Images/Font.h>
-#include <Core/Logging.h>
-
-namespace OrthancStone
-{
-  namespace Samples
-  {
-    class SimpleViewerApplication :
-      public SampleSingleCanvasWithButtonsApplicationBase,
-      public IObserver
-    {
-    private:
-      class ThumbnailInteractor : public Deprecated::IWorldSceneInteractor
-      {
-      private:
-        SimpleViewerApplication&  application_;
-
-      public:
-        ThumbnailInteractor(SimpleViewerApplication&  application) :
-          application_(application)
-        {
-        }
-
-        virtual Deprecated::IWorldSceneMouseTracker* CreateMouseTracker(Deprecated::WorldSceneWidget& widget,
-                                                            const Deprecated::ViewportGeometry& view,
-                                                            MouseButton button,
-                                                            KeyboardModifiers modifiers,
-                                                            int viewportX,
-                                                            int viewportY,
-                                                            double x,
-                                                            double y,
-                                                            Deprecated::IStatusBar* statusBar,
-                                                            const std::vector<Deprecated::Touch>& displayTouches)
-        {
-          if (button == MouseButton_Left)
-          {
-            statusBar->SetMessage("selected thumbnail " + widget.GetName());
-            std::string seriesId = widget.GetName().substr(strlen("thumbnail-series-"));
-            application_.SelectSeriesInMainViewport(seriesId);
-          }
-          return NULL;
-        }
-
-        virtual void MouseOver(CairoContext& context,
-                               Deprecated::WorldSceneWidget& widget,
-                               const Deprecated::ViewportGeometry& view,
-                               double x,
-                               double y,
-                               Deprecated::IStatusBar* statusBar)
-        {
-        }
-
-        virtual void MouseWheel(Deprecated::WorldSceneWidget& widget,
-                                MouseWheelDirection direction,
-                                KeyboardModifiers modifiers,
-                                Deprecated::IStatusBar* statusBar)
-        {
-        }
-
-        virtual void KeyPressed(Deprecated::WorldSceneWidget& widget,
-                                KeyboardKeys key,
-                                char keyChar,
-                                KeyboardModifiers modifiers,
-                                Deprecated::IStatusBar* statusBar)
-        {
-        }
-      };
-
-      class MainWidgetInteractor : public Deprecated::IWorldSceneInteractor
-      {
-      private:
-        SimpleViewerApplication&  application_;
-        
-      public:
-        MainWidgetInteractor(SimpleViewerApplication&  application) :
-          application_(application)
-        {
-        }
-        
-        virtual Deprecated::IWorldSceneMouseTracker* CreateMouseTracker(Deprecated::WorldSceneWidget& widget,
-                                                            const Deprecated::ViewportGeometry& view,
-                                                            MouseButton button,
-                                                            KeyboardModifiers modifiers,
-                                                            int viewportX,
-                                                            int viewportY,
-                                                            double x,
-                                                            double y,
-                                                            Deprecated::IStatusBar* statusBar,
-                                                            const std::vector<Deprecated::Touch>& displayTouches)
-        {
-          if (button == MouseButton_Left)
-          {
-            if (application_.currentTool_ == Tool_LineMeasure)
-            {
-              return new Deprecated::LineMeasureTracker(statusBar, dynamic_cast<Deprecated::SliceViewerWidget&>(widget).GetSlice(),
-                                            x, y, 255, 0, 0, application_.GetFont());
-            }
-            else if (application_.currentTool_ == Tool_CircleMeasure)
-            {
-              return new Deprecated::CircleMeasureTracker(statusBar, dynamic_cast<Deprecated::SliceViewerWidget&>(widget).GetSlice(),
-                                              x, y, 255, 0, 0, application_.GetFont());
-            }
-          }
-          return NULL;
-        }
-
-        virtual void MouseOver(CairoContext& context,
-                               Deprecated::WorldSceneWidget& widget,
-                               const Deprecated::ViewportGeometry& view,
-                               double x,
-                               double y,
-                               Deprecated::IStatusBar* statusBar)
-        {
-          if (statusBar != NULL)
-          {
-            Vector p = dynamic_cast<Deprecated::SliceViewerWidget&>(widget).GetSlice().MapSliceToWorldCoordinates(x, y);
-            
-            char buf[64];
-            sprintf(buf, "X = %.02f Y = %.02f Z = %.02f (in cm)",
-                    p[0] / 10.0, p[1] / 10.0, p[2] / 10.0);
-            statusBar->SetMessage(buf);
-          }
-        }
-
-        virtual void MouseWheel(Deprecated::WorldSceneWidget& widget,
-                                MouseWheelDirection direction,
-                                KeyboardModifiers modifiers,
-                                Deprecated::IStatusBar* statusBar)
-        {
-        }
-
-        virtual void KeyPressed(Deprecated::WorldSceneWidget& widget,
-                                KeyboardKeys key,
-                                char keyChar,
-                                KeyboardModifiers modifiers,
-                                Deprecated::IStatusBar* statusBar)
-        {
-          switch (keyChar)
-          {
-            case 's':
-              widget.FitContent();
-              break;
-
-            case 'l':
-              application_.currentTool_ = Tool_LineMeasure;
-              break;
-
-            case 'c':
-              application_.currentTool_ = Tool_CircleMeasure;
-              break;
-
-            default:
-              break;
-          }
-        }
-      };
-
-
-#if ORTHANC_ENABLE_WASM==1
-      class SimpleViewerApplicationAdapter : public WasmPlatformApplicationAdapter
-      {
-        SimpleViewerApplication&  viewerApplication_;
-
-      public:
-        SimpleViewerApplicationAdapter(MessageBroker& broker, SimpleViewerApplication& application)
-          : WasmPlatformApplicationAdapter(broker, application),
-            viewerApplication_(application)
-        {
-        }
-
-        virtual void HandleSerializedMessageFromWeb(std::string& output, const std::string& input) 
-        {
-          if (input == "select-tool:line-measure")
-          {
-            viewerApplication_.currentTool_ = Tool_LineMeasure;
-            NotifyStatusUpdateFromCppToWebWithString("currentTool=line-measure");
-          }
-          else if (input == "select-tool:circle-measure")
-          {
-            viewerApplication_.currentTool_ = Tool_CircleMeasure;
-            NotifyStatusUpdateFromCppToWebWithString("currentTool=circle-measure");
-          }
-
-          output = "ok";
-        }
-
-        virtual void NotifySerializedMessageFromCppToWeb(const std::string& statusUpdateMessage) 
-        {
-          UpdateStoneApplicationStatusFromCppWithSerializedMessage(statusUpdateMessage.c_str());
-        }
-
-        virtual void NotifyStatusUpdateFromCppToWebWithString(const std::string& statusUpdateMessage) 
-        {
-          UpdateStoneApplicationStatusFromCppWithString(statusUpdateMessage.c_str());
-        }
-
-      };
-#endif
-      enum Tool {
-        Tool_LineMeasure,
-        Tool_CircleMeasure
-      };
-
-      Tool                                 currentTool_;
-      std::unique_ptr<MainWidgetInteractor>  mainWidgetInteractor_;
-      std::unique_ptr<ThumbnailInteractor>   thumbnailInteractor_;
-      Deprecated::LayoutWidget*                        mainLayout_;
-      Deprecated::LayoutWidget*                        thumbnailsLayout_;
-      std::vector<Deprecated::SliceViewerWidget*>      thumbnails_;
-
-      std::map<std::string, std::vector<std::string> > instancesIdsPerSeriesId_;
-      std::map<std::string, Json::Value> seriesTags_;
-
-      unsigned int                         currentInstanceIndex_;
-      Deprecated::WidgetViewport*        wasmViewport1_;
-      Deprecated::WidgetViewport*        wasmViewport2_;
-
-      Deprecated::IStatusBar*                          statusBar_;
-      std::unique_ptr<Deprecated::SmartLoader>           smartLoader_;
-
-      Orthanc::Font                        font_;
-
-    public:
-      SimpleViewerApplication(MessageBroker& broker) :
-        IObserver(broker),
-        currentTool_(Tool_LineMeasure),
-        mainLayout_(NULL),
-        currentInstanceIndex_(0),
-        wasmViewport1_(NULL),
-        wasmViewport2_(NULL)
-      {
-        font_.LoadFromResource(Orthanc::EmbeddedResources::FONT_UBUNTU_MONO_BOLD_16);
-//        DeclareIgnoredMessage(MessageType_Widget_ContentChanged);
-      }
-
-      virtual void DeclareStartupOptions(boost::program_options::options_description& options)
-      {
-        boost::program_options::options_description generic("Sample options");
-        generic.add_options()
-          ("studyId", boost::program_options::value<std::string>(),
-           "Orthanc ID of the study")
-          ;
-
-        options.add(generic);
-      }
-
-      virtual void Initialize(StoneApplicationContext* context,
-                              Deprecated::IStatusBar& statusBar,
-                              const boost::program_options::variables_map& parameters)
-      {
-        using namespace OrthancStone;
-
-        context_ = context;
-        statusBar_ = &statusBar;
-
-        {// initialize viewports and layout
-          mainLayout_ = new Deprecated::LayoutWidget("main-layout");
-          mainLayout_->SetPadding(10);
-          mainLayout_->SetBackgroundCleared(true);
-          mainLayout_->SetBackgroundColor(0, 0, 0);
-          mainLayout_->SetHorizontal();
-
-          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");
-          //mainWidget_->RegisterObserver(*this);
-
-          // hierarchy
-          mainLayout_->AddWidget(thumbnailsLayout_);
-          mainLayout_->AddWidget(mainWidget_);
-
-          // sources
-          smartLoader_.reset(new Deprecated::SmartLoader(GetBroker(), context->GetOrthancApiClient()));
-          smartLoader_->SetImageQuality(Deprecated::SliceImageQuality_FullPam);
-
-          mainLayout_->SetTransmitMouseOver(true);
-          mainWidgetInteractor_.reset(new MainWidgetInteractor(*this));
-          mainWidget_->SetInteractor(*mainWidgetInteractor_);
-          thumbnailInteractor_.reset(new ThumbnailInteractor(*this));
-        }
-
-        statusBar.SetMessage("Use the key \"s\" to reinitialize the layout");
-        statusBar.SetMessage("Use the key \"n\" to go to next image in the main viewport");
-
-
-        if (parameters.count("studyId") < 1)
-        {
-          LOG(WARNING) << "The study ID is missing, will take the first studyId found in Orthanc";
-          context->GetOrthancApiClient().GetJsonAsync(
-            "/studies",
-            new Callable<SimpleViewerApplication, Deprecated::OrthancApiClient::JsonResponseReadyMessage>
-            (*this, &SimpleViewerApplication::OnStudyListReceived));
-        }
-        else
-        {
-          SelectStudy(parameters["studyId"].as<std::string>());
-        }
-      }
-
-      void OnStudyListReceived(const Deprecated::OrthancApiClient::JsonResponseReadyMessage& message)
-      {
-        const Json::Value& response = message.GetJson();
-
-        if (response.isArray() &&
-            response.size() >= 1)
-        {
-          SelectStudy(response[0].asString());
-        }
-      }
-      
-      void OnStudyReceived(const Deprecated::OrthancApiClient::JsonResponseReadyMessage& message)
-      {
-        const Json::Value& response = message.GetJson();
-
-        if (response.isObject() && response["Series"].isArray())
-        {
-          for (size_t i=0; i < response["Series"].size(); i++)
-          {
-            context_->GetOrthancApiClient().GetJsonAsync(
-              "/series/" + response["Series"][(int)i].asString(),
-              new Callable<SimpleViewerApplication, Deprecated::OrthancApiClient::JsonResponseReadyMessage>
-              (*this, &SimpleViewerApplication::OnSeriesReceived));
-          }
-        }
-      }
-
-      void OnSeriesReceived(const Deprecated::OrthancApiClient::JsonResponseReadyMessage& message)
-      {
-        const Json::Value& response = message.GetJson();
-
-        if (response.isObject() &&
-            response["Instances"].isArray() &&
-            response["Instances"].size() > 0)
-        {
-          // keep track of all instances IDs
-          const std::string& seriesId = response["ID"].asString();
-          seriesTags_[seriesId] = response;
-          instancesIdsPerSeriesId_[seriesId] = std::vector<std::string>();
-          for (size_t i = 0; i < response["Instances"].size(); i++)
-          {
-            const std::string& instanceId = response["Instances"][static_cast<int>(i)].asString();
-            instancesIdsPerSeriesId_[seriesId].push_back(instanceId);
-          }
-
-          // load the first instance in the thumbnail
-          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_);
-          if (widget.GetLayerCount() == 0)
-          {
-            smartLoader_->SetFrameInWidget(widget, 0, instancesIdsPerSeriesId_[seriesId][0], 0);
-          }
-        }
-      }
-
-      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);
-        thumbnails_.push_back(thumbnailWidget);
-        thumbnailsLayout_->AddWidget(thumbnailWidget);
-        thumbnailWidget->RegisterObserverCallback(new Callable<SimpleViewerApplication, Deprecated::SliceViewerWidget::GeometryChangedMessage>(*this, &SimpleViewerApplication::OnWidgetGeometryChanged));
-        smartLoader_->SetFrameInWidget(*thumbnailWidget, 0, instanceId, 0);
-        thumbnailWidget->SetInteractor(*thumbnailInteractor_);
-      }
-
-      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));
-      }
-
-      void OnWidgetGeometryChanged(const Deprecated::SliceViewerWidget::GeometryChangedMessage& message)
-      {
-        // TODO: The "const_cast" could probably be replaced by "mainWidget"
-        const_cast<Deprecated::SliceViewerWidget&>(message.GetOrigin()).FitContent();
-      }
-
-      void SelectSeriesInMainViewport(const std::string& seriesId)
-      {
-        Deprecated::SliceViewerWidget& widget = *dynamic_cast<Deprecated::SliceViewerWidget*>(mainWidget_);
-        smartLoader_->SetFrameInWidget(widget, 0, instancesIdsPerSeriesId_[seriesId][0], 0);
-      }
-
-      const Orthanc::Font& GetFont() const
-      {
-        return font_;
-      }
-      
-      virtual void OnPushButton1Clicked() {}
-      virtual void OnPushButton2Clicked() {}
-      virtual void OnTool1Clicked() { currentTool_ = Tool_LineMeasure;}
-      virtual void OnTool2Clicked() { currentTool_ = Tool_CircleMeasure;}
-
-      virtual void GetButtonNames(std::string& pushButton1,
-                                  std::string& pushButton2,
-                                  std::string& tool1,
-                                  std::string& tool2)
-      {
-        tool1 = "line";
-        tool2 = "circle";
-        pushButton1 = "action1";
-        pushButton2 = "action2";
-      }
-
-#if ORTHANC_ENABLE_WASM==1
-      virtual void InitializeWasm()
-      {
-        AttachWidgetToWasmViewport("canvas", thumbnailsLayout_);
-        AttachWidgetToWasmViewport("canvas2", mainWidget_);
-      }
-#endif
-
-    };
-  }
-}
--- a/Applications/Samples/SingleFrameApplication.h	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,271 +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 "SampleApplicationBase.h"
-
-#include "../../Framework/Deprecated/Layers/DicomSeriesVolumeSlicer.h"
-#include "../../Framework/Deprecated/Widgets/SliceViewerWidget.h"
-
-#include <Core/Logging.h>
-#include <Core/OrthancException.h>
-
-#include <boost/math/constants/constants.hpp>
-
-
-namespace OrthancStone
-{
-  namespace Samples
-  {
-    class SingleFrameApplication :
-      public SampleSingleCanvasApplicationBase,
-      public IObserver
-    {
-    private:
-      class Interactor : public Deprecated::IWorldSceneInteractor
-      {
-      private:
-        SingleFrameApplication&  application_;
-        
-      public:
-        Interactor(SingleFrameApplication&  application) :
-          application_(application)
-        {
-        }
-        
-        virtual Deprecated::IWorldSceneMouseTracker* CreateMouseTracker(Deprecated::WorldSceneWidget& widget,
-                                                                        const Deprecated::ViewportGeometry& view,
-                                                                        MouseButton button,
-                                                                        KeyboardModifiers modifiers,
-                                                                        int viewportX,
-                                                                        int viewportY,
-                                                                        double x,
-                                                                        double y,
-                                                                        Deprecated::IStatusBar* statusBar,
-                                                                        const std::vector<Deprecated::Touch>& displayTouches)
-        {
-          return NULL;
-        }
-
-        virtual void MouseOver(CairoContext& context,
-                               Deprecated::WorldSceneWidget& widget,
-                               const Deprecated::ViewportGeometry& view,
-                               double x,
-                               double y,
-                               Deprecated::IStatusBar* statusBar)
-        {
-          if (statusBar != NULL)
-          {
-            Vector p = dynamic_cast<Deprecated::SliceViewerWidget&>(widget).GetSlice().MapSliceToWorldCoordinates(x, y);
-            
-            char buf[64];
-            sprintf(buf, "X = %.02f Y = %.02f Z = %.02f (in cm)", 
-                    p[0] / 10.0, p[1] / 10.0, p[2] / 10.0);
-            statusBar->SetMessage(buf);
-          }
-        }
-
-        virtual void MouseWheel(Deprecated::WorldSceneWidget& widget,
-                                MouseWheelDirection direction,
-                                KeyboardModifiers modifiers,
-                                Deprecated::IStatusBar* statusBar)
-        {
-          int scale = (modifiers & KeyboardModifiers_Control ? 10 : 1);
-          
-          switch (direction)
-          {
-            case MouseWheelDirection_Up:
-              application_.OffsetSlice(-scale);
-              break;
-
-            case MouseWheelDirection_Down:
-              application_.OffsetSlice(scale);
-              break;
-
-            default:
-              break;
-          }
-        }
-
-        virtual void KeyPressed(Deprecated::WorldSceneWidget& widget,
-                                KeyboardKeys key,
-                                char keyChar,
-                                KeyboardModifiers modifiers,
-                                Deprecated::IStatusBar* statusBar)
-        {
-          switch (keyChar)
-          {
-            case 's':
-              widget.FitContent();
-              break;
-
-            default:
-              break;
-          }
-        }
-      };
-
-
-      void OffsetSlice(int offset)
-      {
-        if (source_ != NULL)
-        {
-          int slice = static_cast<int>(slice_) + offset;
-
-          if (slice < 0)
-          {
-            slice = 0;
-          }
-
-          if (slice >= static_cast<int>(source_->GetSlicesCount()))
-          {
-            slice = static_cast<int>(source_->GetSlicesCount()) - 1;
-          }
-
-          if (slice != static_cast<int>(slice_)) 
-          {
-            SetSlice(slice);
-          }   
-        }
-      }
-
-
-      Deprecated::SliceViewerWidget& GetMainWidget()
-      {
-        return *dynamic_cast<Deprecated::SliceViewerWidget*>(mainWidget_);
-      }
-      
-
-      void SetSlice(size_t index)
-      {
-        if (source_ != NULL &&
-            index < source_->GetSlicesCount())
-        {
-          slice_ = static_cast<unsigned int>(index);
-          
-#if 1
-          GetMainWidget().SetSlice(source_->GetSlice(slice_).GetGeometry());
-#else
-          // TEST for scene extents - Rotate the axes
-          double a = 15.0 / 180.0 * boost::math::constants::pi<double>();
-
-#if 1
-          Vector x; GeometryToolbox::AssignVector(x, cos(a), sin(a), 0);
-          Vector y; GeometryToolbox::AssignVector(y, -sin(a), cos(a), 0);
-#else
-          // Flip the normal
-          Vector x; GeometryToolbox::AssignVector(x, cos(a), sin(a), 0);
-          Vector y; GeometryToolbox::AssignVector(y, sin(a), -cos(a), 0);
-#endif
-          
-          SliceGeometry s(source_->GetSlice(slice_).GetGeometry().GetOrigin(), x, y);
-          widget_->SetSlice(s);
-#endif
-        }
-      }
-        
-      
-      void OnMainWidgetGeometryReady(const Deprecated::IVolumeSlicer::GeometryReadyMessage& message)
-      {
-        // 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())
-        {
-          SetSlice(source_->GetSlicesCount() / 2);
-        }
-
-        GetMainWidget().FitContent();
-      }
-      
-      std::unique_ptr<Interactor>         mainWidgetInteractor_;
-      const Deprecated::DicomSeriesVolumeSlicer*    source_;
-      unsigned int                      slice_;
-
-    public:
-      SingleFrameApplication(MessageBroker& broker) :
-        IObserver(broker),
-        source_(NULL),
-        slice_(0)
-      {
-      }
-      
-      virtual void DeclareStartupOptions(boost::program_options::options_description& options)
-      {
-        boost::program_options::options_description generic("Sample options");
-        generic.add_options()
-          ("instance", boost::program_options::value<std::string>(), 
-           "Orthanc ID of the instance")
-          ("frame", boost::program_options::value<unsigned int>()->default_value(0),
-           "Number of the frame, for multi-frame DICOM instances")
-          ("smooth", boost::program_options::value<bool>()->default_value(true), 
-           "Enable bilinear interpolation to smooth the image")
-          ;
-
-        options.add(generic);    
-      }
-
-      virtual void Initialize(StoneApplicationContext* context,
-                              Deprecated::IStatusBar& statusBar,
-                              const boost::program_options::variables_map& parameters)
-      {
-        using namespace OrthancStone;
-
-        context_ = context;
-
-        statusBar.SetMessage("Use the key \"s\" to reinitialize the layout");
-
-        if (parameters.count("instance") != 1)
-        {
-          LOG(ERROR) << "The instance ID is missing";
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-        }
-
-        std::string instance = parameters["instance"].as<std::string>();
-        int frame = parameters["frame"].as<unsigned int>();
-
-        mainWidget_ = new Deprecated::SliceViewerWidget(GetBroker(), "main-widget");
-
-        std::unique_ptr<Deprecated::DicomSeriesVolumeSlicer> layer(new Deprecated::DicomSeriesVolumeSlicer(GetBroker(), context->GetOrthancApiClient()));
-        source_ = layer.get();
-        layer->LoadFrame(instance, frame);
-        layer->RegisterObserverCallback(new Callable<SingleFrameApplication, Deprecated::IVolumeSlicer::GeometryReadyMessage>(*this, &SingleFrameApplication::OnMainWidgetGeometryReady));
-        GetMainWidget().AddLayer(layer.release());
-
-        Deprecated::RenderStyle s;
-
-        if (parameters["smooth"].as<bool>())
-        {
-          s.interpolation_ = ImageInterpolation_Bilinear;
-        }
-
-        GetMainWidget().SetLayerStyle(0, s);
-        GetMainWidget().SetTransmitMouseOver(true);
-
-        mainWidgetInteractor_.reset(new Interactor(*this));
-        GetMainWidget().SetInteractor(*mainWidgetInteractor_);
-      }
-    };
-
-
-  }
-}
--- a/Applications/Samples/SingleFrameEditorApplication.h	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,539 +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 "SampleApplicationBase.h"
-
-#include "../../Framework/Radiography/RadiographyLayerCropTracker.h"
-#include "../../Framework/Radiography/RadiographyLayerMaskTracker.h"
-#include "../../Framework/Radiography/RadiographyLayerMoveTracker.h"
-#include "../../Framework/Radiography/RadiographyLayerResizeTracker.h"
-#include "../../Framework/Radiography/RadiographyLayerRotateTracker.h"
-#include "../../Framework/Radiography/RadiographyScene.h"
-#include "../../Framework/Radiography/RadiographySceneCommand.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>
-#include <Core/Logging.h>
-#include <Core/OrthancException.h>
-#include <Core/Images/PngWriter.h>
-#include <Core/Images/PngReader.h>
-
-
-// Export using PAM is faster than using PNG, but requires Orthanc
-// core >= 1.4.3
-#define EXPORT_USING_PAM  1
-
-
-namespace OrthancStone
-{
-  namespace Samples
-  {
-    class RadiographyEditorInteractor :
-        public Deprecated::IWorldSceneInteractor,
-        public IObserver
-    {
-    private:
-      enum Tool
-      {
-        Tool_Move,
-        Tool_Rotate,
-        Tool_Crop,
-        Tool_Resize,
-        Tool_Mask,
-        Tool_Windowing
-      };
-
-
-      StoneApplicationContext*  context_;
-      UndoRedoStack             undoRedoStack_;
-      Tool                      tool_;
-      RadiographyMaskLayer*     maskLayer_;
-
-
-      static double GetHandleSize()
-      {
-        return 10.0;
-      }
-
-
-    public:
-      RadiographyEditorInteractor(MessageBroker& broker) :
-        IObserver(broker),
-        context_(NULL),
-        tool_(Tool_Move),
-        maskLayer_(NULL)
-      {
-      }
-
-      void SetContext(StoneApplicationContext& context)
-      {
-        context_ = &context;
-      }
-
-      void SetMaskLayer(RadiographyMaskLayer* maskLayer)
-      {
-        maskLayer_ = maskLayer;
-      }
-      virtual Deprecated::IWorldSceneMouseTracker* CreateMouseTracker(Deprecated::WorldSceneWidget& worldWidget,
-                                                                      const Deprecated::ViewportGeometry& view,
-                                                                      MouseButton button,
-                                                                      KeyboardModifiers modifiers,
-                                                                      int viewportX,
-                                                                      int viewportY,
-                                                                      double x,
-                                                                      double y,
-                                                                      Deprecated::IStatusBar* statusBar,
-                                                                      const std::vector<Deprecated::Touch>& displayTouches)
-      {
-        RadiographyWidget& widget = dynamic_cast<RadiographyWidget&>(worldWidget);
-
-        if (button == MouseButton_Left)
-        {
-          size_t selected;
-
-          if (tool_ == Tool_Windowing)
-          {
-            return new RadiographyWindowingTracker(
-                  undoRedoStack_,
-                  widget.GetScene(),
-                  widget,
-                  OrthancStone::ImageInterpolation_Nearest,
-                  viewportX, viewportY,
-                  RadiographyWindowingTracker::Action_DecreaseWidth,
-                  RadiographyWindowingTracker::Action_IncreaseWidth,
-                  RadiographyWindowingTracker::Action_DecreaseCenter,
-                  RadiographyWindowingTracker::Action_IncreaseCenter);
-          }
-          else if (!widget.LookupSelectedLayer(selected))
-          {
-            // No layer is currently selected
-            size_t layer;
-            if (widget.GetScene().LookupLayer(layer, x, y))
-            {
-              widget.Select(layer);
-            }
-
-            return NULL;
-          }
-          else if (tool_ == Tool_Crop ||
-                   tool_ == Tool_Resize ||
-                   tool_ == Tool_Mask)
-          {
-            RadiographyScene::LayerAccessor accessor(widget.GetScene(), selected);
-            
-            ControlPoint controlPoint;
-            if (accessor.GetLayer().LookupControlPoint(controlPoint, x, y, view.GetZoom(), GetHandleSize()))
-            {
-              switch (tool_)
-              {
-              case Tool_Crop:
-                return new RadiographyLayerCropTracker
-                    (undoRedoStack_, widget.GetScene(), view, selected, controlPoint);
-
-              case Tool_Mask:
-                return new RadiographyLayerMaskTracker
-                    (undoRedoStack_, widget.GetScene(), view, selected, controlPoint);
-
-              case Tool_Resize:
-                return new RadiographyLayerResizeTracker
-                    (undoRedoStack_, widget.GetScene(), selected, controlPoint,
-                     (modifiers & KeyboardModifiers_Shift));
-
-              default:
-                throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-              }
-            }
-            else
-            {
-              size_t layer;
-
-              if (widget.GetScene().LookupLayer(layer, x, y))
-              {
-                widget.Select(layer);
-              }
-              else
-              {
-                widget.Unselect();
-              }
-
-              return NULL;
-            }
-          }
-          else
-          {
-            size_t layer;
-
-            if (widget.GetScene().LookupLayer(layer, x, y))
-            {
-              if (layer == selected)
-              {
-                switch (tool_)
-                {
-                case Tool_Move:
-                  return new RadiographyLayerMoveTracker
-                      (undoRedoStack_, widget.GetScene(), layer, x, y,
-                       (modifiers & KeyboardModifiers_Shift));
-
-                case Tool_Rotate:
-                  return new RadiographyLayerRotateTracker
-                      (undoRedoStack_, widget.GetScene(), view, layer, x, y,
-                       (modifiers & KeyboardModifiers_Shift));
-
-                default:
-                  break;
-                }
-
-                return NULL;
-              }
-              else
-              {
-                widget.Select(layer);
-                return NULL;
-              }
-            }
-            else
-            {
-              widget.Unselect();
-              return NULL;
-            }
-          }
-        }
-        else
-        {
-          return NULL;
-        }
-        return NULL;
-      }
-
-      virtual void MouseOver(CairoContext& context,
-                             Deprecated::WorldSceneWidget& worldWidget,
-                             const Deprecated::ViewportGeometry& view,
-                             double x,
-                             double y,
-                             Deprecated::IStatusBar* statusBar)
-      {
-        RadiographyWidget& widget = dynamic_cast<RadiographyWidget&>(worldWidget);
-
-#if 0
-        if (statusBar != NULL)
-        {
-          char buf[64];
-          sprintf(buf, "X = %.02f Y = %.02f (in cm)", x / 10.0, y / 10.0);
-          statusBar->SetMessage(buf);
-        }
-#endif
-
-        size_t selected;
-
-        if (widget.LookupSelectedLayer(selected) &&
-            (tool_ == Tool_Crop ||
-             tool_ == Tool_Resize ||
-             tool_ == Tool_Mask))
-        {
-          RadiographyScene::LayerAccessor accessor(widget.GetScene(), selected);
-
-          ControlPoint controlPoint;
-          if (accessor.GetLayer().LookupControlPoint(controlPoint, x, y, view.GetZoom(), GetHandleSize()))
-          {
-            double z = 1.0 / view.GetZoom();
-
-            context.SetSourceColor(255, 0, 0);
-            cairo_t* cr = context.GetObject();
-            cairo_set_line_width(cr, 2.0 * z);
-            cairo_move_to(cr, controlPoint.x - GetHandleSize() * z, controlPoint.y - GetHandleSize() * z);
-            cairo_line_to(cr, controlPoint.x + GetHandleSize() * z, controlPoint.y - GetHandleSize() * z);
-            cairo_line_to(cr, controlPoint.x + GetHandleSize() * z, controlPoint.y + GetHandleSize() * z);
-            cairo_line_to(cr, controlPoint.x - GetHandleSize() * z, controlPoint.y + GetHandleSize() * z);
-            cairo_line_to(cr, controlPoint.x - GetHandleSize() * z, controlPoint.y - GetHandleSize() * z);
-            cairo_stroke(cr);
-          }
-        }
-      }
-
-      virtual void MouseWheel(Deprecated::WorldSceneWidget& widget,
-                              MouseWheelDirection direction,
-                              KeyboardModifiers modifiers,
-                              Deprecated::IStatusBar* statusBar)
-      {
-      }
-
-      virtual void KeyPressed(Deprecated::WorldSceneWidget& worldWidget,
-                              KeyboardKeys key,
-                              char keyChar,
-                              KeyboardModifiers modifiers,
-                              Deprecated::IStatusBar* statusBar)
-      {
-        RadiographyWidget& widget = dynamic_cast<RadiographyWidget&>(worldWidget);
-
-        switch (keyChar)
-        {
-        case 'a':
-          widget.FitContent();
-          break;
-
-        case 'c':
-          tool_ = Tool_Crop;
-          break;
-
-        case 'm':
-          tool_ = Tool_Mask;
-          widget.Select(1);
-          break;
-
-        case 'd':
-        {
-          // dump to json and reload
-          Json::Value snapshot;
-          RadiographySceneWriter writer;
-          writer.Write(snapshot, widget.GetScene());
-
-          LOG(INFO) << "JSON export was successful: "
-                    << snapshot.toStyledString();
-
-          boost::shared_ptr<RadiographyScene> scene(new RadiographyScene(GetBroker()));
-          RadiographySceneReader reader(*scene, context_->GetOrthancApiClient());
-          reader.Read(snapshot);
-
-          widget.SetScene(scene);
-        };break;
-
-        case 'e':
-        {
-          Orthanc::DicomMap tags;
-
-          // Minimal set of tags to generate a valid CR image
-          tags.SetValue(Orthanc::DICOM_TAG_ACCESSION_NUMBER, "NOPE", false);
-          tags.SetValue(Orthanc::DICOM_TAG_BODY_PART_EXAMINED, "PELVIS", false);
-          tags.SetValue(Orthanc::DICOM_TAG_INSTANCE_NUMBER, "1", false);
-          //tags.SetValue(Orthanc::DICOM_TAG_LATERALITY, "", false);
-          tags.SetValue(Orthanc::DICOM_TAG_MANUFACTURER, "OSIMIS", false);
-          tags.SetValue(Orthanc::DICOM_TAG_MODALITY, "CR", false);
-          tags.SetValue(Orthanc::DICOM_TAG_PATIENT_BIRTH_DATE, "20000101", false);
-          tags.SetValue(Orthanc::DICOM_TAG_PATIENT_ID, "hello", false);
-          tags.SetValue(Orthanc::DICOM_TAG_PATIENT_NAME, "HELLO^WORLD", false);
-          tags.SetValue(Orthanc::DICOM_TAG_PATIENT_ORIENTATION, "", false);
-          tags.SetValue(Orthanc::DICOM_TAG_PATIENT_SEX, "M", false);
-          tags.SetValue(Orthanc::DICOM_TAG_REFERRING_PHYSICIAN_NAME, "HOUSE^MD", false);
-          tags.SetValue(Orthanc::DICOM_TAG_SERIES_NUMBER, "1", false);
-          tags.SetValue(Orthanc::DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.1", false);
-          tags.SetValue(Orthanc::DICOM_TAG_STUDY_ID, "STUDY", false);
-          tags.SetValue(Orthanc::DICOM_TAG_VIEW_POSITION, "", false);
-
-          if (context_ != NULL)
-          {
-            widget.GetScene().ExportDicom(context_->GetOrthancApiClient(),
-                                          tags, std::string(), 0.1, 0.1, widget.IsInverted(),
-                                          false /* autoCrop */, widget.GetInterpolation(), EXPORT_USING_PAM);
-          }
-
-          break;
-        }
-
-        case 'i':
-          widget.SwitchInvert();
-          break;
-
-        case 't':
-          tool_ = Tool_Move;
-          break;
-
-        case 'n':
-        {
-          switch (widget.GetInterpolation())
-          {
-          case ImageInterpolation_Nearest:
-            LOG(INFO) << "Switching to bilinear interpolation";
-            widget.SetInterpolation(ImageInterpolation_Bilinear);
-            break;
-
-          case ImageInterpolation_Bilinear:
-            LOG(INFO) << "Switching to nearest neighbor interpolation";
-            widget.SetInterpolation(ImageInterpolation_Nearest);
-            break;
-
-          default:
-            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-          }
-          
-          break;
-        }
-
-        case 'r':
-          tool_ = Tool_Rotate;
-          break;
-
-        case 's':
-          tool_ = Tool_Resize;
-          break;
-
-        case 'w':
-          tool_ = Tool_Windowing;
-          break;
-
-        case 'y':
-          if (modifiers & KeyboardModifiers_Control)
-          {
-            undoRedoStack_.Redo();
-            widget.NotifyContentChanged();
-          }
-          break;
-
-        case 'z':
-          if (modifiers & KeyboardModifiers_Control)
-          {
-            undoRedoStack_.Undo();
-            widget.NotifyContentChanged();
-          }
-          break;
-
-        default:
-          break;
-        }
-      }
-    };
-
-
-
-    class SingleFrameEditorApplication :
-        public SampleSingleCanvasApplicationBase,
-        public IObserver
-    {
-    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";
-      }
-      
-      virtual void DeclareStartupOptions(boost::program_options::options_description& options)
-      {
-        boost::program_options::options_description generic("Sample options");
-        generic.add_options()
-            ("instance", boost::program_options::value<std::string>(),
-             "Orthanc ID of the instance")
-            ("frame", boost::program_options::value<unsigned int>()->default_value(0),
-             "Number of the frame, for multi-frame DICOM instances")
-            ;
-
-        options.add(generic);
-      }
-
-      virtual void Initialize(StoneApplicationContext* context,
-                              Deprecated::IStatusBar& statusBar,
-                              const boost::program_options::variables_map& parameters)
-      {
-        using namespace OrthancStone;
-
-        context_ = context;
-        interactor_.SetContext(*context);
-
-        statusBar.SetMessage("Use the key \"a\" to reinitialize the layout");
-        statusBar.SetMessage("Use the key \"c\" to crop");
-        statusBar.SetMessage("Use the key \"e\" to export DICOM to the Orthanc server");
-        statusBar.SetMessage("Use the key \"f\" to switch full screen");
-        statusBar.SetMessage("Use the key \"i\" to invert contrast");
-        statusBar.SetMessage("Use the key \"m\" to modify the mask");
-        statusBar.SetMessage("Use the key \"n\" to switch between nearest neighbor and bilinear interpolation");
-        statusBar.SetMessage("Use the key \"r\" to rotate objects");
-        statusBar.SetMessage("Use the key \"s\" to resize objects (not applicable to DICOM layers)");
-        statusBar.SetMessage("Use the key \"t\" to move (translate) objects");
-        statusBar.SetMessage("Use the key \"w\" to change windowing");
-        
-        statusBar.SetMessage("Use the key \"ctrl-z\" to undo action");
-        statusBar.SetMessage("Use the key \"ctrl-y\" to redo action");
-
-        if (parameters.count("instance") != 1)
-        {
-          LOG(ERROR) << "The instance ID is missing";
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-        }
-
-        std::string instance = parameters["instance"].as<std::string>();
-        //int frame = parameters["frame"].as<unsigned int>();
-
-        scene_.reset(new RadiographyScene(GetBroker()));
-        
-        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);
-
-#if !defined(ORTHANC_ENABLE_WASM) || ORTHANC_ENABLE_WASM != 1
-        Orthanc::HttpClient::ConfigureSsl(true, "/etc/ssl/certs/ca-certificates.crt");
-#endif
-        
-        //scene_->LoadDicomWebFrame(context->GetWebService());
-        
-        std::vector<Orthanc::ImageProcessing::ImagePoint> mask;
-        mask.push_back(Orthanc::ImageProcessing::ImagePoint(1100, 100));
-        mask.push_back(Orthanc::ImageProcessing::ImagePoint(1100, 1000));
-        mask.push_back(Orthanc::ImageProcessing::ImagePoint(2000, 1000));
-        mask.push_back(Orthanc::ImageProcessing::ImagePoint(2200, 150));
-        mask.push_back(Orthanc::ImageProcessing::ImagePoint(1500, 550));
-        maskLayer_ = dynamic_cast<RadiographyMaskLayer*>(&(scene_->LoadMask(mask, dynamic_cast<RadiographyDicomLayer&>(dicomLayer), 128.0f, NULL)));
-        interactor_.SetMaskLayer(maskLayer_);
-
-        {
-          std::unique_ptr<Orthanc::ImageAccessor> renderedTextAlpha(TextRenderer::Render(Orthanc::EmbeddedResources::UBUNTU_FONT, 100,
-                                                                                    "%öÇaA&#"));
-          RadiographyLayer& layer = scene_->LoadAlphaBitmap(renderedTextAlpha.release(), NULL);
-          dynamic_cast<RadiographyAlphaLayer&>(layer).SetForegroundValue(200.0f * 256.0f);
-        }
-
-        {
-          RadiographyTextLayer::RegisterFont("ubuntu", Orthanc::EmbeddedResources::UBUNTU_FONT);
-          RadiographyLayer& layer = scene_->LoadText("Hello\nworld", "ubuntu", 20, 128, NULL, false);
-          layer.SetResizeable(true);
-        }
-        
-        {
-          RadiographyLayer& layer = scene_->LoadTestBlock(100, 50, NULL);
-          layer.SetResizeable(true);
-          layer.SetPan(0, 200);
-        }
-        
-        
-        mainWidget_ = new RadiographyWidget(GetBroker(), scene_, "main-widget");
-        mainWidget_->SetTransmitMouseOver(true);
-        mainWidget_->SetInteractor(interactor_);
-
-        //scene_->SetWindowing(128, 256);
-      }
-    };
-  }
-}
--- a/Applications/Samples/SingleVolumeApplication.h	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,277 +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 "SampleApplicationBase.h"
-#include "../../Framework/dev.h"
-#include "../../Framework/Layers/LineMeasureTracker.h"
-#include "../../Framework/Layers/CircleMeasureTracker.h"
-
-#include <Core/Toolbox.h>
-#include <Core/Logging.h>
-
-#include <Plugins/Samples/Common/OrthancHttpConnection.h>   // TODO REMOVE
-#include "../../Framework/Layers/DicomStructureSetSlicer.h"   // TODO REMOVE
-#include "../../Framework/Toolbox/MessagingToolbox.h"   // TODO REMOVE
-
-namespace OrthancStone
-{
-  namespace Samples
-  {
-    class SingleVolumeApplication : public SampleApplicationBase
-    {
-    private:
-      class Interactor : public VolumeImageInteractor
-      {
-      private:
-        SliceViewerWidget&  widget_;
-        size_t        layer_;
-        
-      protected:
-        virtual void NotifySliceContentChange(const ISlicedVolume& volume,
-                                       const size_t& sliceIndex,
-                                       const Slice& slice)
-        {
-          const OrthancVolumeImage& image = dynamic_cast<const OrthancVolumeImage&>(volume);
-
-          RenderStyle s = widget_.GetLayerStyle(layer_);
-
-          if (image.FitWindowingToRange(s, slice.GetConverter()))
-          {
-            //printf("Windowing: %f => %f\n", s.customWindowCenter_, s.customWindowWidth_);
-            widget_.SetLayerStyle(layer_, s);
-          }
-        }
-
-        virtual void MouseOver(CairoContext& context,
-                               WorldSceneWidget& widget,
-                               const ViewportGeometry& view,
-                               double x,
-                               double y,
-                               IStatusBar* statusBar)
-        {
-          const SliceViewerWidget& w = dynamic_cast<const SliceViewerWidget&>(widget);
-          Vector p = w.GetSlice().MapSliceToWorldCoordinates(x, y);
-          printf("%f %f %f\n", p[0], p[1], p[2]);
-        }
-      
-      public:
-        Interactor(OrthancVolumeImage& volume,
-                   SliceViewerWidget& widget,
-                   VolumeProjection projection,
-                   size_t layer) :
-          VolumeImageInteractor(volume, widget, projection),
-          widget_(widget),
-          layer_(layer)
-        {
-        }
-      };
-
-
-    public:
-      virtual void DeclareStartupOptions(boost::program_options::options_description& options)
-      {
-        boost::program_options::options_description generic("Sample options");
-        generic.add_options()
-          ("series", boost::program_options::value<std::string>(), 
-           "Orthanc ID of the series")
-          ("instance", boost::program_options::value<std::string>(), 
-           "Orthanc ID of a multi-frame instance that describes a 3D volume")
-          ("threads", boost::program_options::value<unsigned int>()->default_value(3), 
-           "Number of download threads")
-          ("projection", boost::program_options::value<std::string>()->default_value("axial"), 
-           "Projection of interest (can be axial, sagittal or coronal)")
-          ("reverse", boost::program_options::value<bool>()->default_value(false), 
-           "Reverse the normal direction of the volume")
-          ;
-
-        options.add(generic);    
-      }
-
-      virtual void Initialize(IStatusBar& statusBar,
-                              const boost::program_options::variables_map& parameters)
-      {
-        using namespace OrthancStone;
-
-        if (parameters.count("series") > 1 ||
-            parameters.count("instance") > 1)
-        {
-          LOG(ERROR) << "Only one series or instance is allowed";
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-        }
-
-        if (parameters.count("series") == 1 &&
-            parameters.count("instance") == 1)
-        {
-          LOG(ERROR) << "Cannot specify both a series and an instance";
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-        }
-
-        std::string series;
-        if (parameters.count("series") == 1)
-        {
-          series = parameters["series"].as<std::string>();
-        }
-        
-        std::string instance;
-        if (parameters.count("instance") == 1)
-        {
-          instance = parameters["instance"].as<std::string>();
-        }
-        
-        if (series.empty() &&
-            instance.empty())
-        {
-          LOG(ERROR) << "The series ID or instance ID is missing";
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-        }
-
-        //unsigned int threads = parameters["threads"].as<unsigned int>();
-        //bool reverse = parameters["reverse"].as<bool>();
-
-        std::string tmp = parameters["projection"].as<std::string>();
-        Orthanc::Toolbox::ToLowerCase(tmp);
-
-        VolumeProjection projection;
-        if (tmp == "axial")
-        {
-          projection = VolumeProjection_Axial;
-        }
-        else if (tmp == "sagittal")
-        {
-          projection = VolumeProjection_Sagittal;
-        }
-        else if (tmp == "coronal")
-        {
-          projection = VolumeProjection_Coronal;
-        }
-        else
-        {
-          LOG(ERROR) << "Unknown projection: " << tmp;
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-        }
-
-        std::unique_ptr<SliceViewerWidget> widget(new SliceViewerWidget);
-
-#if 1
-        std::unique_ptr<OrthancVolumeImage> volume(new OrthancVolumeImage(context.GetWebService(), true));
-        if (series.empty())
-        {
-          volume->ScheduleLoadInstance(instance);
-        }
-        else
-        {
-          volume->ScheduleLoadSeries(series);
-        }
-
-        widget->AddLayer(new VolumeImageMPRSlicer(*volume));
-
-        context_->AddInteractor(new Interactor(*volume, *widget, projection, 0));
-        context_->AddSlicedVolume(volume.release());
-
-        if (1)
-        {
-          RenderStyle s;
-          //s.drawGrid_ = true;
-          s.alpha_ = 1;
-          s.windowing_ = ImageWindowing_Bone;
-          widget->SetLayerStyle(0, s);
-        }
-        else
-        {
-          RenderStyle s;
-          s.alpha_ = 1;
-          s.applyLut_ = true;
-          s.lut_ = Orthanc::EmbeddedResources::COLORMAP_JET;
-          s.interpolation_ = ImageInterpolation_Bilinear;
-          widget->SetLayerStyle(0, s);
-        }
-#else
-        std::unique_ptr<OrthancVolumeImage> ct(new OrthancVolumeImage(context_->GetWebService(), false));
-        //ct->ScheduleLoadSeries("15a6f44a-ac7b88fe-19c462d9-dddd918e-b01550d8");  // 0178023P
-        //ct->ScheduleLoadSeries("dd069910-4f090474-7d2bba07-e5c10783-f9e4fb1d");
-        //ct->ScheduleLoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");  // IBA
-        //ct->ScheduleLoadSeries("03677739-1d8bca40-db1daf59-d74ff548-7f6fc9c0");  // 0522c0001 TCIA
-        ct->ScheduleLoadSeries("295e8a13-dfed1320-ba6aebb2-9a13e20f-1b3eb953");  // Captain
-        
-        std::unique_ptr<OrthancVolumeImage> pet(new OrthancVolumeImage(context_->GetWebService(), true));
-        //pet->ScheduleLoadSeries("48d2997f-8e25cd81-dd715b64-bd79cdcc-e8fcee53");  // 0178023P
-        //pet->ScheduleLoadSeries("aabad2e7-80702b5d-e599d26c-4f13398e-38d58a9e");
-        //pet->ScheduleLoadInstance("830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb"); // IBA 1
-        //pet->ScheduleLoadInstance("337876a1-a68a9718-f15abccd-38faafa1-b99b496a"); // IBA 2
-        //pet->ScheduleLoadInstance("830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb");  // IBA 3
-        //pet->ScheduleLoadInstance("269f26f4-0c83eeeb-2e67abbd-5467a40f-f1bec90c");  // 0522c0001 TCIA
-        pet->ScheduleLoadInstance("f080888c-0ab7528a-f7d9c28c-84980eb1-ff3b0ae6");  // Captain 1
-        //pet->ScheduleLoadInstance("4f78055b-6499a2c5-1e089290-394acc05-3ec781c1");  // Captain 2
-
-        std::unique_ptr<StructureSetLoader> rtStruct(new StructureSetLoader(context_->GetWebService()));
-        //rtStruct->ScheduleLoadInstance("c2ebc17b-6b3548db-5e5da170-b8ecab71-ea03add3");  // 0178023P
-        //rtStruct->ScheduleLoadInstance("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9");  // IBA
-        //rtStruct->ScheduleLoadInstance("17cd032b-ad92a438-ca05f06a-f9e96668-7e3e9e20");  // 0522c0001 TCIA
-        rtStruct->ScheduleLoadInstance("96c889ab-29fe5c54-dda6e66c-3949e4da-58f90d75");  // Captain
-        
-        widget->AddLayer(new VolumeImageMPRSlicer(*ct));
-        widget->AddLayer(new VolumeImageMPRSlicer(*pet));
-        widget->AddLayer(new DicomStructureSetSlicer(*rtStruct));
-        
-        context_->AddInteractor(new Interactor(*pet, *widget, projection, 1));
-        //context_->AddInteractor(new VolumeImageInteractor(*ct, *widget, projection));
-
-        context_->AddSlicedVolume(ct.release());
-        context_->AddSlicedVolume(pet.release());
-        context_->AddVolumeLoader(rtStruct.release());
-
-        {
-          RenderStyle s;
-          //s.drawGrid_ = true;
-          s.alpha_ = 1;
-          s.windowing_ = ImageWindowing_Bone;
-          widget->SetLayerStyle(0, s);
-        }
-
-        {
-          RenderStyle s;
-          //s.drawGrid_ = true;
-          s.SetColor(255, 0, 0);  // Draw missing PET layer in red
-          s.alpha_ = 0.5;
-          s.applyLut_ = true;
-          s.lut_ = Orthanc::EmbeddedResources::COLORMAP_JET;
-          s.interpolation_ = ImageInterpolation_Bilinear;
-          s.windowing_ = ImageWindowing_Custom;
-          s.customWindowCenter_ = 0;
-          s.customWindowWidth_ = 128;
-          widget->SetLayerStyle(1, s);
-        }
-#endif
-
-
-        statusBar.SetMessage("Use the keys \"b\", \"l\" and \"d\" to change Hounsfield windowing");
-        statusBar.SetMessage("Use the keys \"t\" to track the (X,Y,Z) mouse coordinates");
-        statusBar.SetMessage("Use the keys \"m\" to measure distances");
-        statusBar.SetMessage("Use the keys \"c\" to draw circles");
-
-        widget->SetTransmitMouseOver(true);
-        context_->SetCentralWidget(widget.release());
-      }
-    };
-  }
-}
--- a/Applications/Samples/StoneSampleCommands.yml	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,35 +0,0 @@
-#
-#        1         2         3         4         5         6         7         8
-# 345678901234567890123456789012345678901234567890123456789012345678901234567890
-#
-rootName: StoneSampleCommands
-
-# +---------------------------------+
-# | Messages from TypeScript to C++ |
-# +---------------------------------+
-
-enum Tool:
-  - LineMeasure
-  - CircleMeasure
-  - Crop
-  - Windowing
-  - Zoom
-  - Pan
-  - Move
-  - Rotate
-  - Resize
-  - Mask
-
-struct SelectTool:
-  __handler: cpp
-  tool: Tool
-
-enum ActionType:
-  - UndoCrop
-  - Rotate
-  - Invert
-
-struct Action:
-  __handler: cpp
-  type: ActionType
-
--- a/Applications/Samples/StoneSampleCommands_generate.py	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,16 +0,0 @@
-import sys
-import os
-
-# add the generation script location to the search paths
-sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'Resources', 'CodeGeneration'))
-
-# import the code generation tooling script
-import stonegentool
-
-schemaFile = os.path.join(os.path.dirname(__file__), 'StoneSampleCommands.yml')
-outDir = os.path.dirname(__file__)
-
-# ignition!
-stonegentool.Process(schemaFile, outDir)
-
-
--- a/Applications/Samples/StoneSampleCommands_generated.hpp	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,703 +0,0 @@
-/*
-         1         2         3         4         5         6         7
-12345678901234567890123456789012345678901234567890123456789012345678901234567890
-
-Generated on 2019-03-18 12:07:42.696093 by stonegentool
-
-*/
-#pragma once
-
-#include <exception>
-#include <iostream>
-#include <string>
-#include <sstream>
-#include <assert.h>
-#include <memory>
-#include <json/json.h>
-
-//#define STONEGEN_NO_CPP11 1
-
-#ifdef STONEGEN_NO_CPP11
-#define StoneSmartPtr std::unique_ptr
-#else 
-#define StoneSmartPtr std::unique_ptr
-#endif 
-
-namespace StoneSampleCommands
-{
-  /** Throws in case of problem */
-  inline void _StoneDeserializeValue(int32_t& destValue, const Json::Value& jsonValue)
-  {
-    destValue = jsonValue.asInt();
-  }
-
-  inline Json::Value _StoneSerializeValue(int32_t value)
-  {
-    Json::Value result(value);
-    return result;
-  }
-
-  inline void _StoneDeserializeValue(Json::Value& destValue, const Json::Value& jsonValue)
-  {
-    destValue = jsonValue;
-  }
-
-  inline Json::Value _StoneSerializeValue(Json::Value value)
-  {
-    return value;
-  }
-
-  /** Throws in case of problem */
-  inline void _StoneDeserializeValue(double& destValue, const Json::Value& jsonValue)
-  {
-    destValue = jsonValue.asDouble();
-  }
-
-  inline Json::Value _StoneSerializeValue(double value)
-  {
-    Json::Value result(value);
-    return result;
-  }
-
-  /** Throws in case of problem */
-  inline void _StoneDeserializeValue(bool& destValue, const Json::Value& jsonValue)
-  {
-    destValue = jsonValue.asBool();
-  }
-
-  inline Json::Value _StoneSerializeValue(bool value)
-  {
-    Json::Value result(value);
-    return result;
-  }
-
-  /** Throws in case of problem */
-  inline void _StoneDeserializeValue(
-       std::string& destValue
-     , const Json::Value& jsonValue)
-  {
-    destValue = jsonValue.asString();
-  }
-
-  inline Json::Value _StoneSerializeValue(const std::string& value)
-  {
-    // the following is better than 
-    Json::Value result(value.data(),value.data()+value.size());
-    return result;
-  }
-
-  inline std::string MakeIndent(size_t indent)
-  {
-    char* txt = reinterpret_cast<char*>(malloc(indent+1)); // NO EXCEPTION BELOW!!!!!!!!!!!!
-    for(size_t i = 0; i < indent; ++i)
-      txt[i] = ' ';
-    txt[indent] = 0;
-    std::string retVal(txt);
-    free(txt); // NO EXCEPTION ABOVE !!!!!!!!!!
-    return retVal;
-  }
-
-  // generic dumper
-  template<typename T>
-  std::ostream& StoneDumpValue(std::ostream& out, const T& value, size_t indent)
-  {
-    out << MakeIndent(indent) << value;
-    return out;
-  }
-
-  // string dumper
-  inline std::ostream& StoneDumpValue(std::ostream& out, const std::string& value, size_t indent)
-  {
-    out << MakeIndent(indent) << "\"" << value  << "\"";
-    return out;
-  }
-
-  /** Throws in case of problem */
-  template<typename T>
-  void _StoneDeserializeValue(
-    std::map<std::string, T>& destValue, const Json::Value& jsonValue)
-  {
-    destValue.clear();
-    for (
-      Json::Value::const_iterator itr = jsonValue.begin();
-      itr != jsonValue.end();
-      itr++)
-    {
-      std::string key;
-      _StoneDeserializeValue(key, itr.key());
-
-      T innerDestValue;
-      _StoneDeserializeValue(innerDestValue, *itr);
-
-      destValue[key] = innerDestValue;
-    }
-  }
-
-  template<typename T>
-  Json::Value _StoneSerializeValue(const std::map<std::string,T>& value)
-  {
-    Json::Value result(Json::objectValue);
-
-    for (typename std::map<std::string, T>::const_iterator it = value.cbegin();
-      it != value.cend(); ++it)
-    {
-      // it->first it->second
-      result[it->first] = _StoneSerializeValue(it->second);
-    }
-    return result;
-  }
-
-  template<typename T>
-  std::ostream& StoneDumpValue(std::ostream& out, const std::map<std::string,T>& value, size_t indent)
-  {
-    out << MakeIndent(indent) << "{\n";
-    for (typename std::map<std::string, T>::const_iterator it = value.cbegin();
-      it != value.cend(); ++it)
-    {
-      out << MakeIndent(indent+2) << "\"" << it->first << "\" : ";
-      StoneDumpValue(out, it->second, indent+2);
-    }
-    out << MakeIndent(indent) << "}\n";
-    return out;
-  }
-
-  /** Throws in case of problem */
-  template<typename T>
-  void _StoneDeserializeValue(
-    std::vector<T>& destValue, const Json::Value& jsonValue)
-  {
-    destValue.clear();
-    destValue.reserve(jsonValue.size());
-    for (Json::Value::ArrayIndex i = 0; i != jsonValue.size(); i++)
-    {
-      T innerDestValue;
-      _StoneDeserializeValue(innerDestValue, jsonValue[i]);
-      destValue.push_back(innerDestValue);
-    }
-  }
-
-  template<typename T>
-  Json::Value _StoneSerializeValue(const std::vector<T>& value)
-  {
-    Json::Value result(Json::arrayValue);
-    for (size_t i = 0; i < value.size(); ++i)
-    {
-      result.append(_StoneSerializeValue(value[i]));
-    }
-    return result;
-  }
-
-  template<typename T>
-  std::ostream& StoneDumpValue(std::ostream& out, const std::vector<T>& value, size_t indent)
-  {
-    out << MakeIndent(indent) << "[\n";
-    for (size_t i = 0; i < value.size(); ++i)
-    {
-      StoneDumpValue(out, value[i], indent+2);
-    }
-    out << MakeIndent(indent) << "]\n";
-    return out;
-  }
-
-  inline void StoneCheckSerializedValueTypeGeneric(const Json::Value& value)
-  {
-    if ((!value.isMember("type")) || (!value["type"].isString()))
-    {
-      std::stringstream ss;
-      ss << "Cannot deserialize value ('type' key invalid)";
-      throw std::runtime_error(ss.str());
-    }
-  }
-
-  inline void StoneCheckSerializedValueType(
-    const Json::Value& value, std::string typeStr)
-  {
-    StoneCheckSerializedValueTypeGeneric(value);
-
-    std::string actTypeStr = value["type"].asString();
-    if (actTypeStr != typeStr)
-    {
-      std::stringstream ss;
-      ss << "Cannot deserialize type" << actTypeStr
-        << "into " << typeStr;
-      throw std::runtime_error(ss.str());
-    }
-  }
-
-  // end of generic methods
-
-// end of generic methods
-
-  enum Tool {
-    Tool_LineMeasure,
-    Tool_CircleMeasure,
-    Tool_Crop,
-    Tool_Windowing,
-    Tool_Zoom,
-    Tool_Pan,
-    Tool_Move,
-    Tool_Rotate,
-    Tool_Resize,
-    Tool_Mask,
-  };
-
-  inline std::string ToString(const Tool& value)
-  {
-    if( value == Tool_LineMeasure)
-    {
-      return std::string("LineMeasure");
-    }
-    if( value == Tool_CircleMeasure)
-    {
-      return std::string("CircleMeasure");
-    }
-    if( value == Tool_Crop)
-    {
-      return std::string("Crop");
-    }
-    if( value == Tool_Windowing)
-    {
-      return std::string("Windowing");
-    }
-    if( value == Tool_Zoom)
-    {
-      return std::string("Zoom");
-    }
-    if( value == Tool_Pan)
-    {
-      return std::string("Pan");
-    }
-    if( value == Tool_Move)
-    {
-      return std::string("Move");
-    }
-    if( value == Tool_Rotate)
-    {
-      return std::string("Rotate");
-    }
-    if( value == Tool_Resize)
-    {
-      return std::string("Resize");
-    }
-    if( value == Tool_Mask)
-    {
-      return std::string("Mask");
-    }
-    std::stringstream ss;
-    ss << "Value \"" << value << "\" cannot be converted to Tool. Possible values are: "
-        << " LineMeasure = " << static_cast<int64_t>(Tool_LineMeasure)  << ", " 
-        << " CircleMeasure = " << static_cast<int64_t>(Tool_CircleMeasure)  << ", " 
-        << " Crop = " << static_cast<int64_t>(Tool_Crop)  << ", " 
-        << " Windowing = " << static_cast<int64_t>(Tool_Windowing)  << ", " 
-        << " Zoom = " << static_cast<int64_t>(Tool_Zoom)  << ", " 
-        << " Pan = " << static_cast<int64_t>(Tool_Pan)  << ", " 
-        << " Move = " << static_cast<int64_t>(Tool_Move)  << ", " 
-        << " Rotate = " << static_cast<int64_t>(Tool_Rotate)  << ", " 
-        << " Resize = " << static_cast<int64_t>(Tool_Resize)  << ", " 
-        << " Mask = " << static_cast<int64_t>(Tool_Mask)  << ", " 
-        << std::endl;
-    std::string msg = ss.str();
-    throw std::runtime_error(msg);
-  }
-
-  inline void FromString(Tool& value, std::string strValue)
-  {
-    if( strValue == std::string("LineMeasure") )
-    {
-      value = Tool_LineMeasure;
-      return;
-    }
-    if( strValue == std::string("CircleMeasure") )
-    {
-      value = Tool_CircleMeasure;
-      return;
-    }
-    if( strValue == std::string("Crop") )
-    {
-      value = Tool_Crop;
-      return;
-    }
-    if( strValue == std::string("Windowing") )
-    {
-      value = Tool_Windowing;
-      return;
-    }
-    if( strValue == std::string("Zoom") )
-    {
-      value = Tool_Zoom;
-      return;
-    }
-    if( strValue == std::string("Pan") )
-    {
-      value = Tool_Pan;
-      return;
-    }
-    if( strValue == std::string("Move") )
-    {
-      value = Tool_Move;
-      return;
-    }
-    if( strValue == std::string("Rotate") )
-    {
-      value = Tool_Rotate;
-      return;
-    }
-    if( strValue == std::string("Resize") )
-    {
-      value = Tool_Resize;
-      return;
-    }
-    if( strValue == std::string("Mask") )
-    {
-      value = Tool_Mask;
-      return;
-    }
-
-    std::stringstream ss;
-    ss << "String \"" << strValue << "\" cannot be converted to Tool. Possible values are: LineMeasure CircleMeasure Crop Windowing Zoom Pan Move Rotate Resize Mask ";
-    std::string msg = ss.str();
-    throw std::runtime_error(msg);
-  }
-
-
-  inline void _StoneDeserializeValue(
-    Tool& destValue, const Json::Value& jsonValue)
-  {
-    FromString(destValue, jsonValue.asString());
-  }
-
-  inline Json::Value _StoneSerializeValue(const Tool& value)
-  {
-    std::string strValue = ToString(value);
-    return Json::Value(strValue);
-  }
-
-  inline std::ostream& StoneDumpValue(std::ostream& out, const Tool& value, size_t indent = 0)
-  {
-    if( value == Tool_LineMeasure)
-    {
-      out << MakeIndent(indent) << "LineMeasure" << std::endl;
-    }
-    if( value == Tool_CircleMeasure)
-    {
-      out << MakeIndent(indent) << "CircleMeasure" << std::endl;
-    }
-    if( value == Tool_Crop)
-    {
-      out << MakeIndent(indent) << "Crop" << std::endl;
-    }
-    if( value == Tool_Windowing)
-    {
-      out << MakeIndent(indent) << "Windowing" << std::endl;
-    }
-    if( value == Tool_Zoom)
-    {
-      out << MakeIndent(indent) << "Zoom" << std::endl;
-    }
-    if( value == Tool_Pan)
-    {
-      out << MakeIndent(indent) << "Pan" << std::endl;
-    }
-    if( value == Tool_Move)
-    {
-      out << MakeIndent(indent) << "Move" << std::endl;
-    }
-    if( value == Tool_Rotate)
-    {
-      out << MakeIndent(indent) << "Rotate" << std::endl;
-    }
-    if( value == Tool_Resize)
-    {
-      out << MakeIndent(indent) << "Resize" << std::endl;
-    }
-    if( value == Tool_Mask)
-    {
-      out << MakeIndent(indent) << "Mask" << std::endl;
-    }
-    return out;
-  }
-
-
-  enum ActionType {
-    ActionType_UndoCrop,
-    ActionType_Rotate,
-    ActionType_Invert,
-  };
-
-  inline std::string ToString(const ActionType& value)
-  {
-    if( value == ActionType_UndoCrop)
-    {
-      return std::string("UndoCrop");
-    }
-    if( value == ActionType_Rotate)
-    {
-      return std::string("Rotate");
-    }
-    if( value == ActionType_Invert)
-    {
-      return std::string("Invert");
-    }
-    std::stringstream ss;
-    ss << "Value \"" << value << "\" cannot be converted to ActionType. Possible values are: "
-        << " UndoCrop = " << static_cast<int64_t>(ActionType_UndoCrop)  << ", " 
-        << " Rotate = " << static_cast<int64_t>(ActionType_Rotate)  << ", " 
-        << " Invert = " << static_cast<int64_t>(ActionType_Invert)  << ", " 
-        << std::endl;
-    std::string msg = ss.str();
-    throw std::runtime_error(msg);
-  }
-
-  inline void FromString(ActionType& value, std::string strValue)
-  {
-    if( strValue == std::string("UndoCrop") )
-    {
-      value = ActionType_UndoCrop;
-      return;
-    }
-    if( strValue == std::string("Rotate") )
-    {
-      value = ActionType_Rotate;
-      return;
-    }
-    if( strValue == std::string("Invert") )
-    {
-      value = ActionType_Invert;
-      return;
-    }
-
-    std::stringstream ss;
-    ss << "String \"" << strValue << "\" cannot be converted to ActionType. Possible values are: UndoCrop Rotate Invert ";
-    std::string msg = ss.str();
-    throw std::runtime_error(msg);
-  }
-
-
-  inline void _StoneDeserializeValue(
-    ActionType& destValue, const Json::Value& jsonValue)
-  {
-    FromString(destValue, jsonValue.asString());
-  }
-
-  inline Json::Value _StoneSerializeValue(const ActionType& value)
-  {
-    std::string strValue = ToString(value);
-    return Json::Value(strValue);
-  }
-
-  inline std::ostream& StoneDumpValue(std::ostream& out, const ActionType& value, size_t indent = 0)
-  {
-    if( value == ActionType_UndoCrop)
-    {
-      out << MakeIndent(indent) << "UndoCrop" << std::endl;
-    }
-    if( value == ActionType_Rotate)
-    {
-      out << MakeIndent(indent) << "Rotate" << std::endl;
-    }
-    if( value == ActionType_Invert)
-    {
-      out << MakeIndent(indent) << "Invert" << std::endl;
-    }
-    return out;
-  }
-
-
-
-#ifdef _MSC_VER
-#pragma region SelectTool
-#endif //_MSC_VER
-
-  struct SelectTool
-  {
-    Tool tool;
-
-    SelectTool(Tool tool = Tool())
-    {
-      this->tool = tool;
-    }
-  };
-
-  inline void _StoneDeserializeValue(SelectTool& destValue, const Json::Value& value)
-  {
-    _StoneDeserializeValue(destValue.tool, value["tool"]);
-    }
-
-  inline Json::Value _StoneSerializeValue(const SelectTool& value)
-  {
-    Json::Value result(Json::objectValue);
-    result["tool"] = _StoneSerializeValue(value.tool);
-
-    return result;
-  }
-
-  inline std::ostream& StoneDumpValue(std::ostream& out, const SelectTool& value, size_t indent = 0)
-  {
-    out << MakeIndent(indent) << "{\n";
-    out << MakeIndent(indent) << "tool:\n";
-    StoneDumpValue(out, value.tool,indent+2);
-    out << "\n";
-
-    out << MakeIndent(indent) << "}\n";
-    return out;
-  }
-
-  inline void StoneDeserialize(SelectTool& destValue, const Json::Value& value)
-  {
-    StoneCheckSerializedValueType(value, "StoneSampleCommands.SelectTool");
-    _StoneDeserializeValue(destValue, value["value"]);
-  }
-
-  inline Json::Value StoneSerializeToJson(const SelectTool& value)
-  {
-    Json::Value result(Json::objectValue);
-    result["type"] = "StoneSampleCommands.SelectTool";
-    result["value"] = _StoneSerializeValue(value);
-    return result;
-  }
-
-  inline std::string StoneSerialize(const SelectTool& value)
-  {
-    Json::Value resultJson = StoneSerializeToJson(value);
-    std::string resultStr = resultJson.toStyledString();
-    return resultStr;
-  }
-
-#ifdef _MSC_VER
-#pragma endregion SelectTool
-#endif //_MSC_VER
-
-#ifdef _MSC_VER
-#pragma region Action
-#endif //_MSC_VER
-
-  struct Action
-  {
-    ActionType type;
-
-    Action(ActionType type = ActionType())
-    {
-      this->type = type;
-    }
-  };
-
-  inline void _StoneDeserializeValue(Action& destValue, const Json::Value& value)
-  {
-    _StoneDeserializeValue(destValue.type, value["type"]);
-    }
-
-  inline Json::Value _StoneSerializeValue(const Action& value)
-  {
-    Json::Value result(Json::objectValue);
-    result["type"] = _StoneSerializeValue(value.type);
-
-    return result;
-  }
-
-  inline std::ostream& StoneDumpValue(std::ostream& out, const Action& value, size_t indent = 0)
-  {
-    out << MakeIndent(indent) << "{\n";
-    out << MakeIndent(indent) << "type:\n";
-    StoneDumpValue(out, value.type,indent+2);
-    out << "\n";
-
-    out << MakeIndent(indent) << "}\n";
-    return out;
-  }
-
-  inline void StoneDeserialize(Action& destValue, const Json::Value& value)
-  {
-    StoneCheckSerializedValueType(value, "StoneSampleCommands.Action");
-    _StoneDeserializeValue(destValue, value["value"]);
-  }
-
-  inline Json::Value StoneSerializeToJson(const Action& value)
-  {
-    Json::Value result(Json::objectValue);
-    result["type"] = "StoneSampleCommands.Action";
-    result["value"] = _StoneSerializeValue(value);
-    return result;
-  }
-
-  inline std::string StoneSerialize(const Action& value)
-  {
-    Json::Value resultJson = StoneSerializeToJson(value);
-    std::string resultStr = resultJson.toStyledString();
-    return resultStr;
-  }
-
-#ifdef _MSC_VER
-#pragma endregion Action
-#endif //_MSC_VER
-
-#ifdef _MSC_VER
-#pragma region Dispatching code
-#endif //_MSC_VER
-
-  class IHandler
-  {
-  public:
-    virtual bool Handle(const SelectTool& value) = 0;
-    virtual bool Handle(const Action& value) = 0;
-  };
-
-  /** Service function for StoneDispatchToHandler */
-  inline bool StoneDispatchJsonToHandler(
-    const Json::Value& jsonValue, IHandler* handler)
-  {
-    StoneCheckSerializedValueTypeGeneric(jsonValue);
-    std::string type = jsonValue["type"].asString();
-    if (type == "")
-    {
-      // this should never ever happen
-      throw std::runtime_error("Caught empty type while dispatching");
-    }
-    else if (type == "StoneSampleCommands.SelectTool")
-    {
-      SelectTool value;
-      _StoneDeserializeValue(value, jsonValue["value"]);
-      return handler->Handle(value);
-    }
-    else if (type == "StoneSampleCommands.Action")
-    {
-      Action value;
-      _StoneDeserializeValue(value, jsonValue["value"]);
-      return handler->Handle(value);
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-  /** Takes a serialized type and passes this to the handler */
-  inline bool StoneDispatchToHandler(std::string strValue, IHandler* handler)
-  {
-    Json::Value readValue;
-
-    Json::CharReaderBuilder builder;
-    Json::CharReader* reader = builder.newCharReader();
-
-    StoneSmartPtr<Json::CharReader> ptr(reader);
-
-    std::string errors;
-
-    bool ok = reader->parse(
-      strValue.c_str(),
-      strValue.c_str() + strValue.size(),
-      &readValue,
-      &errors
-    );
-    if (!ok)
-    {
-      std::stringstream ss;
-      ss << "Jsoncpp parsing error: " << errors;
-      throw std::runtime_error(ss.str());
-    }
-    return StoneDispatchJsonToHandler(readValue, handler);
-  }
-
-#ifdef _MSC_VER
-#pragma endregion Dispatching code
-#endif //_MSC_VER
-}
\ No newline at end of file
--- a/Applications/Samples/StoneSampleCommands_generated.ts	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,333 +0,0 @@
-/*
-         1         2         3         4         5         6         7
-12345678901234567890123456789012345678901234567890123456789012345678901234567890
-
-Generated on 2019-03-18 12:07:42.696093 by stonegentool
-
-*/
-
-function StoneCheckSerializedValueType(value: any, typeStr: string)
-{
-  StoneCheckSerializedValueTypeGeneric(value);
-
-  if (value['type'] != typeStr)
-  {
-    throw new Error(
-      `Cannot deserialize type ${value['type']} into ${typeStr}`);
-  }
-}
-
-function isString(val: any) :boolean
-{
-  return ((typeof val === 'string') || (val instanceof String));
-}
-
-function StoneCheckSerializedValueTypeGeneric(value: any)
-{
-  // console.//log("+-------------------------------------------------+");
-  // console.//log("|            StoneCheckSerializedValueTypeGeneric |");
-  // console.//log("+-------------------------------------------------+");
-  // console.//log("value = ");
-  // console.//log(value);
-  if ( (!('type' in value)) || (!isString(value.type)) )
-  {
-    throw new Error(
-      "Cannot deserialize value ('type' key invalid)");
-  }
-}
-
-// end of generic methods
-
-export enum Tool {
-  LineMeasure = "LineMeasure",
-  CircleMeasure = "CircleMeasure",
-  Crop = "Crop",
-  Windowing = "Windowing",
-  Zoom = "Zoom",
-  Pan = "Pan",
-  Move = "Move",
-  Rotate = "Rotate",
-  Resize = "Resize",
-  Mask = "Mask"
-};
-
-export function Tool_FromString(strValue:string) : Tool
-{
-  if( strValue == "LineMeasure" )
-  {
-    return Tool.LineMeasure;
-  }
-  if( strValue == "CircleMeasure" )
-  {
-    return Tool.CircleMeasure;
-  }
-  if( strValue == "Crop" )
-  {
-    return Tool.Crop;
-  }
-  if( strValue == "Windowing" )
-  {
-    return Tool.Windowing;
-  }
-  if( strValue == "Zoom" )
-  {
-    return Tool.Zoom;
-  }
-  if( strValue == "Pan" )
-  {
-    return Tool.Pan;
-  }
-  if( strValue == "Move" )
-  {
-    return Tool.Move;
-  }
-  if( strValue == "Rotate" )
-  {
-    return Tool.Rotate;
-  }
-  if( strValue == "Resize" )
-  {
-    return Tool.Resize;
-  }
-  if( strValue == "Mask" )
-  {
-    return Tool.Mask;
-  }
-
-  let msg : string =  `String ${strValue} cannot be converted to Tool. Possible values are: LineMeasure, CircleMeasure, Crop, Windowing, Zoom, Pan, Move, Rotate, Resize, Mask`;
-  throw new Error(msg);
-}
-
-export function Tool_ToString(value:Tool) : string
-{
-  if( value == Tool.LineMeasure )
-  {
-    return "LineMeasure";
-  }
-  if( value == Tool.CircleMeasure )
-  {
-    return "CircleMeasure";
-  }
-  if( value == Tool.Crop )
-  {
-    return "Crop";
-  }
-  if( value == Tool.Windowing )
-  {
-    return "Windowing";
-  }
-  if( value == Tool.Zoom )
-  {
-    return "Zoom";
-  }
-  if( value == Tool.Pan )
-  {
-    return "Pan";
-  }
-  if( value == Tool.Move )
-  {
-    return "Move";
-  }
-  if( value == Tool.Rotate )
-  {
-    return "Rotate";
-  }
-  if( value == Tool.Resize )
-  {
-    return "Resize";
-  }
-  if( value == Tool.Mask )
-  {
-    return "Mask";
-  }
-
-  let msg : string = `Value ${value} cannot be converted to Tool. Possible values are: `;
-  {
-    let _LineMeasure_enumValue : string = Tool.LineMeasure; // enums are strings in stonecodegen, so this will work.
-    let msg_LineMeasure : string = `LineMeasure (${_LineMeasure_enumValue}), `;
-    msg = msg + msg_LineMeasure;
-  }
-  {
-    let _CircleMeasure_enumValue : string = Tool.CircleMeasure; // enums are strings in stonecodegen, so this will work.
-    let msg_CircleMeasure : string = `CircleMeasure (${_CircleMeasure_enumValue}), `;
-    msg = msg + msg_CircleMeasure;
-  }
-  {
-    let _Crop_enumValue : string = Tool.Crop; // enums are strings in stonecodegen, so this will work.
-    let msg_Crop : string = `Crop (${_Crop_enumValue}), `;
-    msg = msg + msg_Crop;
-  }
-  {
-    let _Windowing_enumValue : string = Tool.Windowing; // enums are strings in stonecodegen, so this will work.
-    let msg_Windowing : string = `Windowing (${_Windowing_enumValue}), `;
-    msg = msg + msg_Windowing;
-  }
-  {
-    let _Zoom_enumValue : string = Tool.Zoom; // enums are strings in stonecodegen, so this will work.
-    let msg_Zoom : string = `Zoom (${_Zoom_enumValue}), `;
-    msg = msg + msg_Zoom;
-  }
-  {
-    let _Pan_enumValue : string = Tool.Pan; // enums are strings in stonecodegen, so this will work.
-    let msg_Pan : string = `Pan (${_Pan_enumValue}), `;
-    msg = msg + msg_Pan;
-  }
-  {
-    let _Move_enumValue : string = Tool.Move; // enums are strings in stonecodegen, so this will work.
-    let msg_Move : string = `Move (${_Move_enumValue}), `;
-    msg = msg + msg_Move;
-  }
-  {
-    let _Rotate_enumValue : string = Tool.Rotate; // enums are strings in stonecodegen, so this will work.
-    let msg_Rotate : string = `Rotate (${_Rotate_enumValue}), `;
-    msg = msg + msg_Rotate;
-  }
-  {
-    let _Resize_enumValue : string = Tool.Resize; // enums are strings in stonecodegen, so this will work.
-    let msg_Resize : string = `Resize (${_Resize_enumValue}), `;
-    msg = msg + msg_Resize;
-  }
-  {
-    let _Mask_enumValue : string = Tool.Mask; // enums are strings in stonecodegen, so this will work.
-    let msg_Mask : string = `Mask (${_Mask_enumValue})`;
-    msg = msg + msg_Mask;
-  }
-  throw new Error(msg);
-}
-
-export enum ActionType {
-  UndoCrop = "UndoCrop",
-  Rotate = "Rotate",
-  Invert = "Invert"
-};
-
-export function ActionType_FromString(strValue:string) : ActionType
-{
-  if( strValue == "UndoCrop" )
-  {
-    return ActionType.UndoCrop;
-  }
-  if( strValue == "Rotate" )
-  {
-    return ActionType.Rotate;
-  }
-  if( strValue == "Invert" )
-  {
-    return ActionType.Invert;
-  }
-
-  let msg : string =  `String ${strValue} cannot be converted to ActionType. Possible values are: UndoCrop, Rotate, Invert`;
-  throw new Error(msg);
-}
-
-export function ActionType_ToString(value:ActionType) : string
-{
-  if( value == ActionType.UndoCrop )
-  {
-    return "UndoCrop";
-  }
-  if( value == ActionType.Rotate )
-  {
-    return "Rotate";
-  }
-  if( value == ActionType.Invert )
-  {
-    return "Invert";
-  }
-
-  let msg : string = `Value ${value} cannot be converted to ActionType. Possible values are: `;
-  {
-    let _UndoCrop_enumValue : string = ActionType.UndoCrop; // enums are strings in stonecodegen, so this will work.
-    let msg_UndoCrop : string = `UndoCrop (${_UndoCrop_enumValue}), `;
-    msg = msg + msg_UndoCrop;
-  }
-  {
-    let _Rotate_enumValue : string = ActionType.Rotate; // enums are strings in stonecodegen, so this will work.
-    let msg_Rotate : string = `Rotate (${_Rotate_enumValue}), `;
-    msg = msg + msg_Rotate;
-  }
-  {
-    let _Invert_enumValue : string = ActionType.Invert; // enums are strings in stonecodegen, so this will work.
-    let msg_Invert : string = `Invert (${_Invert_enumValue})`;
-    msg = msg + msg_Invert;
-  }
-  throw new Error(msg);
-}
-
-
-
-export class SelectTool {
-  tool:Tool;
-
-  constructor() {
-  }
-
-  public StoneSerialize(): string {
-    let container: object = {};
-    container['type'] = 'StoneSampleCommands.SelectTool';
-    container['value'] = this;
-    return JSON.stringify(container);
-  }
-
-  public static StoneDeserialize(valueStr: string) : SelectTool
-  {
-    let value: any = JSON.parse(valueStr);
-    StoneCheckSerializedValueType(value, 'StoneSampleCommands.SelectTool');
-    let result: SelectTool = value['value'] as SelectTool;
-    return result;
-  }
-}
-export class Action {
-  type:ActionType;
-
-  constructor() {
-  }
-
-  public StoneSerialize(): string {
-    let container: object = {};
-    container['type'] = 'StoneSampleCommands.Action';
-    container['value'] = this;
-    return JSON.stringify(container);
-  }
-
-  public static StoneDeserialize(valueStr: string) : Action
-  {
-    let value: any = JSON.parse(valueStr);
-    StoneCheckSerializedValueType(value, 'StoneSampleCommands.Action');
-    let result: Action = value['value'] as Action;
-    return result;
-  }
-}
-
-export interface IHandler {
-};
-
-/** Service function for StoneDispatchToHandler */
-export function StoneDispatchJsonToHandler(
-  jsonValue: any, handler: IHandler): boolean
-{
-  StoneCheckSerializedValueTypeGeneric(jsonValue);
-  let type: string = jsonValue["type"];
-  if (type == "")
-  {
-    // this should never ever happen
-    throw new Error("Caught empty type while dispatching");
-  }
-  else
-  {
-    return false;
-  }
-}
-
-/** Takes a serialized type and passes this to the handler */
-export function StoneDispatchToHandler(
-  strValue: string, handler: IHandler): boolean
-{
-  // console.//log("+------------------------------------------------+");
-  // console.//log("|            StoneDispatchToHandler              |");
-  // console.//log("+------------------------------------------------+");
-  // console.//log("strValue = ");
-  // console.//log(strValue);
-  let jsonValue: any = JSON.parse(strValue)
-  return StoneDispatchJsonToHandler(jsonValue, handler);
-}
\ No newline at end of file
--- a/Applications/Samples/SynchronizedSeriesApplication.h	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,109 +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 "SampleInteractor.h"
-
-#include "../../Framework/Toolbox/OrthancSeriesLoader.h"
-#include "../../Framework/Layers/SeriesFrameRendererFactory.h"
-#include "../../Framework/Layers/ReferenceLineFactory.h"
-#include "../../Framework/Widgets/LayoutWidget.h"
-
-#include <Core/Logging.h>
-
-namespace OrthancStone
-{
-  namespace Samples
-  {
-    class SynchronizedSeriesApplication : public SampleApplicationBase
-    {
-    private:   
-      LayeredSceneWidget* CreateSeriesWidget(BasicApplicationContext& context,
-                                             const std::string& series)
-      {
-        std::unique_ptr<ISeriesLoader> loader
-          (new OrthancSeriesLoader(context.GetWebService().GetConnection(), series));
-
-        std::unique_ptr<SampleInteractor> interactor(new SampleInteractor(*loader, false));
-
-        std::unique_ptr<LayeredSceneWidget> widget(new LayeredSceneWidget);
-        widget->AddLayer(new SeriesFrameRendererFactory(loader.release(), false));
-        widget->SetSlice(interactor->GetCursor().GetCurrentSlice());
-        widget->SetInteractor(*interactor);
-
-        context.AddInteractor(interactor.release());
-
-        return widget.release();
-      }
-
-    public:
-      virtual void DeclareCommandLineOptions(boost::program_options::options_description& options)
-      {
-        boost::program_options::options_description generic("Sample options");
-        generic.add_options()
-          ("a", boost::program_options::value<std::string>(), 
-           "Orthanc ID of the 1st series")
-          ("b", boost::program_options::value<std::string>(), 
-           "Orthanc ID of the 2nd series")
-          ("c", boost::program_options::value<std::string>(), 
-           "Orthanc ID of the 3rd series")
-          ;
-
-        options.add(generic);    
-      }
-
-      virtual void Initialize(BasicApplicationContext& context,
-                              IStatusBar& statusBar,
-                              const boost::program_options::variables_map& parameters)
-      {
-        if (parameters.count("a") != 1 ||
-            parameters.count("b") != 1 ||
-            parameters.count("c") != 1)
-        {
-          LOG(ERROR) << "At least one of the three series IDs is missing";
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-        }
-
-        std::unique_ptr<LayeredSceneWidget> a(CreateSeriesWidget(context, parameters["a"].as<std::string>()));
-        std::unique_ptr<LayeredSceneWidget> b(CreateSeriesWidget(context, parameters["b"].as<std::string>()));
-        std::unique_ptr<LayeredSceneWidget> c(CreateSeriesWidget(context, parameters["c"].as<std::string>()));
-
-        ReferenceLineFactory::Configure(*a, *b);
-        ReferenceLineFactory::Configure(*a, *c);
-        ReferenceLineFactory::Configure(*b, *c);
-
-        std::unique_ptr<LayoutWidget> layout(new LayoutWidget);
-        layout->SetPadding(5);
-        layout->AddWidget(a.release());
-
-        std::unique_ptr<LayoutWidget> layoutB(new LayoutWidget);
-        layoutB->SetVertical();
-        layoutB->SetPadding(5);
-        layoutB->AddWidget(b.release());
-        layoutB->AddWidget(c.release());
-        layout->AddWidget(layoutB.release());
-
-        context.SetCentralWidget(layout.release());        
-      }
-    };
-  }
-}
--- a/Applications/Samples/TestPatternApplication.h	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,63 +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 "SampleApplicationBase.h"
-
-#include "../../Framework/Widgets/TestCairoWidget.h"
-#include "../../Framework/Widgets/TestWorldSceneWidget.h"
-#include "../../Framework/Widgets/LayoutWidget.h"
-
-namespace OrthancStone
-{
-  namespace Samples
-  {
-    class TestPatternApplication : public SampleApplicationBase
-    {
-    public:
-      virtual void DeclareStartupOptions(boost::program_options::options_description& options)
-      {
-        boost::program_options::options_description generic("Sample options");
-        generic.add_options()
-          ("animate", boost::program_options::value<bool>()->default_value(true), "Animate the test pattern")
-          ;
-
-        options.add(generic);    
-      }
-
-      virtual void Initialize(IStatusBar& statusBar,
-                              const boost::program_options::variables_map& parameters)
-      {
-        using namespace OrthancStone;
-
-        std::unique_ptr<LayoutWidget> layout(new LayoutWidget);
-        layout->SetPadding(10);
-        layout->SetBackgroundCleared(true);
-        layout->AddWidget(new TestCairoWidget(parameters["animate"].as<bool>()));
-        layout->AddWidget(new TestWorldSceneWidget(parameters["animate"].as<bool>()));
-
-        context_->SetCentralWidget(layout.release());
-        context_->SetUpdateDelay(25);  // If animation, update the content each 25ms
-      }
-    };
-  }
-}
--- a/Applications/Samples/Web/index.html	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-<!doctype html>
-
-<html lang="us">
-  <head>
-    <meta charset="utf-8" />
-    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-
-    <!-- Disable pinch zoom on mobile devices -->
-    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
-    <meta name="HandheldFriendly" content="true" />
-
-    <title>Wasm Samples</title>
-
-<body>
-    <ul>
-      <li><a href="simple-viewer/simple-viewer.html">Simple Viewer Project (you may add ?studyId=XXX in the url)</a></li>
-      <li><a href="single-frame.html?instance=XXX">Single frame application (you must replace XXX by a valid instance id in the url)</a></li>
-      <li><a href="single-frame-editor.html?instance=XXX">Single frame editor application (you must replace XXX by a valid instance id in the url)</a></li>
-      <li><a href="simple-viewer-single-file.html">Simple Viewer Single file (to be replaced by other samples)</a></li>
-    </ul>
-</body>
-
-</html>
\ No newline at end of file
--- a/Applications/Samples/Web/samples-styles.css	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,16 +0,0 @@
-html, body {
-  width: 100%;
-  height: 100%;
-  margin: 0px;
-  border: 0;
-  overflow: hidden; /*  Disable scrollbars */
-  display: block;  /* No floating content on sides */
-  background-color: black;
-  color: white;
-  font-family: Arial, Helvetica, sans-serif;
-}
-
-canvas {
-  left:0px;
-  top:0px;
-}
\ No newline at end of file
--- a/Applications/Samples/Web/simple-viewer-single-file.html	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,39 +0,0 @@
-<!doctype html>
-
-<html lang="us">
-
-<head>
-  <meta charset="utf-8" />
-  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-
-  <!-- Disable pinch zoom on mobile devices -->
-  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
-  <meta name="HandheldFriendly" content="true" />
-
-  <title>Simple Viewer</title>
-  <link href="samples-styles.css" rel="stylesheet" />
-
-<body>
-  <div id="breadcrumb">
-    <span id="patient-id"></span>
-    <span id="study-description"></span>
-    <span id="series-description"></span>
-  </div>
-  <div style="height: calc(100% - 50px)">
-    <div style="width: 20%; height: 100%; display: inline-block">
-      <canvas id="canvas"></canvas>
-    </div>
-    <div style="width: 70%; height: 100%; display: inline-block">
-      <canvas id="canvas2"></canvas>
-    </div>
-  </div>
-  <div id="toolbox" style="height: 50px">
-    <input tool-selector="line-measure" type="radio" name="radio-tool-selector" class="tool-selector">line
-    <input tool-selector="circle-measure" type="radio" name="radio-tool-selector" class="tool-selector">circle
-    <button action-trigger="action1" class="action-trigger">action1</button>
-    <button action-trigger="action2" class="action-trigger">action2</button>
-  </div>
-  <script type="text/javascript" src="app-simple-viewer-single-file.js"></script>
-</body>
-
-</html>
\ No newline at end of file
--- a/Applications/Samples/Web/simple-viewer-single-file.ts	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,61 +0,0 @@
-import wasmApplicationRunner = require('../../../Platforms/Wasm/wasm-application-runner');
-
-wasmApplicationRunner.InitializeWasmApplication("OrthancStoneSimpleViewerSingleFile", "/orthanc");
-
-function SelectTool(toolName: string) {
-    var command = {
-        command: "selectTool",
-        args: {
-            toolName: toolName
-        }
-    };
-    wasmApplicationRunner.SendSerializedMessageToStoneApplication(JSON.stringify(command));
-
-}
-
-function PerformAction(commandName: string) {
-    var command = {
-        command: commandName,
-        commandType: "simple",
-        args: {}
-    };
-    wasmApplicationRunner.SendSerializedMessageToStoneApplication(JSON.stringify(command));
-}
-
-//initializes the buttons
-//-----------------------
-// install "SelectTool" handlers
-document.querySelectorAll("[tool-selector]").forEach((e) => {
-    console.log(e);
-    (e as HTMLInputElement).addEventListener("click", () => {
-        console.log(e);
-        SelectTool(e.attributes["tool-selector"].value);
-    });
-});
-
-// install "PerformAction" handlers
-document.querySelectorAll("[action-trigger]").forEach((e) => {
-    (e as HTMLInputElement).addEventListener("click", () => {
-        PerformAction(e.attributes["action-trigger"].value);
-    });
-});
-
-// this method is called "from the C++ code" when the StoneApplication is updated.
-// it can be used to update the UI of the application
-function UpdateWebApplicationWithString(statusUpdateMessage: string) {
-  console.log(statusUpdateMessage);
-  
-  if (statusUpdateMessage.startsWith("series-description=")) {
-      document.getElementById("series-description").innerText = statusUpdateMessage.split("=")[1];
-  }
-}
-
-function UpdateWebApplicationWithSerializedMessage(statusUpdateMessageString: string) {
-  console.log("updating web application with serialized message: ", statusUpdateMessageString);
-  console.log("<not supported in the simple viewer (single file)!>");
-}
-
-// make it available to other js scripts in the application
-(<any> window).UpdateWebApplicationWithString = UpdateWebApplicationWithString;
-
-(<any> window).UpdateWebApplicationWithSerializedMessage = UpdateWebApplicationWithSerializedMessage;
--- a/Applications/Samples/Web/simple-viewer-single-file.tsconfig.json	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-{
-    "extends" : "./tsconfig-samples",
-    "compilerOptions": {
-        // "outFile": "../build-web/app-simple-viewer-single-file.js"
-    },
-    "include" : [
-        "simple-viewer-single-file.ts"
-    ]
-}
\ No newline at end of file
--- a/Applications/Samples/Web/single-frame-editor.html	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-<!doctype html>
-
-<html lang="us">
-  <head>
-    <meta charset="utf-8" />
-    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-
-    <!-- Disable pinch zoom on mobile devices -->
-    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
-    <meta name="HandheldFriendly" content="true" />
-
-    <title>Simple Viewer</title>
-    <link href="samples-styles.css" rel="stylesheet" />
-
-<body>
-  <div style="width: 100%; height: 100%">
-    <canvas id="canvas"></canvas>
-  </div>
-  <script type="text/javascript" src="app-single-frame-editor.js"></script>
-</body>
-
-</html>
\ No newline at end of file
--- a/Applications/Samples/Web/single-frame-editor.ts	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-import wasmApplicationRunner = require('../../../Platforms/Wasm/wasm-application-runner');
-
-wasmApplicationRunner.InitializeWasmApplication("OrthancStoneSingleFrameEditor", "/orthanc");
--- a/Applications/Samples/Web/single-frame-editor.tsconfig.json	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,8 +0,0 @@
-{
-    "extends" : "./tsconfig-samples",
-    "compilerOptions": {
-    },
-    "include" : [
-        "single-frame-editor.ts"
-    ]
-}
--- a/Applications/Samples/Web/single-frame.html	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-<!doctype html>
-
-<html lang="us">
-  <head>
-    <meta charset="utf-8" />
-    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-
-    <!-- Disable pinch zoom on mobile devices -->
-    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
-    <meta name="HandheldFriendly" content="true" />
-
-    <title>Simple Viewer</title>
-    <link href="samples-styles.css" rel="stylesheet" />
-
-<body>
-  <div style="width: 100%; height: 100%">
-    <canvas id="canvas"></canvas>
-  </div>
-  <script type="text/javascript" src="app-single-frame.js"></script>
-</body>
-
-</html>
\ No newline at end of file
--- a/Applications/Samples/Web/single-frame.ts	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,4 +0,0 @@
-import wasmApplicationRunner = require('../../../Platforms/Wasm/wasm-application-runner');
-
-wasmApplicationRunner.InitializeWasmApplication("OrthancStoneSingleFrame", "/orthanc");
-
--- a/Applications/Samples/Web/single-frame.tsconfig.json	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,8 +0,0 @@
-{
-    "extends" : "./tsconfig-samples",
-    "compilerOptions": {
-    },
-    "include" : [
-        "single-frame.ts"
-    ]
-}
--- a/Applications/Samples/Web/tsconfig-samples.json	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-{
-    "extends" : "../../../Platforms/Wasm/tsconfig-stone",
-    "compilerOptions": {
-        "sourceMap": false,
-        "lib" : [
-            "es2017",
-            "dom",
-            "dom.iterable"
-        ]
-    }
-}
--- a/Applications/Samples/build-wasm.sh	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,27 +0,0 @@
-#!/bin/bash
-#
-# usage:
-# to build all targets in Debug:
-# ./build-wasm.sh
-#
-# to build a single target in release:
-# ./build-wasm.sh OrthancStoneSingleFrameEditor Release
-
-set -e
-
-target=${1:-all}
-buildType=${2:-Debug}
-
-currentDir=$(pwd)
-samplesRootDir=$(pwd)
-
-mkdir -p $samplesRootDir/build-wasm
-cd $samplesRootDir/build-wasm
-
-source ~/apps/emsdk/emsdk_env.sh
-cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=~/apps/emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_BUILD_TYPE=$buildType -DSTONE_SOURCES_DIR=$currentDir/../../../orthanc-stone -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT=$currentDir/../../../orthanc -DALLOW_DOWNLOADS=ON .. -DENABLE_WASM=ON
-ninja $target
-
-echo "-- building the web application -- "
-cd $currentDir
-./build-web.sh
\ No newline at end of file
--- a/Applications/Samples/build-wasm.sh.old	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,33 +0,0 @@
-#!/bin/bash
-#
-# usage:
-# to build all targets:
-# ./build-wasm.sh
-#
-# to build a single target:
-# ./build-wasm.sh OrthancStoneSingleFrameEditor
-
-set -e
-
-target=${1:-all}
-
-currentDir=$(pwd)
-samplesRootDir=$(pwd)
-
-mkdir -p $samplesRootDir/build-wasm
-cd $samplesRootDir/build-wasm
-
-source ~/apps/emsdk/emsdk_env.sh
-cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake \
-  -DCMAKE_BUILD_TYPE=Release \
-  -DSTONE_SOURCES_DIR=$currentDir/../../../orthanc-stone \
-  -DORTHANC_FRAMEWORK_SOURCE=path \
-  -DORTHANC_FRAMEWORK_ROOT=$currentDir/../../../orthanc \
-  -DALLOW_DOWNLOADS=ON .. \
-  -DENABLE_WASM=ON
-
-ninja $target
-
-echo "-- building the web application -- "
-cd $currentDir
-./build-web.sh
--- a/Applications/Samples/build-web-ext.sh	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,58 +0,0 @@
-#!/bin/bash
-
-set -e
-
-target=${1:-all}
-# this script currently assumes that the wasm code has been built on its side and is availabie in build-wasm/
-
-currentDir=$(pwd)
-
-scriptDirRel=$(dirname $0)
-#echo $scriptDirRel
-scriptDirAbs=$(realpath $scriptDirRel)
-echo $scriptDirAbs
-
-samplesRootDir=scriptDirAbs
-
-outputDir=$samplesRootDir/build-web/
-mkdir -p $outputDir
-
-# files used by all single files samples
-cp $samplesRootDir/Web/index.html $outputDir
-cp $samplesRootDir/Web/samples-styles.css $outputDir
-
-# build simple-viewer-single-file (obsolete project)
-if [[ $target == "all" || $target == "OrthancStoneSimpleViewerSingleFile" ]]; then
-  cp $samplesRootDir/Web/simple-viewer-single-file.html $outputDir
-  tsc --allowJs --project $samplesRootDir/Web/simple-viewer-single-file.tsconfig.json
-  cp $currentDir/build-wasm/OrthancStoneSimpleViewerSingleFile.js  $outputDir
-  cp $currentDir/build-wasm/OrthancStoneSimpleViewerSingleFile.wasm  $outputDir
-fi
-
-# build single-frame
-if [[ $target == "all" || $target == "OrthancStoneSingleFrame" ]]; then
-  cp $samplesRootDir/Web/single-frame.html $outputDir
-  tsc --allowJs --project $samplesRootDir/Web/single-frame.tsconfig.json
-  cp $currentDir/build-wasm/OrthancStoneSingleFrame.js  $outputDir
-  cp $currentDir/build-wasm/OrthancStoneSingleFrame.wasm  $outputDir
-fi
-
-# build single-frame-editor
-if [[ $target == "all" || $target == "OrthancStoneSingleFrameEditor" ]]; then
-  cp $samplesRootDir/Web/single-frame-editor.html $outputDir
-  tsc --allowJs --project $samplesRootDir/Web/single-frame-editor.tsconfig.json
-  cp $currentDir/build-wasm/OrthancStoneSingleFrameEditor.js  $outputDir
-  cp $currentDir/build-wasm/OrthancStoneSingleFrameEditor.wasm  $outputDir
-fi
-
-# build simple-viewer project
-if [[ $target == "all" || $target == "OrthancStoneSimpleViewer" ]]; then
-  mkdir -p $outputDir/simple-viewer/
-  cp $samplesRootDir/SimpleViewer/Wasm/simple-viewer.html $outputDir/simple-viewer/
-  cp $samplesRootDir/SimpleViewer/Wasm/styles.css $outputDir/simple-viewer/
-  tsc --allowJs --project $samplesRootDir/SimpleViewer/Wasm/tsconfig-simple-viewer.json
-  cp $currentDir/build-wasm/OrthancStoneSimpleViewer.js  $outputDir/simple-viewer/
-  cp $currentDir/build-wasm/OrthancStoneSimpleViewer.wasm  $outputDir/simple-viewer/
-fi
-
-cd $currentDir
--- a/Applications/Samples/build-web.sh	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,74 +0,0 @@
-#!/bin/bash
-
-set -e
-
-target=${1:-all}
-# this script currently assumes that the wasm code has been built on its side and is availabie in build-wasm/
-
-currentDir=$(pwd)
-samplesRootDir=$(pwd)
-
-echo "*************************************************************************"
-echo "samplesRootDir = $samplesRootDir"
-echo "*************************************************************************"
-
-outputDir=$samplesRootDir/build-web/
-mkdir -p "$outputDir"
-
-# files used by all single files samples
-cp "$samplesRootDir/Web/index.html" "$outputDir"
-cp "$samplesRootDir/Web/samples-styles.css" "$outputDir"
-
-# # build simple-viewer-single-file (obsolete project)
-# if [[ $target == "all" || $target == "OrthancStoneSimpleViewerSingleFile" ]]; then
-#   cp $samplesRootDir/Web/simple-viewer-single-file.html $outputDir
-#   tsc --project $samplesRootDir/Web/simple-viewer-single-file.tsconfig.json --outDir "$outputDir"
-#   browserify \
-#       "$outputDir/Platforms/Wasm/wasm-application-runner.js" \
-#       "$outputDir/Applications/Samples/Web/simple-viewer-single-file.js" \
-#       -o "$outputDir/app-simple-viewer-single-file.js"
-#   cp "$currentDir/build-wasm/OrthancStoneSimpleViewerSingleFile.js"  $outputDir
-#   cp "$currentDir/build-wasm/OrthancStoneSimpleViewerSingleFile.wasm"  $outputDir
-# fi
-
-# # build single-frame
-# if [[ $target == "all" || $target == "OrthancStoneSingleFrame" ]]; then
-#   cp $samplesRootDir/Web/single-frame.html $outputDir
-#   tsc --project $samplesRootDir/Web/single-frame.tsconfig.json --outDir "$outputDir"
-#   browserify \
-#       "$outputDir/Platforms/Wasm/wasm-application-runner.js" \
-#       "$outputDir/Applications/Samples/Web/single-frame.js" \
-#       -o "$outputDir/app-single-frame.js"
-#   cp "$currentDir/build-wasm/OrthancStoneSingleFrame.js"  $outputDir
-#   cp "$currentDir/build-wasm/OrthancStoneSingleFrame.wasm"  $outputDir
-# fi
-
-# build single-frame-editor
-if [[ $target == "all" || $target == "OrthancStoneSingleFrameEditor" ]]; then
-  cp $samplesRootDir/Web/single-frame-editor.html $outputDir
-  tsc --project $samplesRootDir/Web/single-frame-editor.tsconfig.json --outDir "$outputDir"
-  browserify \
-      "$outputDir/Platforms/Wasm/wasm-application-runner.js" \
-      "$outputDir/Applications/Samples/Web/single-frame-editor.js" \
-      -o "$outputDir/app-single-frame-editor.js"
-  cp "$currentDir/build-wasm/OrthancStoneSingleFrameEditor.js"  $outputDir
-  cp "$currentDir/build-wasm/OrthancStoneSingleFrameEditor.wasm"  $outputDir
-fi
-
-# build simple-viewer project
-if [[ $target == "all" || $target == "OrthancStoneSimpleViewer" ]]; then
-  mkdir -p $outputDir/simple-viewer/
-  cp $samplesRootDir/SimpleViewer/Wasm/simple-viewer.html $outputDir/simple-viewer/
-  cp $samplesRootDir/SimpleViewer/Wasm/styles.css $outputDir/simple-viewer/
-  
-  # the root dir must contain all the source files for the whole project
-  tsc --module commonjs --allowJs --project "$samplesRootDir/SimpleViewer/Wasm/tsconfig-simple-viewer.json" --rootDir "$samplesRootDir/../.." --outDir "$outputDir/simple-viewer/"
-  browserify \
-    "$outputDir/simple-viewer/Platforms/Wasm/wasm-application-runner.js" \
-    "$outputDir/simple-viewer/Applications/Samples/SimpleViewer/Wasm/simple-viewer.js" \
-    -o "$outputDir/simple-viewer/app-simple-viewer.js"
-  cp "$currentDir/build-wasm/OrthancStoneSimpleViewer.js"  "$outputDir/simple-viewer/"
-  cp "$currentDir/build-wasm/OrthancStoneSimpleViewer.wasm"  "$outputDir/simple-viewer/"
-fi
-
-cd $currentDir
--- a/Applications/Samples/get-requirements-windows.ps1	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,50 +0,0 @@
-
-if ($true) {
-
-    Write-Error "This script is obsolete. Please work under WSL and run build-wasm.sh"
-
-} else {
-
-    param(
-        [IO.DirectoryInfo] $EmsdkRootDir = "C:\Emscripten",
-        [bool] $Overwrite = $false
-      )
-    
-    if (Test-Path -Path $EmsdkRootDir) {
-        if( $Override) {
-            Remove-Item -Path $EmsdkRootDir -Force -Recurse
-        } else {
-            throw "The `"$EmsdkRootDir`" folder may not exist! Use the Overwrite flag to bypass this check."
-        }
-    }
-    
-    # TODO: detect whether git is installed
-    # choco install -y git
-    
-    Write-Host "Will retrieve the Emscripten SDK to the `"$EmsdkRootDir`" folder"
-    
-    $EmsdkParentDir = split-path -Parent $EmsdkRootDir
-    $EmsdkRootName = split-path -Leaf $EmsdkRootDir
-    
-    Push-Location $EmsdkParentDir
-    
-    git clone https://github.com/juj/emsdk.git $EmsdkRootName
-    cd $EmsdkRootName
-    
-    git pull
-    
-    ./emsdk install latest
-    
-    ./emsdk activate latest
-    
-    echo "INFO: the ~/.emscripten file has been configured for this installation of Emscripten."
-    
-    Write-Host "emsdk is now installed in $EmsdkRootDir"
-    
-    Pop-Location
-
-}
-
-
-
-
--- a/Applications/Samples/nginx.local.conf	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,44 +0,0 @@
-# Local config to serve the WASM samples static files and reverse proxy Orthanc.
-# Uses port 9977 instead of 80.
-
-# `events` section is mandatory
-events {
-  worker_connections 1024; # Default: 1024
-}
-
-http {
-
-  # prevent nginx sync issues on OSX
-  proxy_buffering off;
-
-  server {
-    listen 9977 default_server;
-    client_max_body_size 4G;
-
-    # location may have to be adjusted depending on your OS and nginx install
-    include /etc/nginx/mime.types;
-    # if not in your system mime.types, add this line to support WASM:
-    # types {
-    #    application/wasm                      wasm; 
-    # }
-
-    # serve WASM static files
-    root build-web/;
-    location / {
-	}
-
-    # reverse proxy orthanc
-	location /orthanc/ {
-		rewrite /orthanc(.*) $1 break;
-		proxy_pass http://127.0.0.1:8042;
-		proxy_set_header Host $http_host;
-		proxy_set_header my-auth-header good-token;
-		proxy_request_buffering off;
-		proxy_max_temp_file_size 0;
-		client_max_body_size 0;
-	}
-
-
-  }
-  
-}
--- a/Applications/Samples/package-lock.json	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-{
-  "requires": true,
-  "lockfileVersion": 1,
-  "dependencies": {
-    "typescript": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.2.2.tgz",
-      "integrity": "sha512-VCj5UiSyHBjwfYacmDuc/NOk4QQixbE+Wn7MFJuS0nRuPQbof132Pw4u53dm264O8LPc2MVsc7RJNml5szurkg=="
-    }
-  }
-}
--- a/Applications/Samples/rt-viewer-demo/CMakeLists.txt	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,142 +0,0 @@
-cmake_minimum_required(VERSION 2.8.3)
-project(RtViewerDemo)
-
-if(MSVC)
-  add_definitions(/MP)
-  if (CMAKE_BUILD_TYPE MATCHES DEBUG)
-    add_definitions(/JMC)
-  endif()
-endif()
-
-message("-------------------------------------------------------------------------------------------------------------------")
-message("ORTHANC_FRAMEWORK_ROOT is set to ${ORTHANC_FRAMEWORK_ROOT}")
-message("-------------------------------------------------------------------------------------------------------------------")
-
-if(NOT DEFINED ORTHANC_FRAMEWORK_ROOT)
-  message(FATAL_ERROR "The location of the Orthanc source repository must be set in the ORTHANC_FRAMEWORK_ROOT CMake variable")
-endif()
-
-message("-------------------------------------------------------------------------------------------------------------------")
-message("STONE_SOURCES_DIR is set to ${STONE_SOURCES_DIR}")
-message("-------------------------------------------------------------------------------------------------------------------")
-
-if(NOT DEFINED STONE_SOURCES_DIR)
-  message(FATAL_ERROR "The location of the Stone of Orthanc source repository must be set in the STONE_SOURCES_DIR CMake variable")
-endif()
-
-include(${STONE_SOURCES_DIR}/Resources/CMake/OrthancStoneParameters.cmake)
-
-if (OPENSSL_NO_CAPIENG)
-add_definitions(-DOPENSSL_NO_CAPIENG=1)
-endif()
-
-set(ENABLE_SDL OFF CACHE BOOL "Target SDL Native application")
-set(ENABLE_QT OFF CACHE BOOL "Target Qt Native application")
-set(ENABLE_WASM OFF CACHE BOOL "Target WASM application")
-
-if (ENABLE_WASM)
-  #####################################################################
-  ## Configuration of the Emscripten compiler for WebAssembly target
-  #####################################################################
-
-  set(WASM_FLAGS "-s WASM=1")
-  set(WASM_FLAGS "${WASM_FLAGS} -s STRICT=1") # drops support for all deprecated build options
-  set(WASM_FLAGS "${WASM_FLAGS} -s FILESYSTEM=1") # if we don't include it, gen_uuid.c fails to build because srand, getpid(), ... are not defined
-  set(WASM_FLAGS "${WASM_FLAGS} -s DISABLE_EXCEPTION_CATCHING=0") # actually enable exception catching 
-  set(WASM_FLAGS "${WASM_FLAGS} -s ERROR_ON_MISSING_LIBRARIES=1")
-
-  if (CMAKE_BUILD_TYPE MATCHES DEBUG)
-    set(WASM_FLAGS "${WASM_FLAGS} -g4") # generate debug information
-    set(WASM_FLAGS "${WASM_FLAGS} -s ASSERTIONS=2") # more runtime checks
-  else()
-    set(WASM_FLAGS "${WASM_FLAGS} -Os") # optimize for web (speed and size)
-  endif()
-
-  set(WASM_MODULE_NAME "StoneFrameworkModule" CACHE STRING "Name of the WebAssembly module")
-
-  set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WASM_FLAGS}")
-  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WASM_FLAGS}")
-
-  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${WASM_FLAGS}")  # not always clear which flags are for the compiler and which one are for the linker -> pass them all to the linker too
-  # set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --js-library ${STONE_SOURCES_DIR}/Applications/Samples/samples-library.js")
-  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/WasmWebService.js")
-  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/WasmDelayedCallExecutor.js")
-  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/default-library.js")
-  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\"]'")
-  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s EXPORT_NAME='\"${WASM_MODULE_NAME}\"'")
-  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1")
-  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s TOTAL_MEMORY=536870912")
-  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s TOTAL_STACK=128000000")
-
-  add_definitions(-DORTHANC_ENABLE_WASM=1)
-  set(ORTHANC_SANDBOXED ON)
-
-elseif (ENABLE_QT OR ENABLE_SDL)
-
-  set(ENABLE_NATIVE ON)
-  set(ORTHANC_SANDBOXED OFF)
-  set(ENABLE_CRYPTO_OPTIONS ON)
-  set(ENABLE_GOOGLE_TEST ON)
-  set(ENABLE_WEB_CLIENT ON)
-
-endif()
-
-
-#####################################################################
-## Configuration for Orthanc
-#####################################################################
-
-if (ORTHANC_STONE_VERSION STREQUAL "mainline")
-  set(ORTHANC_FRAMEWORK_VERSION "mainline")
-  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
-else()
-  set(ORTHANC_FRAMEWORK_VERSION "1.4.1")
-  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
-endif()
-
-set(ORTHANC_FRAMEWORK_SOURCE "${ORTHANC_FRAMEWORK_DEFAULT_SOURCE}" CACHE STRING "Source of the Orthanc source code (can be \"hg\", \"archive\", \"web\" or \"path\")")
-set(ORTHANC_FRAMEWORK_ARCHIVE "" CACHE STRING "Path to the Orthanc archive, if ORTHANC_FRAMEWORK_SOURCE is \"archive\"")
-set(ORTHANC_FRAMEWORK_ROOT "" CACHE STRING "Path to the Orthanc source directory, if ORTHANC_FRAMEWORK_SOURCE is \"path\"")
-
-add_definitions(
-  -DORTHANC_ENABLE_LOGGING_PLUGIN=0
-  )
-
-
-#####################################################################
-## Build a static library containing the Orthanc Stone framework
-#####################################################################
-
-LIST(APPEND ORTHANC_BOOST_COMPONENTS program_options)
-
-include(${STONE_SOURCES_DIR}/Resources/CMake/OrthancStoneConfiguration.cmake)
-
-add_library(OrthancStone STATIC
-  ${ORTHANC_STONE_SOURCES}
-  )
-
-#####################################################################
-## Build all the sample applications
-#####################################################################
-
-include_directories(${ORTHANC_STONE_ROOT})
-
-list(APPEND RTVIEWERDEMO_APPLICATION_SOURCES
-  ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleInteractor.h
-  ${ORTHANC_STONE_ROOT}/Applications/Samples/SampleApplicationBase.h
-  )
-
-if (ENABLE_WASM)
-  list(APPEND RTVIEWERDEMO_APPLICATION_SOURCES
-    ${STONE_WASM_SOURCES}
-    )
-endif()
-
-add_executable(RtViewerDemo
-  main.cpp
-  ${RTVIEWERDEMO_APPLICATION_SOURCES}
-)
-set_target_properties(RtViewerDemo PROPERTIES COMPILE_DEFINITIONS ORTHANC_STONE_SAMPLE=3)
-target_include_directories(RtViewerDemo PRIVATE ${ORTHANC_STONE_ROOT})
-target_link_libraries(RtViewerDemo OrthancStone)
-
--- a/Applications/Samples/rt-viewer-demo/build-sdl-msvc15.ps1	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,24 +0,0 @@
-if (-not (Test-Path "build-sdl-msvc15")) {
-  mkdir -p "build-sdl-msvc15"
-}
-
-cd build-sdl-msvc15
-
-cmake -G "Visual Studio 15 2017 Win64" -DSTATIC_BUILD=ON -DOPENSSL_NO_CAPIENG=ON -DORTHANC_FRAMEWORK_SOURCE=path -DSTONE_SOURCES_DIR="$($pwd)\..\..\..\.." -DORTHANC_FRAMEWORK_ROOT="$($pwd)\..\..\..\..\..\orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON .. 
-
-if (!$?) {
-	Write-Error 'cmake configuration failed' -ErrorAction Stop
-}
-
-cmake --build . --target RtViewerDemo --config Debug
-
-if (!$?) {
-	Write-Error 'cmake build failed' -ErrorAction Stop
-}
-
-cd Debug
-
-.\RtViewerDemo.exe --ct-series=a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa --dose-instance=830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb --struct-instance=54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9
-
-
-
--- a/Applications/Samples/rt-viewer-demo/build-wasm.sh	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-#!/bin/bash
-#
-# usage:
-# build-wasm BUILD_TYPE
-# where BUILD_TYPE is Debug, RelWithDebInfo or Release
-
-set -e
-
-buildType=${1:-Debug}
-
-currentDir=$(pwd)
-currentDirAbs=$(realpath $currentDir)
-
-mkdir -p build-wasm
-cd build-wasm
-
-source ~/apps/emsdk/emsdk_env.sh
-cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake \
--DCMAKE_BUILD_TYPE=$buildType -DSTONE_SOURCES_DIR=$currentDirAbs/../../../../orthanc-stone \
--DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT=$currentDirAbs/../../../../orthanc \
--DALLOW_DOWNLOADS=ON .. -DENABLE_WASM=ON
-
-ninja $target
-
-echo "-- building the web application -- "
-cd $currentDir
-./build-web.sh
-
-echo "Launch start-serving-files.sh to access the web sample application locally"
--- a/Applications/Samples/rt-viewer-demo/build-web.sh	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,32 +0,0 @@
-#!/bin/bash
-
-set -e
-
-target=${1:-all}
-# this script currently assumes that the wasm code has been built on its side and is availabie in build-wasm/
-
-currentDir=$(pwd)
-samplesRootDir=$(pwd)
-
-tscOutput=$samplesRootDir/build-tsc-output/
-outputDir=$samplesRootDir/build-web/
-mkdir -p "$outputDir"
-
-# files used by all single files samples
-cp "$samplesRootDir/index.html" "$outputDir"
-cp "$samplesRootDir/samples-styles.css" "$outputDir"
-
-# build rt-viewer-demo
-cp $samplesRootDir/rt-viewer-demo.html $outputDir
-tsc --project $samplesRootDir/rt-viewer-demo.tsconfig.json --outDir "$tscOutput"
-browserify \
-    "$tscOutput/orthanc-stone/Platforms/Wasm/logger.js" \
-    "$tscOutput/orthanc-stone/Platforms/Wasm/stone-framework-loader.js" \
-    "$tscOutput/orthanc-stone/Platforms/Wasm/wasm-application-runner.js" \
-    "$tscOutput/orthanc-stone/Platforms/Wasm/wasm-viewport.js" \
-    "$tscOutput/rt-viewer-sample/rt-viewer-demo.js" \
-    -o "$outputDir/app-rt-viewer-demo.js"
-cp "$currentDir/build-wasm/RtViewerDemo.js"  $outputDir
-cp "$currentDir/build-wasm/RtViewerDemo.wasm"  $outputDir
-
-cd $currentDir
--- a/Applications/Samples/rt-viewer-demo/index.html	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-<!doctype html>
-
-<html lang="us">
-  <head>
-    <meta charset="utf-8" />
-    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-
-    <!-- Disable pinch zoom on mobile devices -->
-    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
-    <meta name="HandheldFriendly" content="true" />
-
-    <title>Wasm Samples</title>
-
-<body>
-    <ul>
-      <li><a href="rt-viewer-demo.html?ct-series=a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa&dose-instance=830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb&struct-instance=54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9">RTSTRUCT + CT + RTDOSE viewer demo. Pplease replace the url arguments with suitable IDs (you can find those in the Orthanc Explorer, for instance)</a></li>
-    </ul>
-</body>
-
-</html>
--- a/Applications/Samples/rt-viewer-demo/main.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,893 +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 "Applications/IStoneApplication.h"
-#include "Framework/Widgets/WorldSceneWidget.h"
-#include "Framework/Widgets/LayoutWidget.h"
-
-#if ORTHANC_ENABLE_WASM==1
-  #include "Platforms/Wasm/WasmPlatformApplicationAdapter.h"
-  #include "Platforms/Wasm/Defaults.h"
-  #include "Platforms/Wasm/WasmViewport.h"
-#endif
-
-#if ORTHANC_ENABLE_QT==1
-  #include "Qt/SampleMainWindow.h"
-  #include "Qt/SampleMainWindowWithButtons.h"
-#endif
-
-#include "Framework/Layers/DicomSeriesVolumeSlicer.h"
-#include "Framework/Widgets/SliceViewerWidget.h"
-#include "Framework/Volumes/StructureSetLoader.h"
-
-#include <Core/Logging.h>
-#include <Core/OrthancException.h>
-#include <Core/Images/ImageTraits.h>
-
-#include <boost/math/constants/constants.hpp>
-#include "Framework/dev.h"
-#include "Framework/Widgets/LayoutWidget.h"
-#include "Framework/Layers/DicomStructureSetSlicer.h"
-
-namespace OrthancStone
-{
-  namespace Samples
-  {
-    class RtViewerDemoBaseApplication : public IStoneApplication
-    {
-    protected:
-      // ownership is transferred to the application context
-#ifndef RESTORE_NON_RTVIEWERDEMO_BEHAVIOR
-      LayoutWidget*          mainWidget_;
-#else
-      WorldSceneWidget*  mainWidget_;
-#endif
-
-    public:
-      virtual void Initialize(StoneApplicationContext* context,
-                              IStatusBar& statusBar,
-                              const boost::program_options::variables_map& parameters) ORTHANC_OVERRIDE
-      {
-      }
-
-      virtual std::string GetTitle() const ORTHANC_OVERRIDE
-      {
-        return "Stone of Orthanc - Sample";
-      }
-
-      /**
-       * In the basic samples, the commands are handled by the platform adapter and NOT
-       * by the application handler
-      */
-      virtual void HandleSerializedMessage(const char* data) ORTHANC_OVERRIDE {};
-
-
-      virtual void Finalize() ORTHANC_OVERRIDE {}
-      virtual IWidget* GetCentralWidget() ORTHANC_OVERRIDE {return mainWidget_;}
-
-#if ORTHANC_ENABLE_WASM==1
-      // default implementations for a single canvas named "canvas" in the HTML and an empty WasmApplicationAdapter
-
-      virtual void InitializeWasm() ORTHANC_OVERRIDE
-      {
-        AttachWidgetToWasmViewport("canvas", mainWidget_);
-      }
-
-      virtual WasmPlatformApplicationAdapter* CreateWasmApplicationAdapter(MessageBroker& broker)
-      {
-        return new WasmPlatformApplicationAdapter(broker, *this);
-      }
-#endif
-
-    };
-
-    // this application actually works in Qt and WASM
-    class RtViewerDemoBaseSingleCanvasWithButtonsApplication : public RtViewerDemoBaseApplication
-    {
-public:
-      virtual void OnPushButton1Clicked() {}
-      virtual void OnPushButton2Clicked() {}
-      virtual void OnTool1Clicked() {}
-      virtual void OnTool2Clicked() {}
-
-      virtual void GetButtonNames(std::string& pushButton1,
-                                  std::string& pushButton2,
-                                  std::string& tool1,
-                                  std::string& tool2
-                                  ) {
-        pushButton1 = "action1";
-        pushButton2 = "action2";
-        tool1 = "tool1";
-        tool2 = "tool2";
-      }
-
-#if ORTHANC_ENABLE_QT==1
-      virtual QStoneMainWindow* CreateQtMainWindow() {
-        return new SampleMainWindowWithButtons(dynamic_cast<OrthancStone::NativeStoneApplicationContext&>(*context_), *this);
-      }
-#endif
-
-    };
-
-    // this application actually works in SDL and WASM
-    class RtViewerDemoBaseApplicationSingleCanvas : public RtViewerDemoBaseApplication
-    {
-public:
-
-#if ORTHANC_ENABLE_QT==1
-      virtual QStoneMainWindow* CreateQtMainWindow() {
-        return new SampleMainWindow(dynamic_cast<OrthancStone::NativeStoneApplicationContext&>(*context_), *this);
-      }
-#endif
-    };
-  }
-}
-
-
-
-namespace OrthancStone
-{
-  namespace Samples
-  {
-    template <Orthanc::PixelFormat T>
-    void ReadDistributionInternal(std::vector<float>& distribution,
-                                  const Orthanc::ImageAccessor& image)
-    {
-      const unsigned int width = image.GetWidth();
-      const unsigned int height = image.GetHeight();
-      
-      distribution.resize(width * height);
-      size_t pos = 0;
-
-      for (unsigned int y = 0; y < height; y++)
-      {
-        for (unsigned int x = 0; x < width; x++, pos++)
-        {
-          distribution[pos] = Orthanc::ImageTraits<T>::GetFloatPixel(image, x, y);
-        }
-      }
-    }
-
-    void ReadDistribution(std::vector<float>& distribution,
-                          const Orthanc::ImageAccessor& image)
-    {
-      switch (image.GetFormat())
-      {
-        case Orthanc::PixelFormat_Grayscale8:
-          ReadDistributionInternal<Orthanc::PixelFormat_Grayscale8>(distribution, image);
-          break;
-
-        case Orthanc::PixelFormat_Grayscale16:
-          ReadDistributionInternal<Orthanc::PixelFormat_Grayscale16>(distribution, image);
-          break;
-
-        case Orthanc::PixelFormat_SignedGrayscale16:
-          ReadDistributionInternal<Orthanc::PixelFormat_SignedGrayscale16>(distribution, image);
-          break;
-
-        case Orthanc::PixelFormat_Grayscale32:
-          ReadDistributionInternal<Orthanc::PixelFormat_Grayscale32>(distribution, image);
-          break;
-
-        case Orthanc::PixelFormat_Grayscale64:
-          ReadDistributionInternal<Orthanc::PixelFormat_Grayscale64>(distribution, image);
-          break;
-
-        default:
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-      }
-    }
-
-
-    class DoseInteractor : public VolumeImageInteractor
-    {
-    private:
-      SliceViewerWidget&         widget_;
-      size_t               layer_;
-      DicomFrameConverter  converter_;
-
-
-      
-    protected:
-      virtual void NotifySliceChange(const ISlicedVolume& slicedVolume,
-                                    const size_t& sliceIndex,
-                                    const Slice& slice)
-      {
-        converter_ = slice.GetConverter();
-        
-  #if 0
-        const OrthancVolumeImage& volume = dynamic_cast<const OrthancVolumeImage&>(slicedVolume);
-
-        RenderStyle s = widget_.GetLayerStyle(layer_);
-
-        if (volume.FitWindowingToRange(s, slice.GetConverter()))
-        {
-          printf("Windowing: %f => %f\n", s.customWindowCenter_, s.customWindowWidth_);
-          widget_.SetLayerStyle(layer_, s);
-        }
-  #endif
-      }
-
-      virtual void NotifyVolumeReady(const ISlicedVolume& slicedVolume)
-      {
-        const float percentile = 0.01f;
-        const OrthancVolumeImage& volume = dynamic_cast<const OrthancVolumeImage&>(slicedVolume);
-
-        std::vector<float> distribution;
-        ReadDistribution(distribution, volume.GetImage().GetInternalImage());
-        std::sort(distribution.begin(), distribution.end());
-
-        int start = static_cast<int>(std::ceil(distribution.size() * percentile));
-        int end = static_cast<int>(std::floor(distribution.size() * (1.0f - percentile)));
-
-        float a = 0;
-        float b = 0;
-        
-        if (start < end &&
-            start >= 0 &&
-            end < static_cast<int>(distribution.size()))
-        {
-          a = distribution[start];
-          b = distribution[end];
-        }
-        else if (!distribution.empty())
-        {
-          // Too small distribution: Use full range
-          a = distribution.front();
-          b = distribution.back();
-        }
-
-        //printf("%f %f\n", a, b);
-
-        RenderStyle s = widget_.GetLayerStyle(layer_);
-        s.windowing_ = ImageWindowing_Custom;
-        s.customWindowCenter_ = static_cast<float>(converter_.Apply((a + b) / 2.0f));
-        s.customWindowWidth_ = static_cast<float>(converter_.Apply(b - a));
-        
-        // 96.210556 => 192.421112
-        widget_.SetLayerStyle(layer_, s);
-        printf("Windowing: %f => %f\n", s.customWindowCenter_, s.customWindowWidth_);      
-      }
-
-    public:
-      DoseInteractor(MessageBroker& broker, OrthancVolumeImage& volume,
-                    SliceViewerWidget& widget,
-                    VolumeProjection projection,
-                    size_t layer) :
-        VolumeImageInteractor(broker, volume, widget, projection),
-        widget_(widget),
-        layer_(layer)
-      {
-      }
-    };
-
-    class RtViewerDemoApplication :
-      public RtViewerDemoBaseApplicationSingleCanvas,
-      public IObserver
-    {
-    public:
-      std::vector<std::pair<SliceViewerWidget*, size_t> > doseCtWidgetLayerPairs_;
-      std::list<OrthancStone::IWorldSceneInteractor*>    interactors_;
-
-      class Interactor : public IWorldSceneInteractor
-      {
-      private:
-        RtViewerDemoApplication&  application_;
-
-      public:
-        Interactor(RtViewerDemoApplication&  application) :
-          application_(application)
-        {
-        }
-
-        virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& widget,
-          const ViewportGeometry& view,
-          MouseButton button,
-          KeyboardModifiers modifiers,
-          int viewportX,
-          int viewportY,
-          double x,
-          double y,
-          IStatusBar* statusBar,
-          const std::vector<Touch>& displayTouches)
-        {
-          return NULL;
-        }
-
-        virtual void MouseOver(CairoContext& context,
-          WorldSceneWidget& widget,
-          const ViewportGeometry& view,
-          double x,
-          double y,
-          IStatusBar* statusBar)
-        {
-          if (statusBar != NULL)
-          {
-            Vector p = dynamic_cast<SliceViewerWidget&>(widget).GetSlice().MapSliceToWorldCoordinates(x, y);
-
-            char buf[64];
-            sprintf(buf, "X = %.02f Y = %.02f Z = %.02f (in cm)",
-              p[0] / 10.0, p[1] / 10.0, p[2] / 10.0);
-            statusBar->SetMessage(buf);
-          }
-        }
-
-        virtual void MouseWheel(WorldSceneWidget& widget,
-          MouseWheelDirection direction,
-          KeyboardModifiers modifiers,
-          IStatusBar* statusBar)
-        {
-          int scale = (modifiers & KeyboardModifiers_Control ? 10 : 1);
-
-          switch (direction)
-          {
-          case MouseWheelDirection_Up:
-            application_.OffsetSlice(-scale);
-            break;
-
-          case MouseWheelDirection_Down:
-            application_.OffsetSlice(scale);
-            break;
-
-          default:
-            break;
-          }
-        }
-
-        virtual void KeyPressed(WorldSceneWidget& widget,
-          KeyboardKeys key,
-          char keyChar,
-          KeyboardModifiers modifiers,
-          IStatusBar* statusBar)
-        {
-          switch (keyChar)
-          {
-          case 's':
-            // TODO: recursively traverse children
-            widget.FitContent();
-            break;
-
-          default:
-            break;
-          }
-        }
-      };
-
-      void OffsetSlice(int offset)
-      {
-        if (source_ != NULL)
-        {
-          int slice = static_cast<int>(slice_) + offset;
-
-          if (slice < 0)
-          {
-            slice = 0;
-          }
-
-          if (slice >= static_cast<int>(source_->GetSliceCount()))
-          {
-            slice = static_cast<int>(source_->GetSliceCount()) - 1;
-          }
-
-          if (slice != static_cast<int>(slice_))
-          {
-            SetSlice(slice);
-          }
-        }
-      }
-
-
-      SliceViewerWidget& GetMainWidget()
-      {
-        return *dynamic_cast<SliceViewerWidget*>(mainWidget_);
-      }
-
-
-      void SetSlice(size_t index)
-      {
-        if (source_ != NULL &&
-          index < source_->GetSliceCount())
-        {
-          slice_ = static_cast<unsigned int>(index);
-
-#if 1
-          GetMainWidget().SetSlice(source_->GetSlice(slice_).GetGeometry());
-#else
-          // TEST for scene extents - Rotate the axes
-          double a = 15.0 / 180.0 * boost::math::constants::pi<double>();
-
-#if 1
-          Vector x; GeometryToolbox::AssignVector(x, cos(a), sin(a), 0);
-          Vector y; GeometryToolbox::AssignVector(y, -sin(a), cos(a), 0);
-#else
-          // Flip the normal
-          Vector x; GeometryToolbox::AssignVector(x, cos(a), sin(a), 0);
-          Vector y; GeometryToolbox::AssignVector(y, sin(a), -cos(a), 0);
-#endif
-
-          SliceGeometry s(source_->GetSlice(slice_).GetGeometry().GetOrigin(), x, y);
-          widget_->SetSlice(s);
-#endif
-        }
-      }
-
-
-      void OnMainWidgetGeometryReady(const IVolumeSlicer::GeometryReadyMessage& message)
-      {
-        // 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())
-        {
-          SetSlice(source_->GetSliceCount() / 2);
-        }
-
-        GetMainWidget().FitContent();
-      }
-
-    DicomFrameConverter                                converter_;
-
-    void OnSliceContentChangedMessage(const ISlicedVolume::SliceContentChangedMessage&   message)
-    {
-      converter_ = message.GetSlice().GetConverter();
-    }
-
-    void OnVolumeReadyMessage(const ISlicedVolume::VolumeReadyMessage& message)
-    {
-      const float percentile = 0.01f;
-
-      auto& slicedVolume = message.GetOrigin();
-      const OrthancVolumeImage& volume = dynamic_cast<const OrthancVolumeImage&>(slicedVolume);
-
-      std::vector<float> distribution;
-      ReadDistribution(distribution, volume.GetImage().GetInternalImage());
-      std::sort(distribution.begin(), distribution.end());
-
-      int start = static_cast<int>(std::ceil(distribution.size() * percentile));
-      int end = static_cast<int>(std::floor(distribution.size() * (1.0f - percentile)));
-
-      float a = 0;
-      float b = 0;
-
-      if (start < end &&
-        start >= 0 &&
-        end < static_cast<int>(distribution.size()))
-      {
-        a = distribution[start];
-        b = distribution[end];
-      }
-      else if (!distribution.empty())
-      {
-        // Too small distribution: Use full range
-        a = distribution.front();
-        b = distribution.back();
-      }
-
-      //printf("WINDOWING %f %f\n", a, b);
-
-      for (const auto& pair : doseCtWidgetLayerPairs_)
-      {
-        auto widget = pair.first;
-        auto layer = pair.second;
-        RenderStyle s = widget->GetLayerStyle(layer);
-        s.windowing_ = ImageWindowing_Custom;
-        s.customWindowCenter_ = static_cast<float>(converter_.Apply((a + b) / 2.0f));
-        s.customWindowWidth_ = static_cast<float>(converter_.Apply(b - a));
-
-        // 96.210556 => 192.421112
-        widget->SetLayerStyle(layer, s);
-        printf("Windowing: %f => %f\n", s.customWindowCenter_, s.customWindowWidth_);
-      }
-    }
-      
-
-
-      size_t AddDoseLayer(SliceViewerWidget& widget,
-        OrthancVolumeImage& volume, VolumeProjection projection);
-
-      void AddStructLayer(
-        SliceViewerWidget& widget, StructureSetLoader& loader);
-
-      SliceViewerWidget* CreateDoseCtWidget(
-        std::unique_ptr<OrthancVolumeImage>& ct,
-        std::unique_ptr<OrthancVolumeImage>& dose,
-        std::unique_ptr<StructureSetLoader>& structLoader,
-        VolumeProjection projection);
-
-      void AddCtLayer(SliceViewerWidget& widget, OrthancVolumeImage& volume);
-
-      std::unique_ptr<Interactor>         mainWidgetInteractor_;
-      const DicomSeriesVolumeSlicer*    source_;
-      unsigned int                      slice_;
-
-      std::string                                        ctSeries_;
-      std::string                                        doseInstance_;
-      std::string                                        doseSeries_;
-      std::string                                        structInstance_;
-      std::unique_ptr<OrthancStone::OrthancVolumeImage>    dose_;
-      std::unique_ptr<OrthancStone::OrthancVolumeImage>    ct_;
-      std::unique_ptr<OrthancStone::StructureSetLoader>    struct_;
-
-    public:
-      RtViewerDemoApplication(MessageBroker& broker) :
-        IObserver(broker),
-        source_(NULL),
-        slice_(0)
-      {
-      }
-
-      /*
-      dev options on bgo xps15
-
-      COMMAND LINE
-      --ct-series=a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa --dose-instance=830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb --struct-instance=54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9
-
-      URL PARAMETERS
-      ?ct-series=a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa&dose-instance=830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb&struct-instance=54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9
-
-      */
-
-      void ParseParameters(const boost::program_options::variables_map&  parameters)
-      {
-        // Generic
-        {
-          if (parameters.count("verbose"))
-          {
-            Orthanc::Logging::EnableInfoLevel(true);
-            LOG(INFO) << "Verbose logs (info) are enabled";
-          }
-        }
-
-        {
-          if (parameters.count("trace"))
-          {
-            LOG(INFO) << "parameters.count(\"trace\") != 0";
-            Orthanc::Logging::EnableTraceLevel(true);
-            VLOG(1) << "Trace logs (debug) are enabled";
-          }
-        }
-
-        // CT series
-        {
-
-          if (parameters.count("ct-series") != 1)
-          {
-            LOG(ERROR) << "There must be exactly one CT series specified";
-            throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-          }
-          ctSeries_ = parameters["ct-series"].as<std::string>();
-        }
-
-        // RTDOSE 
-        {
-          if (parameters.count("dose-instance") == 1)
-          {
-            doseInstance_ = parameters["dose-instance"].as<std::string>();
-          }
-          else
-          {
-#ifdef BGO_NOT_IMPLEMENTED_YET
-            // Dose series
-            if (parameters.count("dose-series") != 1)
-            {
-              LOG(ERROR) << "the RTDOSE series is missing";
-              throw Orthanc::OrthancException(
-                Orthanc::ErrorCode_ParameterOutOfRange);
-            }
-            doseSeries_ = parameters["ct"].as<std::string>();
-#endif
-            LOG(ERROR) << "the RTSTRUCT instance is missing";
-            throw Orthanc::OrthancException(
-              Orthanc::ErrorCode_ParameterOutOfRange);
-          }
-        }
-        
-        // RTSTRUCT 
-        {
-          if (parameters.count("struct-instance") == 1)
-          {
-            structInstance_ = parameters["struct-instance"].as<std::string>();
-          }
-          else
-          {
-#ifdef BGO_NOT_IMPLEMENTED_YET
-            // Struct series
-            if (parameters.count("struct-series") != 1)
-            {
-              LOG(ERROR) << "the RTSTRUCT series is missing";
-              throw Orthanc::OrthancException(
-                Orthanc::ErrorCode_ParameterOutOfRange);
-            }
-            structSeries_ = parameters["struct-series"].as<std::string>();
-#endif
-            LOG(ERROR) << "the RTSTRUCT instance is missing";
-            throw Orthanc::OrthancException(
-              Orthanc::ErrorCode_ParameterOutOfRange);
-          }
-        }
-      }
-
-      virtual void DeclareStartupOptions(
-        boost::program_options::options_description& options)
-      {
-        boost::program_options::options_description generic(
-          "RtViewerDemo options. Please note that some of these options "
-          "are mutually exclusive");
-        generic.add_options()
-          ("ct-series", boost::program_options::value<std::string>(),
-            "Orthanc ID of the CT series")
-          ("dose-instance", boost::program_options::value<std::string>(), 
-            "Orthanc ID of the RTDOSE instance (incompatible with dose-series)")
-          ("dose-series", boost::program_options::value<std::string>(), 
-            "NOT IMPLEMENTED YET. Orthanc ID of the RTDOSE series (incompatible"
-            " with dose-instance)")
-          ("struct-instance", boost::program_options::value<std::string>(), 
-            "Orthanc ID of the RTSTRUCT instance (incompatible with struct-"
-            "series)")
-          ("struct-series", boost::program_options::value<std::string>(), 
-            "NOT IMPLEMENTED YET. Orthanc ID of the RTSTRUCT (incompatible with"
-            " struct-instance)")
-          ("smooth", boost::program_options::value<bool>()->default_value(true),
-            "Enable bilinear image smoothing")
-          ;
-
-        options.add(generic);
-      }
-
-      virtual void Initialize(
-        StoneApplicationContext*                      context,
-        IStatusBar&                                   statusBar,
-        const boost::program_options::variables_map&  parameters)
-      {
-        using namespace OrthancStone;
-
-        ParseParameters(parameters);
-
-        context_ = context;
-
-        statusBar.SetMessage("Use the key \"s\" to reinitialize the layout");
-
-        if (!ctSeries_.empty())
-        {
-          printf("CT = [%s]\n", ctSeries_.c_str());
-
-          ct_.reset(new OrthancStone::OrthancVolumeImage(
-            IObserver::GetBroker(), context->GetOrthancApiClient(), false));
-          ct_->ScheduleLoadSeries(ctSeries_);
-          //ct_->ScheduleLoadSeries(
-          //  "a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa"); // IBA
-          //ct_->ScheduleLoadSeries(
-          //  "03677739-1d8bca40-db1daf59-d74ff548-7f6fc9c0"); // 0522c0001 TCIA
-        }
-
-        if (!doseSeries_.empty() ||
-          !doseInstance_.empty())
-        {
-          dose_.reset(new OrthancStone::OrthancVolumeImage(
-            IObserver::GetBroker(), context->GetOrthancApiClient(), true));
-
-
-          dose_->RegisterObserverCallback(
-            new Callable<RtViewerDemoApplication, ISlicedVolume::VolumeReadyMessage>
-            (*this, &RtViewerDemoApplication::OnVolumeReadyMessage));
-
-          dose_->RegisterObserverCallback(
-            new Callable<RtViewerDemoApplication, ISlicedVolume::SliceContentChangedMessage>
-            (*this, &RtViewerDemoApplication::OnSliceContentChangedMessage));
-
-          if (doseInstance_.empty())
-          {
-            dose_->ScheduleLoadSeries(doseSeries_);
-          }
-          else
-          {
-            dose_->ScheduleLoadInstance(doseInstance_);
-          }
-
-          //dose_->ScheduleLoadInstance(
-            //"830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb");  // IBA 1
-          //dose_->ScheduleLoadInstance(
-            //"269f26f4-0c83eeeb-2e67abbd-5467a40f-f1bec90c"); //0522c0001 TCIA
-        }
-
-        if (!structInstance_.empty())
-        {
-          struct_.reset(new OrthancStone::StructureSetLoader(
-            IObserver::GetBroker(), context->GetOrthancApiClient()));
-
-          struct_->ScheduleLoadInstance(structInstance_);
-
-          //struct_->ScheduleLoadInstance(
-            //"54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9"); // IBA
-          //struct_->ScheduleLoadInstance(
-            //"17cd032b-ad92a438-ca05f06a-f9e96668-7e3e9e20"); // 0522c0001 TCIA
-        }
-
-        mainWidget_ = new LayoutWidget("main-layout");
-        mainWidget_->SetBackgroundColor(0, 0, 0);
-        mainWidget_->SetBackgroundCleared(true);
-        mainWidget_->SetPadding(0);
-
-        auto axialWidget = CreateDoseCtWidget
-        (ct_, dose_, struct_, OrthancStone::VolumeProjection_Axial);
-        mainWidget_->AddWidget(axialWidget);
-               
-        std::unique_ptr<OrthancStone::LayoutWidget> subLayout(
-          new OrthancStone::LayoutWidget("main-layout"));
-        subLayout->SetVertical();
-        subLayout->SetPadding(5);
-
-        auto coronalWidget = CreateDoseCtWidget
-        (ct_, dose_, struct_, OrthancStone::VolumeProjection_Coronal);
-        subLayout->AddWidget(coronalWidget);
-
-        auto sagittalWidget = CreateDoseCtWidget
-        (ct_, dose_, struct_, OrthancStone::VolumeProjection_Sagittal);
-        subLayout->AddWidget(sagittalWidget);
-        
-        mainWidget_->AddWidget(subLayout.release());
-      }
-    };
-
-
-    size_t RtViewerDemoApplication::AddDoseLayer(
-      SliceViewerWidget& widget,
-      OrthancVolumeImage& volume, VolumeProjection projection)
-    {
-      size_t layer = widget.AddLayer(
-        new VolumeImageMPRSlicer(IObserver::GetBroker(), volume));
-
-      RenderStyle s;
-      //s.drawGrid_ = true;
-      s.SetColor(255, 0, 0);  // Draw missing PET layer in red
-      s.alpha_ = 0.3f;
-      s.applyLut_ = true;
-      s.lut_ = Orthanc::EmbeddedResources::COLORMAP_JET;
-      s.interpolation_ = ImageInterpolation_Bilinear;
-      widget.SetLayerStyle(layer, s);
-
-      return layer;
-    }
-
-    void RtViewerDemoApplication::AddStructLayer(
-      SliceViewerWidget& widget, StructureSetLoader& loader)
-    {
-      widget.AddLayer(new DicomStructureSetSlicer(
-        IObserver::GetBroker(), loader));
-    }
-
-    SliceViewerWidget* RtViewerDemoApplication::CreateDoseCtWidget(
-      std::unique_ptr<OrthancVolumeImage>& ct,
-      std::unique_ptr<OrthancVolumeImage>& dose,
-      std::unique_ptr<StructureSetLoader>& structLoader,
-      VolumeProjection projection)
-    {
-      std::unique_ptr<OrthancStone::SliceViewerWidget> widget(
-        new OrthancStone::SliceViewerWidget(IObserver::GetBroker(),
-          "ct-dose-widget"));
-
-      if (ct.get() != NULL)
-      {
-        AddCtLayer(*widget, *ct);
-      }
-
-      if (dose.get() != NULL)
-      {
-        size_t layer = AddDoseLayer(*widget, *dose, projection);
-
-        // we need to store the dose rendering widget because we'll update them
-        // according to various asynchronous events
-        doseCtWidgetLayerPairs_.push_back(std::make_pair(widget.get(), layer));
-#if 0
-        interactors_.push_back(new VolumeImageInteractor(
-          IObserver::GetBroker(), *dose, *widget, projection));
-#else
-        interactors_.push_back(new DoseInteractor(
-          IObserver::GetBroker(), *dose, *widget, projection, layer));
-#endif
-      }
-      else if (ct.get() != NULL)
-      {
-        interactors_.push_back(
-          new VolumeImageInteractor(
-            IObserver::GetBroker(), *ct, *widget, projection));
-      }
-
-      if (structLoader.get() != NULL)
-      {
-        AddStructLayer(*widget, *structLoader);
-      }
-
-      return widget.release();
-    }
-
-    void RtViewerDemoApplication::AddCtLayer(
-      SliceViewerWidget& widget,
-      OrthancVolumeImage& volume)
-    {
-      size_t layer = widget.AddLayer(
-        new VolumeImageMPRSlicer(IObserver::GetBroker(), volume));
-
-      RenderStyle s;
-      //s.drawGrid_ = true;
-      s.alpha_ = 1;
-      s.windowing_ = ImageWindowing_Bone;
-      widget.SetLayerStyle(layer, s);
-    }
-  }
-}
-
-
-
-#if ORTHANC_ENABLE_WASM==1
-
-#include "Platforms/Wasm/WasmWebService.h"
-#include "Platforms/Wasm/WasmViewport.h"
-
-#include <emscripten/emscripten.h>
-
-//#include "SampleList.h"
-
-
-OrthancStone::IStoneApplication* CreateUserApplication(OrthancStone::MessageBroker& broker)
-{
-  return new OrthancStone::Samples::RtViewerDemoApplication(broker);
-}
-
-OrthancStone::WasmPlatformApplicationAdapter* CreateWasmApplicationAdapter(OrthancStone::MessageBroker& broker, OrthancStone::IStoneApplication* application)
-{
-  return dynamic_cast<OrthancStone::Samples::RtViewerDemoApplication*>(application)->CreateWasmApplicationAdapter(broker);
-}
-
-#else
-
-//#include "SampleList.h"
-#if ORTHANC_ENABLE_SDL==1
-#include "Applications/Sdl/SdlStoneApplicationRunner.h"
-#endif
-#if ORTHANC_ENABLE_QT==1
-#include "Applications/Qt/SampleQtApplicationRunner.h"
-#endif
-#include "Framework/Messages/MessageBroker.h"
-
-int main(int argc, char* argv[])
-{
-  OrthancStone::MessageBroker broker;
-  OrthancStone::Samples::RtViewerDemoApplication sampleStoneApplication(broker);
-
-#if ORTHANC_ENABLE_SDL==1
-  OrthancStone::SdlStoneApplicationRunner sdlApplicationRunner(broker, sampleStoneApplication);
-  return sdlApplicationRunner.Execute(argc, argv);
-#endif
-#if ORTHANC_ENABLE_QT==1
-  OrthancStone::Samples::SampleQtApplicationRunner qtAppRunner(broker, sampleStoneApplication);
-  return qtAppRunner.Execute(argc, argv);
-#endif
-}
-
-
-#endif
-
-
-
-
-
-
-
--- a/Applications/Samples/rt-viewer-demo/nginx.local.conf	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,44 +0,0 @@
-# Local config to serve the WASM samples static files and reverse proxy Orthanc.
-# Uses port 9977 instead of 80.
-
-# `events` section is mandatory
-events {
-  worker_connections 1024; # Default: 1024
-}
-
-http {
-
-  # prevent nginx sync issues on OSX
-  proxy_buffering off;
-
-  server {
-    listen 9977 default_server;
-    client_max_body_size 4G;
-
-    # location may have to be adjusted depending on your OS and nginx install
-    include /etc/nginx/mime.types;
-    # if not in your system mime.types, add this line to support WASM:
-    # types {
-    #    application/wasm                      wasm; 
-    # }
-
-    # serve WASM static files
-    root build-web/;
-    location / {
-	}
-
-    # reverse proxy orthanc
-	location /orthanc/ {
-		rewrite /orthanc(.*) $1 break;
-		proxy_pass http://127.0.0.1:8042;
-		proxy_set_header Host $http_host;
-		proxy_set_header my-auth-header good-token;
-		proxy_request_buffering off;
-		proxy_max_temp_file_size 0;
-		client_max_body_size 0;
-	}
-
-
-  }
-  
-}
--- a/Applications/Samples/rt-viewer-demo/rt-viewer-demo.html	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,25 +0,0 @@
-<!doctype html>
-
-<html lang="us">
-  <head>
-    <meta charset="utf-8" />
-    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-
-    <!-- Disable pinch zoom on mobile devices -->
-    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
-    <meta name="HandheldFriendly" content="true" />
-
-    <title>Simple Viewer</title>
-    <link href="samples-styles.css" rel="stylesheet" />
-
-<body>
-  <div style="width: 100%; height: 5%">
-    <p>RTSTRUCT viewer demonstration</p>
-  </div>
-  <div style="width: 100%; height: 95%">
-    <canvas id="canvas"></canvas>
-  </div>
-  <script type="text/javascript" src="app-rt-viewer-demo.js"></script>
-</body>
-
-</html>
\ No newline at end of file
--- a/Applications/Samples/rt-viewer-demo/rt-viewer-demo.ts	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,5 +0,0 @@
-import { InitializeWasmApplication } from '../../../Platforms/Wasm/wasm-application-runner';
-
-
-InitializeWasmApplication("RtViewerDemo", "/orthanc");
-
--- a/Applications/Samples/rt-viewer-demo/rt-viewer-demo.tsconfig.json	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,8 +0,0 @@
-{
-    "extends" : "./tsconfig-samples",
-    "compilerOptions": {
-    },
-    "include" : [
-        "rt-viewer-demo.ts"
-    ]
-}
--- a/Applications/Samples/rt-viewer-demo/samples-styles.css	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,16 +0,0 @@
-html, body {
-  width: 100%;
-  height: 100%;
-  margin: 0px;
-  border: 0;
-  overflow: hidden; /*  Disable scrollbars */
-  display: block;  /* No floating content on sides */
-  background-color: black;
-  color: white;
-  font-family: Arial, Helvetica, sans-serif;
-}
-
-canvas {
-  left:0px;
-  top:0px;
-}
\ No newline at end of file
--- a/Applications/Samples/rt-viewer-demo/start-serving-files.sh	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-#!/bin/bash
-
-sudo nginx -p $(pwd) -c nginx.local.conf
-
-echo "Please browse to :"
-
-echo "http://localhost:9977/rt-viewer-demo.html?ct-series=a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa&dose-instance=830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb&struct-instance=54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9"
-
-echo "(This requires you have uploaded the correct files to your local Orthanc instance)"
--- a/Applications/Samples/rt-viewer-demo/stop-serving-files.sh	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,4 +0,0 @@
-#!/bin/bash
-
-sudo nginx -s stop
-
--- a/Applications/Samples/rt-viewer-demo/tsconfig-samples.json	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-{
-    "extends" : "../../../Platforms/Wasm/tsconfig-stone.json",
-    "compilerOptions": {
-        "sourceMap": false,
-        "lib" : [
-            "es2017",
-            "dom",
-            "dom.iterable"
-        ]
-    }
-}
--- a/Applications/Samples/tsconfig-stone.json	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,7 +0,0 @@
-{
-    "include" : [
-        "../../Platforms/Wasm/stone-framework-loader.ts",
-        "../../Platforms/Wasm/wasm-application-runner.ts",
-        "../../Platforms/Wasm/wasm-viewport.ts"
-    ]
-}
--- a/Applications/Sdl/SdlCairoSurface.h	Mon Apr 20 18:26:32 2020 +0200
+++ b/Applications/Sdl/SdlCairoSurface.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Applications/Sdl/SdlEngine.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Applications/Sdl/SdlEngine.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Applications/Sdl/SdlOrthancSurface.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Applications/Sdl/SdlStoneApplicationRunner.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Applications/Sdl/SdlStoneApplicationRunner.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Applications/StoneApplicationContext.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Applications/StoneApplicationContext.h	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,429 @@
+## 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`)
+  - 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");
+```
+
+# Notes
+
+- It is NOT possible to abandon the existing loaders : they contain too much loader-specific getters
+
+
+
--- a/Framework/Deprecated/Layers/DicomSeriesVolumeSlicer.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Deprecated/Layers/DicomSeriesVolumeSlicer.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Deprecated/Layers/DicomSeriesVolumeSlicer.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Deprecated/Layers/DicomStructureSetSlicer.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Deprecated/Layers/DicomStructureSetSlicer.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Deprecated/Layers/IVolumeSlicer.h	Wed Apr 22 14:05:47 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/DicomStructureSetLoader2.cpp	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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/Messages/LockingEmitter.cpp	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Deprecated/SmartLoader.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Deprecated/SmartLoader.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Deprecated/Toolbox/BaseWebService.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Deprecated/Toolbox/BaseWebService.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Deprecated/Toolbox/IDelayedCallExecutor.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Deprecated/Toolbox/IWebService.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Deprecated/Toolbox/MessagingToolbox.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Deprecated/Toolbox/OrthancApiClient.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Deprecated/Toolbox/OrthancApiClient.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Deprecated/Toolbox/OrthancSlicesLoader.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Deprecated/Toolbox/OrthancSlicesLoader.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Deprecated/Viewport/IViewport.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Deprecated/Viewport/WidgetViewport.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Deprecated/Viewport/WidgetViewport.h	Wed Apr 22 14:05:47 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();
 
--- a/Framework/Deprecated/Volumes/ISlicedVolume.h	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Deprecated/Volumes/ISlicedVolume.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Deprecated/Volumes/IVolumeLoader.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Deprecated/Volumes/StructureSetLoader.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Deprecated/Volumes/StructureSetLoader.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Deprecated/Widgets/LayoutWidget.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Deprecated/Widgets/LayoutWidget.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Deprecated/Widgets/SliceViewerWidget.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Deprecated/Widgets/SliceViewerWidget.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Fonts/GlyphTextureAlphabet.cpp	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Loaders/DicomStructureSetLoader.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -29,11 +29,6 @@
 
 #include <algorithm>
 
-#if 0
-bool logbgo233 = false;
-bool logbgo115 = false;
-#endif
-
 namespace OrthancStone
 {
 
@@ -67,7 +62,7 @@
     {
     }
 
-    virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
+    virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message)
     {
       Json::Value tags;
       message.ParseJsonBody(tags);
@@ -78,10 +73,12 @@
       DicomStructureSetLoader& loader = GetLoader<DicomStructureSetLoader>();
 
       loader.content_->AddReferencedSlice(dicom);
-
       loader.countProcessedInstances_ ++;
       assert(loader.countProcessedInstances_ <= loader.countReferencedInstances_);
 
+      loader.revision_++;
+      loader.SetStructuresUpdated();
+
       if (loader.countProcessedInstances_ == loader.countReferencedInstances_)
       {
         // All the referenced instances have been loaded, finalize the RT-STRUCT
@@ -107,7 +104,7 @@
     {
     }
 
-    virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
+    virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message)
     {
 #if 0
       LOG(TRACE) << "DicomStructureSetLoader::LookupInstance::Handle() (SUCCESS)";
@@ -127,7 +124,7 @@
       {
         std::stringstream msg;
         msg << "Unknown resource! message.GetAnswer() = " << message.GetAnswer() << " message.GetAnswerHeaders() = ";
-        for (OrthancRestApiCommand::HttpHeaders::const_iterator it = message.GetAnswerHeaders().begin();
+        for (OrthancStone::OrthancRestApiCommand::HttpHeaders::const_iterator it = message.GetAnswerHeaders().begin();
              it != message.GetAnswerHeaders().end(); ++it)
         {
           msg << "\nkey: \"" << it->first << "\" value: \"" << it->second << "\"\n";
@@ -140,11 +137,11 @@
       const std::string instanceId = lookup[0]["ID"].asString();
 
       {
-        std::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+        std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand);
         command->SetHttpHeader("Accept-Encoding", "gzip");
         std::string uri = "/instances/" + instanceId + "/tags";
         command->SetUri(uri);
-        command->SetPayload(new AddReferencedInstance(loader, instanceId));
+        command->AcquirePayload(new AddReferencedInstance(loader, instanceId));
         Schedule(command.release());
       }
     }
@@ -159,17 +156,13 @@
     {
     }
     
-    virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
+    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 DicomStructureSet(dicom));
+        loader.content_.reset(new OrthancStone::DicomStructureSet(dicom));
         size_t structureCount = loader.content_->GetStructuresCount();
         loader.structureVisibility_.resize(structureCount);
         bool everythingVisible = false;
@@ -227,11 +220,11 @@
       for (std::set<std::string>::const_iterator
         it = nonEmptyInstances.begin(); it != nonEmptyInstances.end(); ++it)
       {
-        std::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+        std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand);
         command->SetUri("/tools/lookup");
         command->SetMethod(Orthanc::HttpMethod_Post);
         command->SetBody(*it);
-        command->SetPayload(new LookupInstance(loader, *it));
+        command->AcquirePayload(new LookupInstance(loader, *it));
         Schedule(command.release());
       }
     }
@@ -241,7 +234,7 @@
   class DicomStructureSetLoader::Slice : public IExtractedSlice
   {
   private:
-    const DicomStructureSet&  content_;
+    const OrthancStone::DicomStructureSet&  content_;
     uint64_t                  revision_;
     bool                      isValid_;
     std::vector<bool>         visibility_;
@@ -257,9 +250,9 @@
     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,
+    Slice(const OrthancStone::DicomStructureSet& content,
           uint64_t revision,
-          const CoordinateSystem3D& cuttingPlane,
+          const OrthancStone::CoordinateSystem3D& cuttingPlane,
           std::vector<bool> visibility = std::vector<bool>()) 
       : content_(content)
       , revision_(revision)
@@ -270,11 +263,11 @@
 
       bool opposite;
 
-      const Vector normal = content.GetNormal();
+      const OrthancStone::Vector normal = content.GetNormal();
       isValid_ = (
-        GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetNormal()) ||
-        GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisX()) ||
-        GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisY()));
+        OrthancStone::GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetNormal()) ||
+        OrthancStone::GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisX()) ||
+        OrthancStone::GeometryToolbox::IsParallelOrOpposite(opposite, normal, cuttingPlane.GetAxisY()));
     }
       
     virtual bool IsValid()
@@ -287,22 +280,23 @@
       return revision_;
     }
 
-    virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator,
-                                          const CoordinateSystem3D& cuttingPlane)
+    virtual OrthancStone::ISceneLayer* CreateSceneLayer(
+      const OrthancStone::ILayerStyleConfigurator* configurator,
+      const OrthancStone::CoordinateSystem3D& cuttingPlane)
     {
       assert(isValid_);
 
-      std::unique_ptr<PolylineSceneLayer> layer(new PolylineSceneLayer);
+      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 Color& color = content_.GetStructureColor(i);
+          const OrthancStone::Color& color = content_.GetStructureColor(i);
 
 #ifdef USE_BOOST_UNION_FOR_POLYGONS 
-          std::vector< std::vector<Point2D> > polygons;
+          std::vector< std::vector<OrthancStone::Point2D> > polygons;
 
           if (content_.ProjectStructure(polygons, i, cuttingPlane))
           {
@@ -320,17 +314,17 @@
   }
         }
 #else
-          std::vector< std::pair<Point2D, Point2D> > segments;
+          std::vector< std::pair<OrthancStone::Point2D, OrthancStone::Point2D> > segments;
 
           if (content_.ProjectStructure(segments, i, cuttingPlane))
           {
             for (size_t j = 0; j < segments.size(); j++)
             {
-              PolylineSceneLayer::Chain chain;
+              OrthancStone::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);
+              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);
             }
@@ -344,18 +338,28 @@
   };
     
 
-  DicomStructureSetLoader::DicomStructureSetLoader(IOracle& oracle,
-                                                   IObservable& oracleObservable) :
-    LoaderStateMachine(oracle, oracleObservable),
-    IObservable(oracleObservable.GetBroker()),
-    revision_(0),
-    countProcessedInstances_(0),
-    countReferencedInstances_(0),
-    structuresReady_(false)
+  DicomStructureSetLoader::DicomStructureSetLoader(
+    OrthancStone::ILoadersContext& loadersContext) 
+    : LoaderStateMachine(loadersContext)
+    , loadersContext_(loadersContext)
+    , revision_(0)
+    , countProcessedInstances_(0)
+    , countReferencedInstances_(0)
+    , structuresReady_(false)
   {
   }
+   
     
-    
+  boost::shared_ptr<OrthancStone::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;
@@ -377,24 +381,24 @@
     initiallyVisibleStructures_ = initiallyVisibleStructures;
 
     {
-      std::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+      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->SetPayload(new LoadStructure(*this));
+      command->AcquirePayload(new LoadStructure(*this));
       Schedule(command.release());
     }
   }
 
 
-  IVolumeSlicer::IExtractedSlice* DicomStructureSetLoader::ExtractSlice(const CoordinateSystem3D& cuttingPlane)
+  OrthancStone::IVolumeSlicer::IExtractedSlice* DicomStructureSetLoader::ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane)
   {
     if (content_.get() == NULL)
     {
       // Geometry is not available yet
-      return new IVolumeSlicer::InvalidSlice;
+      return new OrthancStone::IVolumeSlicer::InvalidSlice;
     }
     else
     {
@@ -402,6 +406,11 @@
     }
   }
 
+  void DicomStructureSetLoader::SetStructuresUpdated()
+  {
+    BroadcastMessage(DicomStructureSetLoader::StructuresUpdated(*this));
+  }
+
   void DicomStructureSetLoader::SetStructuresReady()
   {
     ORTHANC_ASSERT(!structuresReady_);
--- a/Framework/Loaders/DicomStructureSetLoader.h	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Loaders/DicomStructureSetLoader.h	Wed Apr 22 14:05:47 2020 +0200
@@ -23,6 +23,7 @@
 
 #include "../Toolbox/DicomStructureSet.h"
 #include "../Volumes/IVolumeSlicer.h"
+#include "../Loaders/ILoadersContext.h"
 #include "LoaderStateMachine.h"
 
 #include <vector>
@@ -31,8 +32,8 @@
 {
   class DicomStructureSetLoader :
     public LoaderStateMachine,
-    public IVolumeSlicer,
-    public IObservable
+    public OrthancStone::IVolumeSlicer,
+    public OrthancStone::IObservable
   {
   private:
     class Slice;
@@ -42,14 +43,15 @@
     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_;  
+    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_;
+    bool                                              structuresReady_;
 
     /**
     At load time, these strings are used to initialize the structureVisibility_ 
@@ -68,13 +70,17 @@
     */
     std::vector<bool>                  structureVisibility_;
 
+  protected:
+    DicomStructureSetLoader(OrthancStone::ILoadersContext& loadersContext);
+
   public:
     ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, StructuresReady, DicomStructureSetLoader);
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, StructuresUpdated, DicomStructureSetLoader);
 
-    DicomStructureSetLoader(IOracle& oracle,
-                            IObservable& oracleObservable);    
-    
-    DicomStructureSet* GetContent()
+    static boost::shared_ptr<DicomStructureSetLoader> Create(
+      OrthancStone::ILoadersContext& loadersContext);
+
+    OrthancStone::DicomStructureSet* GetContent()
     {
       return content_.get();
     }
@@ -91,9 +97,10 @@
     void LoadInstance(const std::string& instanceId, 
                       const std::vector<std::string>& initiallyVisibleStructures = std::vector<std::string>());
 
-    virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE;
+    virtual IExtractedSlice* ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE;
 
     void SetStructuresReady();
+    void SetStructuresUpdated();
 
     bool AreStructuresReady() const;
   };
--- a/Framework/Loaders/DicomStructureSetLoader2.cpp	Mon Apr 20 18:26:32 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	Mon Apr 20 18:26:32 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	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Loaders/LoaderCache.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -25,11 +25,7 @@
 #include "OrthancMultiframeVolumeLoader.h"
 #include "DicomStructureSetLoader.h"
 
-#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2
-#include "DicomStructureSetLoader2.h"
-#endif 
- //BGO_ENABLE_DICOMSTRUCTURESETLOADER2
-
+#include "../Loaders/ILoadersContext.h"
 
 #if ORTHANC_ENABLE_WASM == 1
 # include <unistd.h>
@@ -38,41 +34,21 @@
 # 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)
+  LoaderCache::LoaderCache(OrthancStone::ILoadersContext& loadersContext)
+    : loadersContext_(loadersContext)
   {
 
   }
-#else
-  LoaderCache::LoaderCache(ThreadedOracle& oracle, LockingEmitter& lockingEmitter)
-    : oracle_(oracle)
-    , lockingEmitter_(lockingEmitter)
-  {
-  }
-#endif
 
-  boost::shared_ptr<OrthancStone::OrthancSeriesVolumeProgressiveLoader> 
+  boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> 
     LoaderCache::GetSeriesVolumeProgressiveLoader(std::string seriesUuid)
   {
     try
@@ -85,26 +61,15 @@
       // 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);
+        std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock());
+
+        boost::shared_ptr<OrthancStone::DicomVolumeImage> volumeImage(new OrthancStone::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";
-        }
+      
+        // 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
@@ -149,7 +114,7 @@
     return multiframeVolumeLoaders_[instanceUuid];
   }
 
-  boost::shared_ptr<DicomVolumeImageMPRSlicer> LoaderCache::GetMultiframeDicomVolumeImageMPRSlicer(std::string instanceUuid)
+  boost::shared_ptr<OrthancStone::DicomVolumeImageMPRSlicer> LoaderCache::GetMultiframeDicomVolumeImageMPRSlicer(std::string instanceUuid)
   {
     try
     {
@@ -160,22 +125,15 @@
       // find in cache
       if (dicomVolumeImageMPRSlicers_.find(instanceUuid) == dicomVolumeImageMPRSlicers_.end())
       {
-        boost::shared_ptr<DicomVolumeImage> volumeImage(new DicomVolumeImage);
+        std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock());
+        boost::shared_ptr<OrthancStone::DicomVolumeImage> volumeImage(new OrthancStone::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 = OrthancMultiframeVolumeLoader::Create(loadersContext_, volumeImage);
           loader->LoadInstance(instanceUuid);
         }
         multiframeVolumeLoaders_[instanceUuid] = loader;
-        boost::shared_ptr<DicomVolumeImageMPRSlicer> mprSlicer(new DicomVolumeImageMPRSlicer(volumeImage));
+        boost::shared_ptr<OrthancStone::DicomVolumeImageMPRSlicer> mprSlicer(new OrthancStone::DicomVolumeImageMPRSlicer(volumeImage));
         dicomVolumeImageMPRSlicers_[instanceUuid] = mprSlicer;
       }
       return dicomVolumeImageMPRSlicers_[instanceUuid];
@@ -204,23 +162,6 @@
     }
   }
   
-#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
@@ -264,15 +205,11 @@
       // find in cache
       if (dicomStructureSetLoaders_.find(entryKey) == dicomStructureSetLoaders_.end())
       {
+        std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock());
+
         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 = DicomStructureSetLoader::Create(loadersContext_);
           loader->LoadInstance(inInstanceUuid, initiallyVisibleStructures);
         }
         dicomStructureSetLoaders_[entryKey] = loader;
@@ -303,88 +240,19 @@
     }
   }
 
-#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
+    std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock());
     
-//#ifndef NDEBUG
+#ifndef NDEBUG
     // ISO way of checking for debug builds
     DebugDisplayObjRefCounts();
-//#endif
+#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(
@@ -406,10 +274,5 @@
     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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Loaders/LoaderCache.h	Wed Apr 22 14:05:47 2020 +0200
@@ -20,6 +20,11 @@
 
 #pragma once
 
+#include "../Volumes/DicomVolumeImageMPRSlicer.h"
+#include "OrthancSeriesVolumeProgressiveLoader.h"
+#include "OrthancMultiframeVolumeLoader.h"
+#include "DicomStructureSetLoader.h"
+
 #include <boost/shared_ptr.hpp>
 
 #include <map>
@@ -28,37 +33,20 @@
 
 namespace OrthancStone
 {
-  class OrthancSeriesVolumeProgressiveLoader;
-  class DicomVolumeImageMPRSlicer;
-  class DicomStructureSetLoader;
-#ifdef BGO_ENABLE_DICOMSTRUCTURESETLOADER2
-  class DicomStructureSetLoader2;
-  class DicomStructureSetSlicer2;
-  class DicomStructureSet2;
-#endif 
-  //BGO_ENABLE_DICOMSTRUCTURESETLOADER2
-  class OrthancMultiframeVolumeLoader;
+  class ILoadersContext;
+}
 
-#if ORTHANC_ENABLE_WASM == 1
-  class WebAssemblyOracle;
-#else
-  class ThreadedOracle;
-  class LockingEmitter;
-#endif
-
+namespace OrthancStone
+{
   class LoaderCache
   {
   public:
-#if ORTHANC_ENABLE_WASM == 1
-    LoaderCache(WebAssemblyOracle& oracle);
-#else
-    LoaderCache(ThreadedOracle& oracle, LockingEmitter& lockingEmitter);
-#endif
+    LoaderCache(OrthancStone::ILoadersContext& loadersContext);
 
     boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader>
       GetSeriesVolumeProgressiveLoader      (std::string seriesUuid);
     
-    boost::shared_ptr<DicomVolumeImageMPRSlicer>
+    boost::shared_ptr<OrthancStone::DicomVolumeImageMPRSlicer>
       GetMultiframeDicomVolumeImageMPRSlicer(std::string instanceUuid);
 
     boost::shared_ptr<OrthancMultiframeVolumeLoader>
@@ -69,44 +57,22 @@
         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
+
+    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<DicomVolumeImageMPRSlicer> >
+    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
   };
 }
 
--- a/Framework/Loaders/LoaderStateMachine.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Loaders/LoaderStateMachine.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -21,33 +21,35 @@
 
 #include "LoaderStateMachine.h"
 
+#include "../Loaders/ILoadersContext.h"
+
 #include <Core/OrthancException.h>
 
 namespace OrthancStone
 {
-  void LoaderStateMachine::State::Handle(const OrthancRestApiCommand::SuccessMessage& message)
+  void LoaderStateMachine::State::Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message)
   {
     throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
   }
       
 
-  void LoaderStateMachine::State::Handle(const GetOrthancImageCommand::SuccessMessage& message)
+  void LoaderStateMachine::State::Handle(const OrthancStone::GetOrthancImageCommand::SuccessMessage& message)
   {
     throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
   }
 
       
-  void LoaderStateMachine::State::Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message)
+  void LoaderStateMachine::State::Handle(const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message)
   {
     throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
   }
 
 
-  void LoaderStateMachine::Schedule(OracleCommandWithPayload* command)
+  void LoaderStateMachine::Schedule(OrthancStone::OracleCommandBase* command)
   {
     LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::Schedule()";
 
-    std::unique_ptr<OracleCommandWithPayload> protection(command);
+    std::unique_ptr<OrthancStone::OracleCommandBase> protection(command);
 
     if (command == NULL)
     {
@@ -90,14 +92,18 @@
         activeCommands_ < simultaneousDownloads_)
     {
 
-      IOracleCommand* nextCommand = pendingCommands_.front();
+      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;
 
-      oracle_.Schedule(*this, nextCommand);
+      {
+        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_++;
@@ -123,9 +129,9 @@
 
     pendingCommands_.clear();
   }
-
+  
 
-  void LoaderStateMachine::HandleExceptionMessage(const OracleCommandExceptionMessage& message)
+  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: " <<
@@ -136,7 +142,6 @@
   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_;
     }
@@ -157,37 +162,40 @@
   }
 
 
-  LoaderStateMachine::LoaderStateMachine(IOracle& oracle,
-                                         IObservable& oracleObservable) :
-    IObserver(oracleObservable.GetBroker()),
-    oracle_(oracle),
-    oracleObservable_(oracleObservable),
-    active_(false),
-    simultaneousDownloads_(4),
-    activeCommands_(0)
+  LoaderStateMachine::LoaderStateMachine(
+    OrthancStone::ILoadersContext& loadersContext)
+    : loadersContext_(loadersContext)
+    , active_(false)
+    , simultaneousDownloads_(4)
+    , activeCommands_(0)
   {
-    LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::LoaderStateMachine()";
+    using OrthancStone::ILoadersContext;
+
+    LOG(TRACE) 
+      << "LoaderStateMachine(" << std::hex << this 
+      << std::dec << ")::LoaderStateMachine()";
+  }
 
-    oracleObservable.RegisterObserverCallback(
-      new Callable<LoaderStateMachine, OrthancRestApiCommand::SuccessMessage>
-      (*this, &LoaderStateMachine::HandleSuccessMessage));
+  void LoaderStateMachine::PostConstructor()
+  {
+    std::unique_ptr<OrthancStone::ILoadersContext::ILock>
+      lock(loadersContext_.Lock());
 
-    oracleObservable.RegisterObserverCallback(
-      new Callable<LoaderStateMachine, GetOrthancImageCommand::SuccessMessage>
-      (*this, &LoaderStateMachine::HandleSuccessMessage));
+    OrthancStone::IObservable& observable = lock->GetOracleObservable();
 
-    oracleObservable.RegisterObserverCallback(
-      new Callable<LoaderStateMachine, GetOrthancWebViewerJpegCommand::SuccessMessage>
-      (*this, &LoaderStateMachine::HandleSuccessMessage));
-
-    oracleObservable.RegisterObserverCallback(
-      new Callable<LoaderStateMachine, OracleCommandExceptionMessage>
-      (*this, &LoaderStateMachine::HandleExceptionMessage));
+    // 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()
   {
-    oracleObservable_.Unregister(this);
     LOG(TRACE) << "LoaderStateMachine(" << std::hex << this << std::dec << ")::~LoaderStateMachine()";
     Clear();
   }
--- a/Framework/Loaders/LoaderStateMachine.h	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Loaders/LoaderStateMachine.h	Wed Apr 22 14:05:47 2020 +0200
@@ -22,7 +22,7 @@
 #pragma once
 
 #include "../Messages/IObservable.h"
-#include "../Messages/IObserver.h"
+#include "../Messages/ObserverBase.h"
 #include "../Oracle/GetOrthancImageCommand.h"
 #include "../Oracle/GetOrthancWebViewerJpegCommand.h"
 #include "../Oracle/IOracle.h"
@@ -35,15 +35,23 @@
 
 namespace OrthancStone
 {
+  class ILoadersContext;
+
   /**
      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 IObserver
+
+  class LoaderStateMachine : public OrthancStone::ObserverBase<LoaderStateMachine>
   {
-  protected:
+  public:
     class State : public Orthanc::IDynamicObject
     {
     private:
@@ -60,7 +68,7 @@
       {
       }
 
-      void Schedule(OracleCommandWithPayload* command) const
+      void Schedule(OrthancStone::OracleCommandBase* command) const
       {
         that_.Schedule(command);
       }
@@ -71,14 +79,14 @@
         return dynamic_cast<T&>(that_);
       }
       
-      virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message);
+      virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message);
       
-      virtual void Handle(const GetOrthancImageCommand::SuccessMessage& message);
+      virtual void Handle(const OrthancStone::GetOrthancImageCommand::SuccessMessage& message);
       
-      virtual void Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message);
+      virtual void Handle(const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message);
     };
 
-    void Schedule(OracleCommandWithPayload* command);
+    void Schedule(OrthancStone::OracleCommandBase* command);
 
     void Start();
 
@@ -87,23 +95,24 @@
 
     void Clear();
 
-    void HandleExceptionMessage(const OracleCommandExceptionMessage& message);
+    void HandleExceptionMessage(const OrthancStone::OracleCommandExceptionMessage& message);
 
     template <typename T>
     void HandleSuccessMessage(const T& message);
 
-    typedef std::list<IOracleCommand*>  PendingCommands;
+    typedef std::list<OrthancStone::IOracleCommand*>  PendingCommands;
 
-    IOracle&         oracle_;
-    IObservable&     oracleObservable_;
-    bool             active_;
-    unsigned int     simultaneousDownloads_;
-    PendingCommands  pendingCommands_;
-    unsigned int     activeCommands_;
+    OrthancStone::ILoadersContext&  loadersContext_;
+    bool                            active_;
+    unsigned int                    simultaneousDownloads_;
+    PendingCommands                 pendingCommands_;
+    unsigned int                    activeCommands_;
+
 
   public:
-    LoaderStateMachine(IOracle& oracle,
-                       IObservable& oracleObservable);
+    LoaderStateMachine(OrthancStone::ILoadersContext& loadersContext);
+
+    void PostConstructor();
 
     virtual ~LoaderStateMachine();
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/OracleScheduler.cpp	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Loaders/OrthancMultiframeVolumeLoader.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -44,7 +44,7 @@
 
     }
 
-    virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
+    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());
@@ -78,7 +78,7 @@
     {
     }
       
-    virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
+    virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message)
     {
       OrthancMultiframeVolumeLoader& loader = GetLoader<OrthancMultiframeVolumeLoader>();
         
@@ -93,15 +93,15 @@
       std::unique_ptr<Orthanc::DicomMap> dicom(new Orthanc::DicomMap);
       dicom->FromDicomAsJson(body);
 
-      if (StringToSopClassUid(GetSopClassUid(*dicom)) == SopClassUid_RTDose)
+      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<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+        std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand);
         command->SetUri("/instances/" + loader.GetInstanceId() + "/content/" +
                         Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR.Format());
-        command->SetPayload(new LoadRTDoseGeometry(loader, dicom.release()));
+        command->AcquirePayload(new LoadRTDoseGeometry(loader, dicom.release()));
 
         Schedule(command.release());
       }
@@ -120,7 +120,7 @@
     {
     }
       
-    virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
+    virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message)
     {
       GetLoader<OrthancMultiframeVolumeLoader>().SetTransferSyntax(message.GetAnswer());
     }
@@ -134,7 +134,7 @@
     {
     }
       
-    virtual void Handle(const OrthancRestApiCommand::SuccessMessage& message)
+    virtual void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message)
     {
       GetLoader<OrthancMultiframeVolumeLoader>().SetUncompressedPixelData(message.GetAnswer());
     }
@@ -171,11 +171,11 @@
         transferSyntaxUid_ == "1.2.840.10008.1.2.1" ||
         transferSyntaxUid_ == "1.2.840.10008.1.2.2")
     {
-      std::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+      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->SetPayload(new LoadUncompressedPixelData(*this));
+      command->AcquirePayload(new LoadUncompressedPixelData(*this));
       Schedule(command.release());
     }
     else
@@ -194,7 +194,7 @@
 
   void OrthancMultiframeVolumeLoader::SetGeometry(const Orthanc::DicomMap& dicom)
   {
-    DicomInstanceParameters parameters(dicom);
+    OrthancStone::DicomInstanceParameters parameters(dicom);
     volume_->SetDicomParameters(parameters);
       
     Orthanc::PixelFormat format;
@@ -206,7 +206,7 @@
     double spacingZ;
     switch (parameters.GetSopClassUid())
     {
-      case SopClassUid_RTDose:
+      case OrthancStone::SopClassUid_RTDose:
         spacingZ = parameters.GetThickness();
         break;
 
@@ -221,7 +221,7 @@
     const unsigned int depth = parameters.GetImageInformation().GetNumberOfFrames();
 
     {
-      VolumeImageGeometry geometry;
+      OrthancStone::VolumeImageGeometry geometry;
       geometry.SetSizeInVoxels(width, height, depth);
       geometry.SetAxialGeometry(parameters.GetGeometry());
       geometry.SetVoxelDimensions(parameters.GetPixelSpacingX(),
@@ -235,7 +235,7 @@
 
 
 
-    BroadcastMessage(DicomVolumeImage::GeometryReadyMessage(*volume_));
+    BroadcastMessage(OrthancStone::DicomVolumeImage::GeometryReadyMessage(*volume_));
   }
 
 
@@ -266,7 +266,7 @@
   void OrthancMultiframeVolumeLoader::CopyPixelDataAndComputeDistribution(
     const std::string& pixelData, std::map<T,uint64_t>& distribution)
   {
-    ImageBuffer3D& target = volume_->GetPixelData();
+    OrthancStone::ImageBuffer3D& target = volume_->GetPixelData();
       
     const unsigned int bpp = target.GetBytesPerPixel();
     const unsigned int width = target.GetWidth();
@@ -308,11 +308,11 @@
 
       for (unsigned int z = 0; z < depth; z++)
       {
-        ImageBuffer3D::SliceWriter writer(target, VolumeProjection_Axial, z);
+        OrthancStone::ImageBuffer3D::SliceWriter writer(target, OrthancStone::VolumeProjection_Axial, z);
 
         assert(writer.GetAccessor().GetWidth() == width &&
           writer.GetAccessor().GetHeight() == height);
-
+#if 0
         for (unsigned int y = 0; y < height; y++)
         {
           assert(sizeof(T) == Orthanc::GetBytesPerPixel(target.GetFormat()));
@@ -329,6 +329,28 @@
             source += bpp;
           }
         }
+#else
+        // optimized version (fixed) as of 2020-04-15
+        unsigned int pitch = writer.GetAccessor().GetPitch();
+        T* targetAddrLine = reinterpret_cast<T*>(writer.GetAccessor().GetRow(0));
+        assert(sizeof(T) == Orthanc::GetBytesPerPixel(target.GetFormat()));
+
+        for (unsigned int y = 0; y < height; y++)
+        {
+          T* targetAddrPix = targetAddrLine;
+          for (unsigned int x = 0; x < width; x++)
+          {
+            CopyPixel(*targetAddrPix, source);
+
+            distribution[*targetAddrPix] += 1;
+
+            targetAddrPix++;
+            source += bpp;
+          }
+          uint8_t* targetAddrLineBytes = reinterpret_cast<uint8_t*>(targetAddrLine) + pitch;
+          targetAddrLine = reinterpret_cast<T*>(targetAddrLineBytes);
+        }
+#endif
       }
     }
   }
@@ -343,7 +365,7 @@
     }
     else
     {
-      ImageBuffer3D& target = volume_->GetPixelData();
+      OrthancStone::ImageBuffer3D& target = volume_->GetPixelData();
 
       const uint64_t width = target.GetWidth();
       const uint64_t height = target.GetHeight();
@@ -494,7 +516,7 @@
     volume_->IncrementRevision();
 
     pixelDataLoaded_ = true;
-    BroadcastMessage(DicomVolumeImage::ContentUpdatedMessage(*volume_));
+    BroadcastMessage(OrthancStone::DicomVolumeImage::ContentUpdatedMessage(*volume_));
   }
   
   bool OrthancMultiframeVolumeLoader::HasGeometry() const
@@ -508,19 +530,17 @@
   }
 
   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)
+    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)
     {
@@ -528,12 +548,27 @@
     }
   }
 
+
+  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
   {
@@ -563,17 +598,17 @@
     instanceId_ = instanceId;
 
     {
-      std::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+      std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand);
       command->SetHttpHeader("Accept-Encoding", "gzip");
       command->SetUri("/instances/" + instanceId + "/tags");
-      command->SetPayload(new LoadGeometry(*this));
+      command->AcquirePayload(new LoadGeometry(*this));
       Schedule(command.release());
     }
 
     {
-      std::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+      std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand);
       command->SetUri("/instances/" + instanceId + "/metadata/TransferSyntax");
-      command->SetPayload(new LoadTransferSyntax(*this));
+      command->AcquirePayload(new LoadTransferSyntax(*this));
       Schedule(command.release());
     }
   }
--- a/Framework/Loaders/OrthancMultiframeVolumeLoader.h	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Loaders/OrthancMultiframeVolumeLoader.h	Wed Apr 22 14:05:47 2020 +0200
@@ -23,6 +23,7 @@
 
 #include "LoaderStateMachine.h"
 #include "../Volumes/DicomVolumeImage.h"
+#include "../Volumes/IGeometryProvider.h"
 
 #include <boost/shared_ptr.hpp>
 
@@ -30,7 +31,7 @@
 {
   class OrthancMultiframeVolumeLoader :
     public LoaderStateMachine,
-    public IObservable,
+    public OrthancStone::IObservable,
     public IGeometryProvider
   {
   private:
@@ -39,7 +40,7 @@
     class LoadTransferSyntax;    
     class LoadUncompressedPixelData;
 
-    boost::shared_ptr<DicomVolumeImage>  volume_;
+    boost::shared_ptr<OrthancStone::DicomVolumeImage>  volume_;
     std::string                          instanceId_;
     std::string                          transferSyntaxUid_;
     bool                                 pixelDataLoaded_;
@@ -89,15 +90,21 @@
 
     void SetUncompressedPixelData(const std::string& pixelData);
 
-    virtual bool HasGeometry() const ORTHANC_OVERRIDE;
-    virtual const VolumeImageGeometry& GetImageGeometry() const ORTHANC_OVERRIDE;
+    bool HasGeometry() const;
+    const OrthancStone::VolumeImageGeometry& GetImageGeometry() const;
 
+  protected:
+    OrthancMultiframeVolumeLoader(
+      OrthancStone::ILoadersContext& loadersContext,
+      boost::shared_ptr<OrthancStone::DicomVolumeImage> volume,
+      float outliersHalfRejectionRate);
   public:
-    OrthancMultiframeVolumeLoader(boost::shared_ptr<DicomVolumeImage> volume,
-                                  IOracle& oracle,
-                                  IObservable& oracleObservable,
-                                  float outliersHalfRejectionRate = 0.0005);
-    
+
+    static boost::shared_ptr<OrthancMultiframeVolumeLoader> Create(
+      OrthancStone::ILoadersContext& loadersContext,
+      boost::shared_ptr<OrthancStone::DicomVolumeImage> volume,
+      float outliersHalfRejectionRate = 0.0005);
+
     virtual ~OrthancMultiframeVolumeLoader();
 
     bool IsPixelDataLoaded() const
--- a/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -21,30 +21,35 @@
 
 #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 "BasicFetchingItemsSorter.h"
-#include "BasicFetchingStrategy.h"
 
 #include <Core/Images/ImageProcessing.h>
 #include <Core/OrthancException.h>
 
+
 namespace OrthancStone
 {
-  class OrthancSeriesVolumeProgressiveLoader::ExtractedSlice : public DicomVolumeImageMPRSlicer::Slice
+  using OrthancStone::ILoadersContext;
+
+  class OrthancSeriesVolumeProgressiveLoader::ExtractedSlice : public OrthancStone::DicomVolumeImageMPRSlicer::Slice
   {
   private:
     const OrthancSeriesVolumeProgressiveLoader&  that_;
 
   public:
     ExtractedSlice(const OrthancSeriesVolumeProgressiveLoader& that,
-                   const CoordinateSystem3D& plane) :
-      DicomVolumeImageMPRSlicer::Slice(*that.volume_, plane),
+                   const OrthancStone::CoordinateSystem3D& plane) :
+      OrthancStone::DicomVolumeImageMPRSlicer::Slice(*that.volume_, plane),
       that_(that)
     {
       if (IsValid())
       {
-        if (GetProjection() == VolumeProjection_Axial)
+        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,
@@ -54,22 +59,20 @@
         }
       
         if (that_.strategy_.get() != NULL &&
-            GetProjection() == VolumeProjection_Axial)
+            GetProjection() == OrthancStone::VolumeProjection_Axial)
         {
           that_.strategy_->SetCurrent(GetSliceIndex());
         }
       }
     }
   };
-
     
-    
-  void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSlice(size_t index,
-                                                                        const DicomInstanceParameters& reference) const
+  void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSlice(
+    size_t index, const OrthancStone::DicomInstanceParameters& reference) const
   {
-    const DicomInstanceParameters& slice = *slices_[index];
+    const OrthancStone::DicomInstanceParameters& slice = *slices_[index];
       
-    if (!GeometryToolbox::IsParallel(
+    if (!OrthancStone::GeometryToolbox::IsParallel(
           reference.GetGeometry().GetNormal(),
           slice.GetGeometry().GetNormal()))
     {
@@ -90,8 +93,8 @@
                                       "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()))
+    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");
@@ -113,7 +116,7 @@
 
     if (slices_.size() != 0)
     {
-      const DicomInstanceParameters& reference = *slices_[0];
+      const OrthancStone::DicomInstanceParameters& reference = *slices_[0];
 
       for (size_t i = 1; i < slices_.size(); i++)
       {
@@ -157,7 +160,7 @@
 
   // WARNING: The payload of "slices" must be of class "DicomInstanceParameters"
   // (called with the slices created in LoadGeometry)
-  void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::ComputeGeometry(SlicesSorter& slices)
+  void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::ComputeGeometry(OrthancStone::SlicesSorter& slices)
   {
     Clear();
       
@@ -169,7 +172,7 @@
 
     if (slices.GetSlicesCount() == 0)
     {
-      geometry_.reset(new VolumeImageGeometry);
+      geometry_.reset(new OrthancStone::VolumeImageGeometry);
     }
     else
     {
@@ -178,30 +181,39 @@
 
       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));
+        const OrthancStone::DicomInstanceParameters& slice =
+          dynamic_cast<const OrthancStone::DicomInstanceParameters&>(slices.GetSlicePayload(i));
+        slices_.push_back(new OrthancStone::DicomInstanceParameters(slice));
       }
 
       CheckVolume();
 
-      const double spacingZ = slices.ComputeSpacingBetweenSlices();
-      LOG(INFO) << "Computed spacing between slices: " << spacingZ << "mm";
+      double spacingZ;
+
+      if (slices.ComputeSpacingBetweenSlices(spacingZ))
+      {
+        LOG(TRACE) << "Computed spacing between slices: " << spacingZ << "mm";
       
-      const DicomInstanceParameters& parameters = *slices_[0];
+        const OrthancStone::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);
+        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 VolumeImageGeometry& OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetImageGeometry() const
+  const OrthancStone::VolumeImageGeometry& OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetImageGeometry() const
   {
     if (!HasGeometry())
     {
@@ -216,7 +228,7 @@
   }
 
 
-  const DicomInstanceParameters& OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetSliceParameters(size_t index) const
+  const OrthancStone::DicomInstanceParameters& OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetSliceParameters(size_t index) const
   {
     CheckSliceIndex(index);
     return *slices_[index];
@@ -237,8 +249,9 @@
   }
 
 
-  static unsigned int GetSliceIndexPayload(const OracleCommandWithPayload& command)
+  static unsigned int GetSliceIndexPayload(const OrthancStone::OracleCommandBase& command)
   {
+    assert(command.HasPayload());
     return dynamic_cast< const Orthanc::SingleValueObject<unsigned int>& >(command.GetPayload()).GetValue();
   }
 
@@ -247,13 +260,17 @@
   {
     assert(strategy_.get() != NULL);
       
-    unsigned int sliceIndex, quality;
+    unsigned int sliceIndex = 0, quality = 0;
       
     if (strategy_->GetNext(sliceIndex, quality))
     {
-      assert(quality <= BEST_QUALITY);
+      if (!progressiveQuality_)
+      {
+        ORTHANC_ASSERT(quality == QUALITY_00, "INTERNAL ERROR. quality != QUALITY_00 in "
+                       << "OrthancSeriesVolumeProgressiveLoader::ScheduleNextSliceDownload");
+      }
 
-      const DicomInstanceParameters& slice = seriesGeometry_.GetSliceParameters(sliceIndex);
+      const OrthancStone::DicomInstanceParameters& slice = seriesGeometry_.GetSliceParameters(sliceIndex);
           
       const std::string& instance = slice.GetOrthancInstanceIdentifier();
       if (instance.empty())
@@ -261,11 +278,11 @@
         throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
       }
 
-      std::unique_ptr<OracleCommandWithPayload> command;
+      std::unique_ptr<OrthancStone::OracleCommandBase> command;
         
-      if (quality == BEST_QUALITY)
+      if (!progressiveQuality_ || quality == QUALITY_02)
       {
-        std::unique_ptr<GetOrthancImageCommand> tmp(new GetOrthancImageCommand);
+        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. 
@@ -277,22 +294,36 @@
         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
+      else // progressive mode is true AND quality is not final (different from QUALITY_02
       {
-        std::unique_ptr<GetOrthancWebViewerJpegCommand> tmp(new GetOrthancWebViewerJpegCommand);
+        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));
+        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->SetPayload(new Orthanc::SingleValueObject<unsigned int>(sliceIndex));
-      oracle_.Schedule(*this, command.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
     {
@@ -305,7 +336,7 @@
 /**
    This is called in response to GET "/series/XXXXXXXXXXXXX/instances-tags"
 */
-  void OrthancSeriesVolumeProgressiveLoader::LoadGeometry(const OrthancRestApiCommand::SuccessMessage& message)
+  void OrthancSeriesVolumeProgressiveLoader::LoadGeometry(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message)
   {
     Json::Value body;
     message.ParseJsonBody(body);
@@ -318,18 +349,18 @@
     {
       Json::Value::Members instances = body.getMemberNames();
 
-      SlicesSorter slices;
+      OrthancStone::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));
+        std::unique_ptr<OrthancStone::DicomInstanceParameters> instance(new OrthancStone::DicomInstanceParameters(dicom));
         instance->SetOrthancInstanceIdentifier(instances[i]);
 
         // the 3D plane corresponding to the slice
-        CoordinateSystem3D geometry = instance->GetGeometry();
+        OrthancStone::CoordinateSystem3D geometry = instance->GetGeometry();
         slices.AddSlice(geometry, instance.release());
       }
 
@@ -344,14 +375,22 @@
     }
     else
     {
-      const DicomInstanceParameters& parameters = seriesGeometry_.GetSliceParameters(0);
+      const OrthancStone::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));
-        
+      // 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++)
       {
@@ -361,7 +400,7 @@
 
     slicesQuality_.resize(slicesCount, 0);
 
-    BroadcastMessage(DicomVolumeImage::GeometryReadyMessage(*volume_));
+    BroadcastMessage(OrthancStone::DicomVolumeImage::GeometryReadyMessage(*volume_));
   }
 
 
@@ -369,13 +408,22 @@
                                                              const Orthanc::ImageAccessor& image,
                                                              unsigned int quality)
   {
-    assert(sliceIndex < slicesQuality_.size() &&
+    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])
     {
       {
-        ImageBuffer3D::SliceWriter writer(volume_->GetPixelData(), VolumeProjection_Axial, sliceIndex);
+        OrthancStone::ImageBuffer3D::SliceWriter writer(volume_->GetPixelData(), 
+                                                        OrthancStone::VolumeProjection_Axial, 
+                                                        sliceIndex);
+        
         Orthanc::ImageProcessing::Copy(writer.GetAccessor(), image);
       }
 
@@ -383,31 +431,42 @@
       seriesGeometry_.IncrementSliceRevision(sliceIndex);
       slicesQuality_[sliceIndex] = quality;
 
-      BroadcastMessage(DicomVolumeImage::ContentUpdatedMessage(*volume_));
+      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;
 
-  void OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent(const GetOrthancImageCommand::SuccessMessage& message)
-  {
-    SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), BEST_QUALITY);
+    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!");
 
-  void OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent(const GetOrthancWebViewerJpegCommand::SuccessMessage& message)
-  {
+    LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent";
     unsigned int quality;
       
-    switch (message.GetOrigin().GetQuality())
+    switch (dynamic_cast<const OrthancStone::GetOrthancWebViewerJpegCommand&>(message.GetOrigin()).GetQuality())
     {
       case 50:
-        quality = LOW_QUALITY;
+        quality = QUALITY_00;
         break;
 
       case 90:
-        quality = MIDDLE_QUALITY;
+        quality = QUALITY_01;
         break;
 
       default:
@@ -417,36 +476,50 @@
     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)
+  {
+  }
 
-  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)
+  boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> 
+    OrthancSeriesVolumeProgressiveLoader::Create(
+      OrthancStone::ILoadersContext& loadersContext,
+      boost::shared_ptr<OrthancStone::DicomVolumeImage> volume,
+      bool progressiveQuality)
   {
-    oracleObservable.RegisterObserverCallback(
-      new Callable<OrthancSeriesVolumeProgressiveLoader, OrthancRestApiCommand::SuccessMessage>
-      (*this, &OrthancSeriesVolumeProgressiveLoader::LoadGeometry));
+    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);
 
-    oracleObservable.RegisterObserverCallback(
-      new Callable<OrthancSeriesVolumeProgressiveLoader, GetOrthancImageCommand::SuccessMessage>
-      (*this, &OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent));
+    obj->Register<OrthancStone::GetOrthancImageCommand::SuccessMessage>(
+      lock->GetOracleObservable(),
+      &OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent);
 
-    oracleObservable.RegisterObserverCallback(
-      new Callable<OrthancSeriesVolumeProgressiveLoader, GetOrthancWebViewerJpegCommand::SuccessMessage>
-      (*this, &OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent));
+    obj->Register<OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage>(
+      lock->GetOracleObservable(),
+      &OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent);
+
+    return obj;
   }
 
+
   OrthancSeriesVolumeProgressiveLoader::~OrthancSeriesVolumeProgressiveLoader()
   {
-    oracleObservable_.Unregister(this);
     LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::~OrthancSeriesVolumeProgressiveLoader()";
   }
 
@@ -470,10 +543,8 @@
 
   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);
     }
@@ -481,18 +552,19 @@
     {
       active_ = true;
 
-      std::unique_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+      std::unique_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::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";
+      {
+        std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(loadersContext_.Lock());
+        boost::shared_ptr<IObserver> observer(GetSharedObserver());
+        lock->Schedule(observer, 0, command.release()); //TODO: priority!
+      }
     }
   }
   
 
-  IVolumeSlicer::IExtractedSlice* 
-  OrthancSeriesVolumeProgressiveLoader::ExtractSlice(const CoordinateSystem3D& cuttingPlane)
+  OrthancStone::IVolumeSlicer::IExtractedSlice* 
+  OrthancSeriesVolumeProgressiveLoader::ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane)
   {
     if (volume_->HasGeometry())
     {
--- a/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h	Wed Apr 22 14:05:47 2020 +0200
@@ -21,8 +21,10 @@
 
 #pragma once
 
+#include "../Loaders/IFetchingItemsSorter.h"
+#include "../Loaders/IFetchingStrategy.h"
 #include "../Messages/IObservable.h"
-#include "../Messages/IObserver.h"
+#include "../Messages/ObserverBase.h"
 #include "../Oracle/GetOrthancImageCommand.h"
 #include "../Oracle/GetOrthancWebViewerJpegCommand.h"
 #include "../Oracle/IOracle.h"
@@ -30,28 +32,30 @@
 #include "../Toolbox/SlicesSorter.h"
 #include "../Volumes/DicomVolumeImage.h"
 #include "../Volumes/IVolumeSlicer.h"
-#include "IFetchingItemsSorter.h"
-#include "IFetchingStrategy.h"
+
+#include "../Volumes/IGeometryProvider.h"
+
 
 #include <boost/shared_ptr.hpp>
 
 namespace OrthancStone
 {
+  class ILoadersContext;
   /**
     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 OrthancStone::ObserverBase<OrthancSeriesVolumeProgressiveLoader>,
+    public OrthancStone::IObservable,
+    public OrthancStone::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;
-    
+    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 */
@@ -59,7 +63,7 @@
     {
     private:
       void CheckSlice(size_t index,
-                      const DicomInstanceParameters& reference) const;
+                      const OrthancStone::DicomInstanceParameters& reference) const;
     
       void CheckVolume() const;
 
@@ -67,8 +71,8 @@
 
       void CheckSliceIndex(size_t index) const;
 
-      std::unique_ptr<VolumeImageGeometry>     geometry_;
-      std::vector<DicomInstanceParameters*>  slices_;
+      std::unique_ptr<OrthancStone::VolumeImageGeometry>     geometry_;
+      std::vector<OrthancStone::DicomInstanceParameters*>  slices_;
       std::vector<uint64_t>                  slicesRevision_;
 
     public:
@@ -77,16 +81,16 @@
         Clear();
       }
 
-      void ComputeGeometry(SlicesSorter& slices);
+      void ComputeGeometry(OrthancStone::SlicesSorter& slices);
 
       virtual bool HasGeometry() const
       {
         return geometry_.get() != NULL;
       }
 
-      virtual const VolumeImageGeometry& GetImageGeometry() const;
+      virtual const OrthancStone::VolumeImageGeometry& GetImageGeometry() const;
 
-      const DicomInstanceParameters& GetSliceParameters(size_t index) const;
+      const OrthancStone::DicomInstanceParameters& GetSliceParameters(size_t index) const;
 
       uint64_t GetSliceRevision(size_t index) const;
 
@@ -95,35 +99,43 @@
 
     void ScheduleNextSliceDownload();
 
-    void LoadGeometry(const OrthancRestApiCommand::SuccessMessage& message);
+    void LoadGeometry(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message);
 
     void SetSliceContent(unsigned int sliceIndex,
                          const Orthanc::ImageAccessor& image,
                          unsigned int quality);
 
-    void LoadBestQualitySliceContent(const GetOrthancImageCommand::SuccessMessage& message);
+    void LoadBestQualitySliceContent(const OrthancStone::GetOrthancImageCommand::SuccessMessage& message);
 
-    void LoadJpegSliceContent(const GetOrthancWebViewerJpegCommand::SuccessMessage& message);
+    void LoadJpegSliceContent(const OrthancStone::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_;
-
-
+    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);
 
-
-    OrthancSeriesVolumeProgressiveLoader(const boost::shared_ptr<DicomVolumeImage>& volume,
-                                         IOracle& oracle,
-                                         IObservable& oracleObservable);
+    /**
+    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();
 
@@ -149,7 +161,7 @@
     /**
     Same remark as HasGeometry
     */
-    const VolumeImageGeometry& GetImageGeometry() const ORTHANC_OVERRIDE
+    const OrthancStone::VolumeImageGeometry& GetImageGeometry() const ORTHANC_OVERRIDE
     {
       return seriesGeometry_.GetImageGeometry();
     }
@@ -160,6 +172,6 @@
     take into account this request (this is done in the ExtractedSlice ctor)
     */
     virtual IExtractedSlice*
-      ExtractSlice(const CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE;
+      ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE;
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/SeriesFramesLoader.cpp	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Messages/ICallable.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Messages/IMessage.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Messages/IMessageEmitter.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Messages/IObservable.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Messages/IObservable.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Messages/IObserver.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 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	Mon Apr 20 18:26:32 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	Mon Apr 20 18:26:32 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	Mon Apr 20 18:26:32 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	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/OpenGL/OpenGLIncludes.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/OpenGL/SdlOpenGLContext.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/OpenGL/SdlOpenGLContext.h	Wed Apr 22 14:05:47 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();
+    }
   };
 }
 
--- a/Framework/OpenGL/WebAssemblyOpenGLContext.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/OpenGL/WebAssemblyOpenGLContext.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -37,26 +37,27 @@
     class WebAssemblyOpenGLContext::PImpl
     {
     private:
-      std::string                     canvas_;
+      std::string                     canvasSelector_;
       EMSCRIPTEN_WEBGL_CONTEXT_HANDLE context_;
       unsigned int                    canvasWidth_;
       unsigned int                    canvasHeight_;
       bool                            isContextLost_;
 
     public:
-      PImpl(const std::string& canvas)
-        : canvas_(canvas)
+      PImpl(const std::string& canvasSelector)
+        : canvasSelector_(canvasSelector)
         , isContextLost_(false)
       {
         // Context configuration
         EmscriptenWebGLContextAttributes attr; 
         emscripten_webgl_init_context_attributes(&attr);
 
-        context_ = emscripten_webgl_create_context(canvas.c_str(), &attr);
+        context_ = emscripten_webgl_create_context(canvasSelector.c_str(), &attr);
         if (context_ == 0)
         {
-          std::string message("Cannot create an OpenGL context for canvas: ");
-          message += canvas;
+          std::string message("Cannot create an OpenGL context for the element with the following CSS selector: \"");
+          message += canvasSelector;
+          message += "\"  Please make sure the -s DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1 flag has been passed to Emscripten when building.";
           LOG(ERROR) << message;
           throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, message);
         }
@@ -113,9 +114,9 @@
         }
       }
 
-      const std::string& GetCanvasIdentifier() const
+      const std::string& GetCanvasSelector() const
       {
-        return canvas_;
+        return canvasSelector_;
       }
 
       void MakeCurrent()
@@ -128,7 +129,7 @@
 
         if (emscripten_is_webgl_context_lost(context_))
         {
-          LOG(ERROR) << "OpenGL context has been lost for canvas: " << canvas_;
+          LOG(ERROR) << "OpenGL context has been lost for canvas selector: " << canvasSelector_;
           SetLostContext();
           throw StoneException(ErrorCode_WebGLContextLost);
         }
@@ -166,7 +167,7 @@
       void UpdateSize()
       {
         double w, h;
-        emscripten_get_element_css_size(canvas_.c_str(), &w, &h);
+        emscripten_get_element_css_size(canvasSelector_.c_str(), &w, &h);
 
         /**
          * Emscripten has the function emscripten_get_element_css_size()
@@ -190,59 +191,16 @@
           canvasHeight_ = static_cast<unsigned int>(boost::math::iround(h));
         }
         
-        emscripten_set_canvas_element_size(canvas_.c_str(), canvasWidth_, canvasHeight_);
+        emscripten_set_canvas_element_size(canvasSelector_.c_str(), canvasWidth_, canvasHeight_);
       }
     };
 
 
-    WebAssemblyOpenGLContext::WebAssemblyOpenGLContext(const std::string& canvas) :
-      pimpl_(new PImpl(canvas))
+    WebAssemblyOpenGLContext::WebAssemblyOpenGLContext(const std::string& canvasSelector) :
+      pimpl_(new PImpl(canvasSelector))
     {
     }
 
-    //bool WebAssemblyOpenGLContext::TryRecreate()
-    //{
-    //  // LOG(ERROR) << "WebAssemblyOpenGLContext::TryRecreate() trying to recreate context";
-    //  try
-    //  {
-    //    std::string canvasId = GetCanvasIdentifier();
-    //    pimpl_.reset(new PImpl(canvasId));
-
-    //    // no exception does not mean the context is fully 
-    //    // functional! Most probably, if we have >= than 16 
-    //    // contexts, context wil remain lost for some time
-    //    bool lost = IsContextLost();
-    //    if (lost) {
-    //      // LOG(ERROR) << "WebAssemblyOpenGLContext::TryRecreate() context is still lost!";
-    //      return false;
-    //    } else {
-    //      return true;
-    //    }
-    //  }
-    //  catch (const Orthanc::OrthancException& e)
-    //  {
-    //    if (e.HasDetails())
-    //    {
-    //      LOG(ERROR) << "OrthancException in WebAssemblyOpenGLContext::TryRecreate: " << e.What() << " Details: " << e.GetDetails();
-    //    }
-    //    else
-    //    {
-    //      LOG(ERROR) << "OrthancException in WebAssemblyOpenGLContext::TryRecreate: " << e.What();
-    //    }
-    //    return false;
-    //  }
-    //  catch (const std::exception& e)
-    //  {
-    //    LOG(ERROR) << "std::exception in WebAssemblyOpenGLContext::TryRecreate: " << e.what();
-    //    return false;
-    //  }
-    //  catch (...)
-    //  {
-    //    LOG(ERROR) << "Unknown exception WebAssemblyOpenGLContext::in TryRecreate";
-    //    return false;
-    //  }
-    //}
-
     bool WebAssemblyOpenGLContext::IsContextLost()
     {
       return pimpl_->IsContextLost();
@@ -288,10 +246,10 @@
       pimpl_->UpdateSize();
     }
 
-    const std::string& WebAssemblyOpenGLContext::GetCanvasIdentifier() const
+    const std::string& WebAssemblyOpenGLContext::GetCanvasSelector() const
     {
       assert(pimpl_.get() != NULL);
-      return pimpl_->GetCanvasIdentifier();
+      return pimpl_->GetCanvasSelector();
     }
   }
 }
--- a/Framework/OpenGL/WebAssemblyOpenGLContext.h	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/OpenGL/WebAssemblyOpenGLContext.h	Wed Apr 22 14:05:47 2020 +0200
@@ -54,7 +54,7 @@
       boost::shared_ptr<PImpl>  pimpl_;
 
     public:
-      WebAssemblyOpenGLContext(const std::string& canvas);
+      WebAssemblyOpenGLContext(const std::string& canvasSelector);
 
       virtual bool IsContextLost() ORTHANC_OVERRIDE;
 
@@ -73,7 +73,7 @@
 
       void UpdateSize();
 
-      const std::string& GetCanvasIdentifier() const;
+      const std::string& GetCanvasSelector() const;
 
 
       /**
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Oracle/GenericOracleRunner.cpp	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Oracle/GetOrthancImageCommand.cpp	Wed Apr 22 14:05:47 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(); 
@@ -115,7 +125,16 @@
 
       case Orthanc::MimeType_Pam:
       {
+#ifdef __EMSCRIPTEN__
+        // "true" means we ask the PamReader to make an extra copy so that
+        // the resulting Orthanc::ImageAccessor is aligned (as malloc is).
+        // Indeed, even though alignment is not required in Web Assembly,
+        // Emscripten seems to check it and bail out if addresses are "odd"
+        image.reset(new Orthanc::PamReader(true));
+#else
+        // potentially unaligned, with is faster and consumes less heap memory
         image.reset(new Orthanc::PamReader);
+#endif
         dynamic_cast<Orthanc::PamReader&>(*image).ReadFromMemory(answer);
         break;
       }
@@ -147,7 +166,24 @@
       }
     }
 
-    SuccessMessage message(*this, image.release(), contentType);
+    //{
+    //  // DEBUG DISPLAY IMAGE PROPERTIES BGO 2020-04-11
+    //  const Orthanc::ImageAccessor& source = *image;
+    //  const void* sourceBuffer = source.GetConstBuffer();
+    //  intptr_t sourceBufferInt = reinterpret_cast<intptr_t>(sourceBuffer);
+    //  int sourceWidth = source.GetWidth();
+    //  int sourceHeight = source.GetHeight();
+    //  int sourcePitch = source.GetPitch();
+
+    //  // TODO: turn error into trace below
+    //  LOG(ERROR) << "GetOrthancImageCommand::ProcessHttpAnswer | source:"
+    //    << " W = " << sourceWidth << " H = " << sourceHeight
+    //    << " P = " << sourcePitch << " B = " << sourceBufferInt
+    //    << " B % 4 == " << sourceBufferInt % 4;
+    //}
+
+
+    SuccessMessage message(*this, *image, contentType);
     emitter.EmitMessage(receiver, message);
   }
 }
--- a/Framework/Oracle/GetOrthancImageCommand.h	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Oracle/GetOrthancImageCommand.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Oracle/GetOrthancWebViewerJpegCommand.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Oracle/GetOrthancWebViewerJpegCommand.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Oracle/HttpCommand.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Oracle/HttpCommand.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Oracle/IOracle.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Oracle/IOracleCommand.h	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Oracle/OracleCommandExceptionMessage.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 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	Mon Apr 20 18:26:32 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Oracle/OrthancRestApiCommand.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Oracle/OrthancRestApiCommand.h	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Oracle/SleepOracleCommand.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Oracle/ThreadedOracle.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Oracle/ThreadedOracle.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Oracle/WebAssemblyOracle.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Oracle/WebAssemblyOracle.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Radiography/RadiographyAlphaLayer.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Radiography/RadiographyDicomLayer.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Radiography/RadiographyDicomLayer.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Radiography/RadiographyLayer.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Radiography/RadiographyLayer.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Radiography/RadiographyMaskLayer.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Radiography/RadiographyScene.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -150,7 +150,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;
   }
@@ -170,9 +170,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
@@ -328,7 +327,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)
     {
@@ -385,7 +384,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)
     {
@@ -398,7 +397,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)
     {
@@ -415,7 +414,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);
 
@@ -437,7 +436,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)
@@ -451,8 +450,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()));
     }
 
@@ -470,8 +469,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()));
     }
 
@@ -481,7 +480,7 @@
 
   RadiographyLayer& RadiographyScene::LoadDicomWebFrame(Deprecated::IWebService& web)
   {
-    RadiographyLayer& layer = RegisterLayer(new RadiographyDicomLayer(IObservable::GetBroker(), *this));
+    RadiographyLayer& layer = RegisterLayer(new RadiographyDicomLayer(*this));
 
 
     return layer;
@@ -857,8 +856,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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Radiography/RadiographyScene.h	Wed Apr 22 14:05:47 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"
@@ -35,8 +36,8 @@
   class RadiographyDicomLayer;
 
   class RadiographyScene :
-      public IObserver,
-      public IObservable
+    public ObserverBase<RadiographyScene>,
+    public IObservable
   {
     friend class RadiographySceneGeometryReader;
   public:
@@ -193,7 +194,7 @@
     virtual void OnLayerEdited(const RadiographyLayer::LayerEditedMessage& message);
 
   public:
-    RadiographyScene(MessageBroker& broker);
+    RadiographyScene();
     
     virtual ~RadiographyScene();
 
--- a/Framework/Radiography/RadiographySceneReader.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Radiography/RadiographySceneReader.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Radiography/RadiographySceneReader.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Radiography/RadiographyTextLayer.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Radiography/RadiographyTextLayer.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Radiography/RadiographyWidget.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Radiography/RadiographyWidget.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/CairoCompositor.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/CairoCompositor.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/FloatTextureSceneLayer.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/FloatTextureSceneLayer.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/GrayscaleStyleConfigurator.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/GrayscaleStyleConfigurator.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/ICompositor.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/ISceneLayer.h	Wed Apr 22 14:05:47 2020 +0200
@@ -33,6 +33,7 @@
   public:
     enum Type
     {
+      Type_NullLayer,
       Type_InfoPanel,
       Type_ColorTexture,
       Type_Polyline,
--- a/Framework/Scene2D/InfoPanelSceneLayer.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/InfoPanelSceneLayer.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/InfoPanelSceneLayer.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/Internals/CairoFloatTextureRenderer.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/Internals/CairoInfoPanelRenderer.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/Internals/CairoInfoPanelRenderer.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/Internals/CairoLookupTableTextureRenderer.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/Internals/CompositorHelper.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/Internals/CompositorHelper.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/Internals/FixedPointAligner.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/Internals/FixedPointAligner.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/Internals/OpenGLFloatTextureRenderer.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/Internals/OpenGLInfoPanelRenderer.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/Internals/OpenGLInfoPanelRenderer.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/Internals/OpenGLLookupTableTextureRenderer.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/LookupTableStyleConfigurator.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/LookupTableStyleConfigurator.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/LookupTableTextureSceneLayer.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/LookupTableTextureSceneLayer.h	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/OpenGLCompositor.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/OpenGLCompositor.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/PanSceneTracker.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/PanSceneTracker.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/PointerEvent.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -26,6 +26,7 @@
 namespace OrthancStone
 {
   PointerEvent::PointerEvent() :
+    button_(MouseButton_None),
     hasAltModifier_(false),
     hasControlModifier_(false),
     hasShiftModifier_(false)
--- a/Framework/Scene2D/PointerEvent.h	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/PointerEvent.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/RotateSceneTracker.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/RotateSceneTracker.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/Scene2D.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/Scene2D.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/ScenePoint2D.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/ZoomSceneTracker.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2D/ZoomSceneTracker.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/AngleMeasureTool.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/AngleMeasureTool.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/CreateAngleMeasureCommand.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/CreateAngleMeasureCommand.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/CreateAngleMeasureTracker.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/CreateAngleMeasureTracker.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/CreateLineMeasureCommand.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/CreateLineMeasureCommand.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/CreateLineMeasureTracker.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/CreateLineMeasureTracker.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/EditAngleMeasureCommand.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/EditAngleMeasureCommand.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/EditAngleMeasureTracker.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/EditAngleMeasureTracker.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/EditLineMeasureCommand.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/EditLineMeasureCommand.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/EditLineMeasureTracker.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/EditLineMeasureTracker.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/LayerHolder.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/LayerHolder.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/LineMeasureTool.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/LineMeasureTool.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/MeasureCommands.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/MeasureCommands.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/MeasureTool.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/MeasureTool.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/MeasureToolsToolbox.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/MeasureTrackers.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/MeasureTrackers.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/OneGesturePointerTracker.cpp	Wed Apr 22 14:05:47 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)
   {
@@ -41,10 +41,10 @@
     // gesture
     ORTHANC_ASSERT(currentTouchCount_ > 0, "Wrong state in tracker");
     currentTouchCount_--;
-    LOG(INFO) << "currentTouchCount_ becomes: " << currentTouchCount_;
+    //LOG(TRACE) << "currentTouchCount_ becomes: " << currentTouchCount_;
     if (currentTouchCount_ == 0)
     {
-      LOG(INFO) << "currentTouchCount_ == 0 --> alive_ = false";
+      //LOG(TRACE) << "currentTouchCount_ == 0 --> alive_ = false";
       alive_ = false;
     }
   }
@@ -54,16 +54,20 @@
     // additional touches are not taken into account but we need to count 
     // the number of active touches
     currentTouchCount_++;
-    LOG(INFO) << "currentTouchCount_ becomes: " << currentTouchCount_;
+    //LOG(TRACE) << "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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/OneGesturePointerTracker.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/PredeclaredTypes.h	Wed Apr 22 14:05:47 2020 +0200
@@ -38,4 +38,5 @@
   class MeasureCommand;
   class ViewportController;
   class LayerHolder;
+  class IViewport;
 }
--- a/Framework/Scene2DViewport/ViewportController.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/ViewportController.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Scene2DViewport/ViewportController.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/StoneEnumerations.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/StoneInitialization.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/StoneInitialization.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Toolbox/CoordinateSystem3D.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Toolbox/CoordinateSystem3D.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Toolbox/DicomInstanceParameters.cpp	Wed Apr 22 14:05:47 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)
     {
@@ -387,6 +387,21 @@
   TextureBaseSceneLayer* DicomInstanceParameters::CreateTexture
   (const Orthanc::ImageAccessor& pixelData) const
   {
+    // {
+    //   const Orthanc::ImageAccessor& source = pixelData;
+    //   const void* sourceBuffer = source.GetConstBuffer();
+    //   intptr_t sourceBufferInt = reinterpret_cast<intptr_t>(sourceBuffer);
+    //   int sourceWidth = source.GetWidth();
+    //   int sourceHeight = source.GetHeight();
+    //   int sourcePitch = source.GetPitch();
+
+    //   // TODO: turn error into trace below
+    //   LOG(ERROR) << "ConvertGrayscaleToFloat | source:"
+    //     << " W = " << sourceWidth << " H = " << sourceHeight
+    //     << " P = " << sourcePitch << " B = " << sourceBufferInt
+    //     << " B % 4 == " << sourceBufferInt % 4;
+    // }
+
     assert(sizeof(float) == 4);
 
     Orthanc::PixelFormat sourceFormat = pixelData.GetFormat();
@@ -468,4 +483,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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Toolbox/DicomInstanceParameters.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Toolbox/DicomStructureSet.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Toolbox/DicomStructureSet.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Toolbox/GenericToolbox.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Toolbox/LinearAlgebra.cpp	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Toolbox/SlicesSorter.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Toolbox/SlicesSorter.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Viewport/IViewport.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Viewport/SdlViewport.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Viewport/SdlViewport.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Viewport/SdlWindow.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Viewport/SdlWindow.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 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	Mon Apr 20 18:26:32 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	Wed Apr 22 14:05:47 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(GetCanvasCssSelector().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);
+      },
+      GetCanvasId().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(GetCanvasCssSelector().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(GetCanvasCssSelector().c_str(), 
+                                       width, 
+                                       height);
+
+    AcquireCompositor(new CairoCompositor(width, height));
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Viewport/WebAssemblyCairoViewport.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Viewport/WebAssemblyViewport.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -21,311 +21,314 @@
 
 #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->GetCanvasCssSelector();      
+
+    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)
+  {
+    WebAssemblyViewport* that = reinterpret_cast<WebAssemblyViewport*>(userData);
+
+    if (that->compositor_.get() != NULL &&
+        that->controller_->HasActiveTracker())
+    {
+      PointerEvent pointer;
+      ConvertMouseEvent(pointer, *mouseEvent, *that->compositor_);
+      if (that->controller_->HandleMouseMove(pointer))
       {
-        LOG(WARNING) << "Context is lost! Compositor will be disabled.";
-        DisableCompositor();
-        // we now need to wait for the "context restored" callback
-      }
-      else
-      {
-        throw;
+        that->Invalidate();
       }
     }
-    catch (...)
-    {
-      // something else nasty happened
-      throw;
-    }
+
+    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()
+  {
+    emscripten_request_animation_frame(OnRequestAnimationFrame, reinterpret_cast<void*>(this));
+  }
+
+  void WebAssemblyViewport::AcquireCompositor(ICompositor* compositor /* takes ownership */)
   {
-    LOG(ERROR) << "WebAssemblyOpenGLViewport::OpenGLContextRestored() for canvas: " << GetCanvasIdentifier();
-    
-    // maybe the context has already been restored by other means (the 
-    // Refresh() function)
-    if (!HasCompositor())
+    if (compositor == NULL)
     {
-      RestoreCompositor();
-      UpdateSize();
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
     }
-    return false;
+    else
+    {
+      compositor_.reset(compositor);
+    }
   }
 
-  void WebAssemblyOpenGLViewport::RegisterContextCallbacks()
-  {
-#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;
+#if DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR == 1
+// everything OK..... we're using the new setting
+#else
+#pragma message("WARNING: DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR is not defined or equal to 0. Stone will use the OLD Emscripten rules for DOM element selection.")
+#endif
 
-    //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)
-    {
-      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());
-    }
-    LOG(TRACE) << "WebAssemblyOpenGLViewport::RegisterContextCallbacks() SUCCESS!!!";
+  WebAssemblyViewport::WebAssemblyViewport(
+    const std::string& canvasId, bool enableEmscriptenMouseEvents) :
+    canvasId_(canvasId),
+#if DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR == 1
+    canvasCssSelector_("#" + canvasId),
+#else
+    canvasCssSelector_(canvasId),
 #endif
-  }
-
-  WebAssemblyCairoViewport::WebAssemblyCairoViewport(const std::string& canvas) :
-    WebAssemblyViewport(canvas),
-    canvas_(canvas),
-    compositor_(GetScene(), 1024, 768)
+    interactor_(new DefaultViewportInteractor),
+    enableEmscriptenMouseEvents_(enableEmscriptenMouseEvents)
   {
   }
 
-  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: " 
+      << canvasId_;
+
+    if (canvasId_.empty() ||
+        canvasId_[0] == '#')
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
+        "The canvas identifier must not start with '#'");
+    }
+
+    // Disable right-click on the canvas (i.e. context menu)
+    EM_ASM({
+        document.getElementById(UTF8ToString($0)).oncontextmenu = 
+        function(event)
+        {
+          event.preventDefault();
+        }
+      },
+      canvasId_.c_str()   // $0
+      );
+
+    // It is not possible to monitor the resizing of individual
+    // canvas, so we track the full window of the browser
+    emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW,
+                                   reinterpret_cast<void*>(this),
+                                   false,
+                                   OnResize);
+
+    if (enableEmscriptenMouseEvents_)
+    {
+
+      // if any of this function causes an error in the console, please
+      // make sure you are using the new (as of 1.39.x) version of 
+      // emscripten element lookup rules( pass 
+      // "-s DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1" to the linker.
+
+      emscripten_set_mousedown_callback(canvasCssSelector_.c_str(),
+                                        reinterpret_cast<void*>(this),
+                                        false,
+                                        OnMouseDown);
+
+      emscripten_set_mousemove_callback(canvasCssSelector_.c_str(),
+                                        reinterpret_cast<void*>(this),
+                                        false,
+                                        OnMouseMove);
+
+      emscripten_set_mouseup_callback(canvasCssSelector_.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);
+    emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW,
+                                   reinterpret_cast<void*>(this),
+                                   false,
+                                   NULL);
+
+    if (enableEmscriptenMouseEvents_)
+    {
+
+      emscripten_set_mousedown_callback(canvasCssSelector_.c_str(),
+                                        reinterpret_cast<void*>(this),
+                                        false,
+                                        OnMouseDown);
+
+      emscripten_set_mousemove_callback(canvasCssSelector_.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(canvasCssSelector_.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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Viewport/WebAssemblyViewport.h	Wed Apr 22 14:05:47 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,102 @@
 
 #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                           canvasId_;
+    std::string                           canvasCssSelector_;
+    std::unique_ptr<ICompositor>          compositor_;
+    std::unique_ptr<ViewportController>   controller_;
+    std::unique_ptr<IViewportInteractor>  interactor_;
+    bool                                  enableEmscriptenMouseEvents_;
 
-  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 enableEmscriptenMouseEvents = 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& GetCanvasId() const
     {
-      return (compositor_.get() != NULL);
-    }
-    
-    bool IsContextLost()
-    {
-      return context_.IsContextLost();
+      return canvasId_;
     }
 
-    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
+    /**
+    emscripten functions requires the css selector for the canvas. This is 
+    different from the canvas id (the syntax is '#mycanvasid')
+    */
+    const std::string& GetCanvasCssSelector() const
     {
-      return true;
-    }
-    
-    virtual ICompositor& GetCompositor() ORTHANC_OVERRIDE
-    {
-      return compositor_;
+      return canvasCssSelector_;
     }
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Viewport/WebGLViewport.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,105 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <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, bool enableEmscriptenMouseEvents) :
+    WebAssemblyViewport(canvasId,enableEmscriptenMouseEvents),
+    context_(GetCanvasCssSelector())
+  {
+    AcquireCompositor(new OpenGLCompositor(context_));
+  }
+
+  boost::shared_ptr<WebGLViewport> WebGLViewport::Create(
+    const std::string& canvasId, bool enableEmscriptenMouseEvents)
+  {
+    boost::shared_ptr<WebGLViewport> that = boost::shared_ptr<WebGLViewport>(
+        new WebGLViewport(canvasId, enableEmscriptenMouseEvents));
+    
+    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	Wed Apr 22 14:05:47 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, bool enableEmscriptenMouseEvents);
+
+  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, bool enableEmscriptenMouseEvents = true);
+
+    virtual ~WebGLViewport();
+
+    bool IsContextLost()
+    {
+      return context_.IsContextLost();
+    } 
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Viewport/WebGLViewportsRegistry.cpp	Wed Apr 22 14:05:47 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	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Volumes/DicomVolumeImage.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Volumes/DicomVolumeImageMPRSlicer.cpp	Wed Apr 22 14:05:47 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();
   }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Volumes/IGeometryProvider.h	Wed Apr 22 14:05:47 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 OrthancStone
+{
+  class IGeometryProvider
+  {
+  public:
+    virtual ~IGeometryProvider() {}
+    virtual bool HasGeometry() const = 0;
+    virtual const OrthancStone::VolumeImageGeometry& GetImageGeometry() const = 0;
+  };
+}
--- a/Framework/Volumes/IVolumeSlicer.cpp~	Mon Apr 20 18:26:32 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~	Mon Apr 20 18:26:32 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Volumes/VolumeImageGeometry.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Volumes/VolumeSceneLayerSource.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Framework/Volumes/VolumeSceneLayerSource.h	Wed Apr 22 14:05:47 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;
 
-    void Update(const CoordinateSystem3D& plane);  
+    /**
+    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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Platforms/Generic/DelayedCallCommand.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Platforms/Generic/DelayedCallCommand.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Platforms/Generic/OracleDelayedCallExecutor.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Platforms/Generic/OracleWebService.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Platforms/Generic/OracleWebService.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Platforms/Generic/WebServiceCommandBase.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Platforms/Generic/WebServiceCommandBase.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Platforms/Generic/WebServiceDeleteCommand.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Platforms/Generic/WebServiceDeleteCommand.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Platforms/Generic/WebServiceGetCommand.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Platforms/Generic/WebServiceGetCommand.h	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Platforms/Generic/WebServicePostCommand.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Platforms/Generic/WebServicePostCommand.h	Wed Apr 22 14:05:47 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/README.md	Mon Apr 20 18:26:32 2020 +0200
+++ b/README.md	Wed Apr 22 14:05:47 2020 +0200
@@ -91,133 +91,11 @@
 http://book.orthanc-server.com/developers/stone.html
 
 Stone of Orthanc comes with several sample applications in the
-`Samples` folder. These samples can be compiled into Web Assembly or
-into native SDL applications.
-
-The following assumes that the source code to be downloaded in
-`~/orthanc-stone` and Orthanc source code to be checked out in
-`~/orthanc`. 
-
-Building the WASM samples
--------------------------------------
-```
-cd ~/orthanc-stone/Applications/Samples
-./build-wasm.sh
-```
-
-Serving the WASM samples
-------------------------------------
-```
-# launch an Orthanc listening on 8042 port:
-Orthanc
-
-# launch an nginx that will serve the WASM static files and reverse
-# proxy
-sudo nginx -p $(pwd) -c nginx.local.conf
-```
-
-You can now open the samples in http://localhost:9977
-
-Building the SDL native samples (SimpleViewer only)
----------------------------------------------------
-
-The following also assumes that you have checked out the Orthanc
-source code in an `orthanc` folder next to the Stone of Orthanc
-repository, please enter the following:
-
-**Simple make generator with dynamic build**
-
-```
-# Please set $currentDir to the current folder
-mkdir -p ~/builds/orthanc-stone-build
-cd ~/builds/orthanc-stone-build
-cmake -DORTHANC_FRAMEWORK_SOURCE=path \
-    -DORTHANC_FRAMEWORK_ROOT=$currentDir/../../../orthanc \
-    -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON \
-    ~/orthanc-stone/Applications/Samples/
-```
-
-**Ninja generator with static SDL build (pwsh script)**
-
-```
-# Please yourself one level above the orthanc-stone and orthanc folders
-if( -not (test-path stone_build_sdl)) { mkdir stone_build_sdl }
-cd stone_build_sdl
-cmake -G Ninja -DSTATIC_BUILD=ON -DOPENSSL_NO_CAPIENG=ON -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT="$($pwd)\..\orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON ../orthanc-stone/Applications/Samples/
-```
-
-**Ninja generator with static SDL build (bash/zsh script)**
-
-```
-# Please yourself one level above the orthanc-stone and orthanc folders
-if( -not (test-path stone_build_sdl)) { mkdir stone_build_sdl }
-cd stone_build_sdl
-cmake -G Ninja -DSTATIC_BUILD=ON -DOPENSSL_NO_CAPIENG=ON -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT="`pwd`/../orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON ../orthanc-stone/Applications/Samples/
-```
+`Samples` folder.
 
-**Visual Studio 2017 generator with static SDL build  (pwsh script)**
-
-```
-# The following will use Visual Studio 2017 to build the SDL samples
-# in debug mode (with multiple compilers in parallel). NOTE: place 
-# yourself one level above the `orthanc-stone` and `orthanc` folders
-
-if( -not (test-path stone_build_sdl)) { mkdir stone_build_sdl }
-cd stone_build_sdl
-cmake -G "Visual Studio 15 2017 Win64" -DMSVC_MULTIPLE_PROCESSES=ON -DSTATIC_BUILD=ON -DOPENSSL_NO_CAPIENG=ON -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT="$($pwd)\..\orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON ../orthanc-stone/Applications/Samples/
-cmake --build . --config Debug
-```
-
-If you are working on Windows, add the correct generator option to
-cmake to, for instance, generate msbuild files for Visual Studio.
-
-Then, under Linux:
-```
-cmake --build . --target OrthancStoneSimpleViewer -- -j 5
-```
-
-Note: replace `$($pwd)` with the current directory when not using Powershell
-
-Building the Qt native samples (SimpleViewer only) under Windows:
-------------------------------------------------------------------
-
-**Visual Studio 2017 generator with static Qt build  (pwsh script)**
-
-For instance, if Qt is installed in `C:\Qt\5.12.0\msvc2017_64`
+Please see the Samples/README.md file for information on the samples building
+and running procedure.
 
-```
-# The following will use Visual Studio 2017 to build the SDL samples
-# in debug mode (with multiple compilers in parallel). NOTE: place 
-# yourself one level above the `orthanc-stone` and `orthanc` folders
-
-if( -not (test-path stone_build_qt)) { mkdir stone_build_qt }
-cd stone_build_qt
-cmake -G "Visual Studio 15 2017 Win64" -DMSVC_MULTIPLE_PROCESSES=ON -DSTATIC_BUILD=ON -DOPENSSL_NO_CAPIENG=ON -DCMAKE_PREFIX_PATH=C:\Qt\5.12.0\msvc2017_64 -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT="$($pwd)\..\orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_QT=ON  ../orthanc-stone/Applications/Samples/
-cmake --build . --config Debug
-```
-
-Note: replace `$($pwd)` with the current directory when not using Powershell
-
-
-
-
-
-
-Building the SDL native samples (SimpleViewer only) under Windows:
-------------------------------------------------------------------
-`cmake -DSTATIC_BUILD=ON -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT="$($pwd)\..\orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON -G "Visual Studio 15 2017 Win64" ../orthanc-stone/Applications/Samples/`
-
-Note: replace `$($pwd)` with the current directory when not using Powershell
-
-Executing the native samples:
---------------------------------
-```
-# launch an Orthanc listening on 8042 port:
-Orthanc
-
-# launch the sample
-./OrthancStoneSimpleViewer --studyId=XX
-``` 
 
 Licensing
 ---------
@@ -245,59 +123,3 @@
   url="https://doi.org/10.1007/s10278-018-0082-y"
 }
 
-Build the Application Samples
------------------------------
-
-**Visual Studio 2008 (v90) **
-
-```
-cmake -G "Visual Studio 9 2008" -DUSE_LEGACY_JSONCPP=ON -DENABLE_OPENGL=ON -DSTATIC_BUILD=ON -DOPENSSL_NO_CAPIENG=ON -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT="$($pwd)\..\orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON ../orthanc-stone/Applications/Samples
-```
-
-**Visual Studio 2019 (v142) **
-
-```
-cmake -G "Visual Studio 16 2019" -A x64 -DMSVC_MULTIPLE_PROCESSES=ON -DENABLE_OPENGL=ON -DSTATIC_BUILD=ON -DOPENSSL_NO_CAPIENG=ON -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT="$($pwd)\..\orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON ../orthanc-stone/Applications/Samples
-```
-
-**Visual Studio 2017 (v140) **
-
-```
-cmake -G "Visual Studio 15 2017 Win64" -DMSVC_MULTIPLE_PROCESSES=ON -DENABLE_OPENGL=ON -DSTATIC_BUILD=ON -DOPENSSL_NO_CAPIENG=ON -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT="$($pwd)\..\orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON ../orthanc-stone/Applications/Samples
-```
-
-
-Build the core Samples
----------------------------
-How to build the newest (2019-04-29) SDL samples under Windows, *inside* a
-folder that is sibling to the orthanc-stone folder: 
-
-**Visual Studio 2019 (v142) **
-
-```
-cmake -G "Visual Studio 16 2019" -A x64 -DMSVC_MULTIPLE_PROCESSES=ON -DENABLE_OPENGL=ON -DSTATIC_BUILD=ON -DOPENSSL_NO_CAPIENG=ON -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT="$($pwd)\..\orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON ../orthanc-stone/Samples/Sdl
-```
-
-**Visual Studio 2017 (v140) **
-
-```
-cmake -G "Visual Studio 15 2017 Win64" -DMSVC_MULTIPLE_PROCESSES=ON -DENABLE_OPENGL=ON -DSTATIC_BUILD=ON -DOPENSSL_NO_CAPIENG=ON -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT="$($pwd)\..\orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON ../orthanc-stone/Samples/Sdl
-```
-
-**Visual Studio 2008 (v90) **
-
-```
-cmake -G "Visual Studio 9 2008" -DUSE_LEGACY_JSONCPP=ON -DENABLE_OPENGL=ON -DSTATIC_BUILD=ON -DOPENSSL_NO_CAPIENG=ON -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT="$($pwd)\..\orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON ../orthanc-stone/Samples/Sdl
-```
-
-And under Ubuntu (note the /mnt/c/osi/dev/orthanc folder):
-```
-cmake -G "Ninja" -DENABLE_OPENGL=ON -DSTATIC_BUILD=OFF -DOPENSSL_NO_CAPIENG=ON -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT="/mnt/c/osi/dev/orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON ../orthanc-stone/Samples/Sdl
-```
-
-TODO trackers:
-- CANCELLED (using outlined text now) text overlay 50% --> ColorTextureLayer 50%
-- DONE angle tracker: draw arcs
-- Handles on arc
-- Select measure tool with hit test --> Delete command
-
--- a/Resources/CMake/OrthancStoneConfiguration.cmake	Mon Apr 20 18:26:32 2020 +0200
+++ b/Resources/CMake/OrthancStoneConfiguration.cmake	Wed Apr 22 14:05:47 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,138 @@
     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_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/DicomStructureSetLoader2.cpp
+    ${ORTHANC_STONE_ROOT}/Framework/Deprecated/Loaders/DicomStructureSetLoader2.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/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,20 +469,39 @@
 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()
 
+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()
+
 
 list(APPEND ORTHANC_STONE_SOURCES
   #${ORTHANC_STONE_ROOT}/Framework/Layers/SeriesFrameRendererFactory.cpp
@@ -441,57 +524,102 @@
   ${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/DicomResourcesLoader.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomSource.cpp
   ${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/DicomVolumeLoader.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/IFetchingItemsSorter.h
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/IFetchingStrategy.h
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoadedDicomResources.cpp
   ${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/OracleScheduler.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h
-  
+  ${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,9 +716,8 @@
   ${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/IGeometryProvider.h
   ${ORTHANC_STONE_ROOT}/Framework/Volumes/IVolumeSlicer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Volumes/IVolumeSlicer.h
   ${ORTHANC_STONE_ROOT}/Framework/Volumes/OrientedVolumeBoundingBox.cpp
@@ -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	Mon Apr 20 18:26:32 2020 +0200
+++ b/Resources/CMake/QtConfiguration.cmake	Wed Apr 22 14:05:47 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)
 
--- a/Resources/CMake/Utilities.cmake	Mon Apr 20 18:26:32 2020 +0200
+++ b/Resources/CMake/Utilities.cmake	Wed Apr 22 14:05:47 2020 +0200
@@ -35,3 +35,50 @@
 endmacro()
 
 Test_GetFilenameFromPath()
+
+macro(SortFilesInSourceGroups)
+  if(FALSE)
+    foreach(source IN LISTS ORTHANC_STONE_SOURCES)
+        # if("${source}" MATCHES ".*/pixman.*\\.c")
+        #   message("pixman source: ${source}")
+        # elseif("${source}" MATCHES ".*/pixman.*\\.c")
+        #   message("pixman header: ${source}")
+        # endif()
+        
+        if("${source}" MATCHES ".*\\.\\./.*")
+          message("source raw: ${source}")
+          #file(TO_CMAKE_PATH ${source} sourceCMakePath)
+          get_filename_component(sourceCMakePath ${source} ABSOLUTE)
+          message("source CMake: ${sourceCMakePath}")
+        endif()
+
+        # returns the containing directory with forward slashes
+        # get_filename_component(source_path "${source}" PATH) 
+
+        # converts / to \
+        # string(REPLACE "/" "\\" source_path_msvc "${source_path}")
+        #source_group("Stone ${source_path_msvc}" FILES "${source}")
+    endforeach()
+  endif()
+
+  source_group("Orthanc Framework\\Sources" REGULAR_EXPRESSION ".*/orthanc/(Core|Plugins)/.*cpp")
+  source_group("Orthanc Framework\\Headers" REGULAR_EXPRESSION ".*/orthanc/(Core|Plugins)/.*hpp")
+  source_group("Orthanc Framework\\Headers" REGULAR_EXPRESSION ".*/orthanc/(Core|Plugins)/.*h")
+
+  source_group("Stone Library\\Sources" REGULAR_EXPRESSION ".*/orthanc-stone/.*cpp")
+  source_group("Stone Library\\Headers" REGULAR_EXPRESSION ".*/orthanc-stone/.*hpp")
+  source_group("Stone Library\\Headers" REGULAR_EXPRESSION ".*/orthanc-stone/.*h")
+
+  source_group("Stone Samples\\Source" REGULAR_EXPRESSION ".*orthanc-stone/Samples/.*\\.cpp")
+  source_group("Stone Samples\\Headers" REGULAR_EXPRESSION ".*orthanc-stone/Samples/.*\\.h")
+
+  source_group("ThirdParty\\cairo" REGULAR_EXPRESSION ".*${CMAKE_BINARY_DIR}/cairo[^/]*/.*")
+  source_group("ThirdParty\\pixman" REGULAR_EXPRESSION ".*${CMAKE_BINARY_DIR}/pixman[^/]*/.*")
+  source_group("ThirdParty\\base64" REGULAR_EXPRESSION ".*ThirdParty/base64.*")
+  source_group("ThirdParty\\SDL2" REGULAR_EXPRESSION ".*${CMAKE_BINARY_DIR}/SDL2.*")
+  source_group("ThirdParty\\glew" REGULAR_EXPRESSION ".*${CMAKE_BINARY_DIR}/glew.*")
+  source_group("AUTOGENERATED" REGULAR_EXPRESSION ".*${CMAKE_BINARY_DIR}/AUTOGENERATED/.*")
+  source_group("ThirdParty\\minizip" REGULAR_EXPRESSION ".*ThirdParty/minizip/.*")
+  source_group("ThirdParty\\md5" REGULAR_EXPRESSION ".*ThirdParty/md5/.*")
+endmacro()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Conventions.txt	Wed Apr 22 14:05:47 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<>".
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/MultiPlatform/BasicScene/BasicScene.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,275 @@
+/**
+ * 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 "BasicScene.h"
+
+// From Stone
+#include "Framework/Scene2D/Scene2D.h"
+#include "Framework/Scene2D/ColorTextureSceneLayer.h"
+#include "Framework/Scene2D/PolylineSceneLayer.h"
+#include "Framework/Scene2D/TextSceneLayer.h"
+
+#include "Framework/Scene2D/PanSceneTracker.h"
+#include "Framework/Scene2D/ZoomSceneTracker.h"
+#include "Framework/Scene2D/RotateSceneTracker.h"
+
+#include "Framework/Scene2D/CairoCompositor.h"
+
+// From Orthanc framework
+#include <Core/Images/Image.h>
+#include <Core/Images/ImageProcessing.h>
+#include <Core/Images/PngWriter.h>
+
+using namespace OrthancStone;
+
+const unsigned int BASIC_SCENE_FONT_SIZE = 32;
+const int BASIC_SCENE_LAYER_POSITION = 150;
+
+void PrepareScene(Scene2D& scene)
+{
+  //Scene2D& scene(*controller->GetScene());
+  // Texture of 2x2 size
+  {
+    Orthanc::Image i(Orthanc::PixelFormat_RGB24, 2, 2, false);
+
+    uint8_t *p = reinterpret_cast<uint8_t*>(i.GetRow(0));
+    p[0] = 255;
+    p[1] = 0;
+    p[2] = 0;
+
+    p[3] = 0;
+    p[4] = 255;
+    p[5] = 0;
+
+    p = reinterpret_cast<uint8_t*>(i.GetRow(1));
+    p[0] = 0;
+    p[1] = 0;
+    p[2] = 255;
+
+    p[3] = 255;
+    p[4] = 0;
+    p[5] = 0;
+
+    scene.SetLayer(12, new ColorTextureSceneLayer(i));
+
+    std::unique_ptr<ColorTextureSceneLayer> l(new ColorTextureSceneLayer(i));
+    l->SetOrigin(-3, 2);
+    l->SetPixelSpacing(1.5, 1);
+    l->SetAngle(20.0 / 180.0 * 3.14);
+    scene.SetLayer(14, l.release());
+  }
+
+  // Texture of 1x1 size
+  {
+    Orthanc::Image i(Orthanc::PixelFormat_RGB24, 1, 1, false);
+
+    uint8_t *p = reinterpret_cast<uint8_t*>(i.GetRow(0));
+    p[0] = 255;
+    p[1] = 0;
+    p[2] = 0;
+
+    std::unique_ptr<ColorTextureSceneLayer> l(new ColorTextureSceneLayer(i));
+    l->SetOrigin(-2, 1);
+    l->SetAngle(20.0 / 180.0 * 3.14);
+    scene.SetLayer(13, l.release());
+  }
+
+  // Some lines
+  {
+    std::unique_ptr<PolylineSceneLayer> layer(new PolylineSceneLayer);
+
+    layer->SetThickness(1);
+
+    PolylineSceneLayer::Chain chain;
+    chain.push_back(ScenePoint2D(0 - 0.5, 0 - 0.5));
+    chain.push_back(ScenePoint2D(0 - 0.5, 2 - 0.5));
+    chain.push_back(ScenePoint2D(2 - 0.5, 2 - 0.5));
+    chain.push_back(ScenePoint2D(2 - 0.5, 0 - 0.5));
+    layer->AddChain(chain, true, 255, 0, 0);
+
+    chain.clear();
+    chain.push_back(ScenePoint2D(-5, -5));
+    chain.push_back(ScenePoint2D(5, -5));
+    chain.push_back(ScenePoint2D(5, 5));
+    chain.push_back(ScenePoint2D(-5, 5));
+    layer->AddChain(chain, true, 0, 255, 0);
+
+    double dy = 1.01;
+    chain.clear();
+    chain.push_back(ScenePoint2D(-4, -4));
+    chain.push_back(ScenePoint2D(4, -4 + dy));
+    chain.push_back(ScenePoint2D(-4, -4 + 2.0 * dy));
+    chain.push_back(ScenePoint2D(4, 2));
+    layer->AddChain(chain, false, 0, 0, 255);
+
+    //    layer->SetColor(0,255, 255);
+    scene.SetLayer(50, layer.release());
+  }
+
+  // Some text
+  {
+    std::unique_ptr<TextSceneLayer> layer(new TextSceneLayer);
+    layer->SetText("Hello");
+    scene.SetLayer(100, layer.release());
+  }
+}
+
+#if ORTHANC_SANDBOXED == 0
+void TakeScreenshot(const std::string& target,
+                    const OrthancStone::Scene2D& scene,
+                    unsigned int canvasWidth,
+                    unsigned int canvasHeight)
+{
+  using namespace OrthancStone;
+  // Take a screenshot, then save it as PNG file
+  CairoCompositor compositor(scene, canvasWidth, canvasHeight);
+  compositor.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, BASIC_SCENE_FONT_SIZE, Orthanc::Encoding_Latin1);
+  compositor.Refresh();
+
+  Orthanc::ImageAccessor canvas;
+  compositor.GetCanvas().GetReadOnlyAccessor(canvas);
+
+  Orthanc::Image png(Orthanc::PixelFormat_RGB24, canvas.GetWidth(), canvas.GetHeight(), false);
+  Orthanc::ImageProcessing::Convert(png, canvas);
+
+  Orthanc::PngWriter writer;
+  writer.WriteToFile(target, png);
+}
+#endif
+
+void ShowCursorInfo(Scene2D& scene, const PointerEvent& pointerEvent)
+{
+  ScenePoint2D p = pointerEvent.GetMainPosition().Apply(scene.GetCanvasToSceneTransform());
+
+  char buf[64];
+  sprintf(buf, "(%0.02f,%0.02f)", p.GetX(), p.GetY());
+
+  if (scene.HasLayer(BASIC_SCENE_LAYER_POSITION))
+  {
+    TextSceneLayer& layer =
+        dynamic_cast<TextSceneLayer&>(scene.GetLayer(BASIC_SCENE_LAYER_POSITION));
+    layer.SetText(buf);
+    layer.SetPosition(p.GetX(), p.GetY());
+  }
+  else
+  {
+    std::unique_ptr<TextSceneLayer>
+        layer(new TextSceneLayer);
+    layer->SetColor(0, 255, 0);
+    layer->SetText(buf);
+    layer->SetBorder(20);
+    layer->SetAnchor(BitmapAnchor_BottomCenter);
+    layer->SetPosition(p.GetX(), p.GetY());
+    scene.SetLayer(BASIC_SCENE_LAYER_POSITION, layer.release());
+  }
+}
+
+
+
+bool BasicScene2DInteractor::OnMouseEvent(const GuiAdapterMouseEvent& event, const PointerEvent& pointerEvent)
+{
+  if (currentTracker_.get() != NULL)
+  {
+    switch (event.type)
+    {
+    case GUIADAPTER_EVENT_MOUSEUP:
+    {
+      currentTracker_->PointerUp(pointerEvent);
+      if (!currentTracker_->IsAlive())
+      {
+        currentTracker_.reset();
+      }
+    };break;
+    case GUIADAPTER_EVENT_MOUSEMOVE:
+    {
+      currentTracker_->PointerMove(pointerEvent);
+    };break;
+    default:
+      return false;
+    }
+    return true;
+  }
+  else if (event.type == GUIADAPTER_EVENT_MOUSEDOWN)
+  {
+    if (event.button == GUIADAPTER_MOUSEBUTTON_LEFT)
+    {
+      currentTracker_.reset(new RotateSceneTracker(viewportController_, pointerEvent));
+    }
+    else if (event.button == GUIADAPTER_MOUSEBUTTON_MIDDLE)
+    {
+      currentTracker_.reset(new PanSceneTracker(viewportController_, pointerEvent));
+    }
+    else if (event.button == GUIADAPTER_MOUSEBUTTON_RIGHT)
+    {
+      currentTracker_.reset(new ZoomSceneTracker(viewportController_, pointerEvent, viewportController_->GetViewport().GetCanvasHeight()));
+    }
+  }
+  else if (event.type == GUIADAPTER_EVENT_MOUSEMOVE)
+  {
+    if (showCursorInfo_)
+    {
+      Scene2D& scene(viewportController_->GetScene());
+      ShowCursorInfo(scene, pointerEvent);
+    }
+    return true;
+  }
+  return false;
+}
+
+bool BasicScene2DInteractor::OnKeyboardEvent(const GuiAdapterKeyboardEvent& guiEvent)
+{
+  if (guiEvent.type == GUIADAPTER_EVENT_KEYDOWN)
+  {
+    switch (guiEvent.sym[0])
+    {
+    case 's':
+    {
+      //viewportController_->FitContent(viewportController_->GetViewport().GetCanvasWidth(), viewportController_->GetViewport().GetCanvasHeight());
+      viewportController_->FitContent();
+      return true;
+    };
+#if ORTHANC_SANDBOXED == 0
+    case 'c':
+    {
+      Scene2D& scene(viewportController_->GetScene());
+      TakeScreenshot("screenshot.png", scene, viewportController_->GetViewport().GetCanvasWidth(), viewportController_->GetViewport().GetCanvasHeight());
+      return true;
+    }
+#endif
+    case 'd':
+    {
+      showCursorInfo_ = !showCursorInfo_;
+      if (!showCursorInfo_)
+      {
+        Scene2D& scene(viewportController_->GetScene());
+        scene.DeleteLayer(BASIC_SCENE_LAYER_POSITION);
+      }
+
+      return true;
+    }
+    }
+  }
+  return false;
+}
+
+bool BasicScene2DInteractor::OnWheelEvent(const GuiAdapterWheelEvent& guiEvent)
+{
+  return false;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/MultiPlatform/BasicScene/BasicScene.h	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,55 @@
+/**
+ * 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 "Framework/Scene2DViewport/ViewportController.h"
+#include "Framework/Scene2D/Scene2D.h"
+
+extern const unsigned int BASIC_SCENE_FONT_SIZE;
+extern const int BASIC_SCENE_LAYER_POSITION;
+
+extern void PrepareScene(OrthancStone::Scene2D& scene);
+extern void TakeScreenshot(const std::string& target,
+                           const OrthancStone::Scene2D& scene,
+                           unsigned int canvasWidth,
+                           unsigned int canvasHeight);
+
+
+#include "Applications/Generic/Scene2DInteractor.h"
+#include "Framework/Scene2DViewport/IFlexiblePointerTracker.h"
+
+
+class BasicScene2DInteractor : public OrthancStone::Scene2DInteractor
+{
+  boost::shared_ptr<OrthancStone::IFlexiblePointerTracker>  currentTracker_;
+  bool                                                      showCursorInfo_;
+public:
+  BasicScene2DInteractor(boost::shared_ptr<OrthancStone::ViewportController> viewportController) :
+    Scene2DInteractor(viewportController),
+    showCursorInfo_(false)
+  {}
+
+  virtual bool OnMouseEvent(const OrthancStone::GuiAdapterMouseEvent& event, const OrthancStone::PointerEvent& pointerEvent) override;
+  virtual bool OnKeyboardEvent(const OrthancStone::GuiAdapterKeyboardEvent& guiEvent) override;
+  virtual bool OnWheelEvent(const OrthancStone::GuiAdapterWheelEvent& guiEvent) override;
+};
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/MultiPlatform/BasicScene/mainQt.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,103 @@
+/**
+ * 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/>.
+ **/
+
+#define GLEW_STATIC 1
+// From Stone
+#include "../../Framework/OpenGL/OpenGLIncludes.h"
+#include "../../Applications/Sdl/SdlWindow.h"
+#include "../../Framework/Scene2D/CairoCompositor.h"
+#include "../../Framework/Scene2D/ColorTextureSceneLayer.h"
+#include "../../Framework/Scene2D/OpenGLCompositor.h"
+#include "../../Framework/Scene2D/PanSceneTracker.h"
+#include "../../Framework/Scene2D/RotateSceneTracker.h"
+#include "../../Framework/Scene2D/Scene2D.h"
+#include "../../Framework/Scene2D/ZoomSceneTracker.h"
+#include "../../Framework/Scene2DViewport/ViewportController.h"
+#include "../../Framework/Scene2DViewport/UndoStack.h"
+
+#include "../../Framework/StoneInitialization.h"
+#include "../../Framework/Messages/MessageBroker.h"
+
+// From Orthanc framework
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+#include <Core/Images/Image.h>
+#include <Core/Images/ImageProcessing.h>
+#include <Core/Images/PngWriter.h>
+
+#include <boost/make_shared.hpp>
+#include <boost/ref.hpp>
+#include "EmbeddedResources.h"
+
+#include <stdio.h>
+#include <QDebug>
+#include <QWindow>
+
+#include "BasicScene.h"
+
+
+using namespace OrthancStone;
+
+
+
+static void GLAPIENTRY OpenGLMessageCallback(GLenum source,
+                                             GLenum type,
+                                             GLuint id,
+                                             GLenum severity,
+                                             GLsizei length,
+                                             const GLchar* message,
+                                             const void* userParam )
+{
+  if (severity != GL_DEBUG_SEVERITY_NOTIFICATION)
+  {
+    fprintf(stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n",
+            ( type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : "" ),
+            type, severity, message );
+  }
+}
+
+extern void InitGL();
+
+#include <QApplication>
+#include "BasicSceneWindow.h"
+
+int main(int argc, char* argv[])
+{
+  QApplication a(argc, argv);
+
+  OrthancStone::Samples::BasicSceneWindow window;
+  window.show();
+  window.GetOpenGlWidget().Init();
+
+  MessageBroker broker;
+  boost::shared_ptr<UndoStack> undoStack(new UndoStack);
+  boost::shared_ptr<ViewportController> controller = boost::make_shared<ViewportController>(undoStack, boost::ref(broker), window.GetOpenGlWidget());
+  PrepareScene(controller->GetScene());
+
+  window.GetOpenGlWidget().GetCompositor().SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT,
+                     BASIC_SCENE_FONT_SIZE, Orthanc::Encoding_Latin1);
+
+  boost::shared_ptr<OrthancStone::Scene2DInteractor> interactor(new BasicScene2DInteractor(controller));
+  window.GetOpenGlWidget().SetInteractor(interactor);
+
+  controller->FitContent();
+
+  return a.exec();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/MultiPlatform/BasicScene/mainSdl.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,199 @@
+/**
+ * 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/>.
+ **/
+
+
+// From Stone
+#include "Framework/Viewport/SdlViewport.h"
+#include "Framework/Scene2D/OpenGLCompositor.h"
+#include "Framework/Scene2DViewport/UndoStack.h"
+#include "Framework/StoneInitialization.h"
+#include "Framework/Messages/MessageBroker.h"
+
+// From Orthanc framework
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+
+#include <boost/make_shared.hpp>
+#include <boost/ref.hpp>
+
+#include <SDL.h>
+#include <stdio.h>
+
+
+#include "BasicScene.h"
+
+using namespace OrthancStone;
+
+boost::shared_ptr<BasicScene2DInteractor> interactor;
+
+void HandleApplicationEvent(boost::shared_ptr<OrthancStone::ViewportController> controller,
+                            const SDL_Event& event)
+{
+  using namespace OrthancStone;
+  Scene2D& scene(controller->GetScene());
+  if (event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP || event.type == SDL_MOUSEMOTION)
+  {
+    // TODO: this code is copy/pasted from GuiAdapter::Run() -> find the right place
+    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 guiEvent;
+    ConvertFromPlatform(guiEvent, ctrlPressed, shiftPressed, altPressed, event);
+    PointerEvent pointerEvent;
+    pointerEvent.AddPosition(controller->GetViewport().GetPixelCenterCoordinates(event.button.x, event.button.y));
+
+    interactor->OnMouseEvent(guiEvent, pointerEvent);
+    return;
+  }
+  else if ((event.type == SDL_KEYDOWN || event.type == SDL_KEYUP) && event.key.repeat == 0  /* Ignore key bounce */)
+  {
+    GuiAdapterKeyboardEvent guiEvent;
+    ConvertFromPlatform(guiEvent, event);
+
+    interactor->OnKeyboardEvent(guiEvent);
+  }
+
+}
+
+
+static void GLAPIENTRY
+OpenGLMessageCallback(GLenum source,
+                      GLenum type,
+                      GLuint id,
+                      GLenum severity,
+                      GLsizei length,
+                      const GLchar* message,
+                      const void* userParam )
+{
+  if (severity != GL_DEBUG_SEVERITY_NOTIFICATION)
+  {
+    fprintf(stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n",
+            ( type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : "" ),
+            type, severity, message );
+  }
+}
+
+
+void Run(boost::shared_ptr<OrthancStone::ViewportController> controller)
+{
+  SdlViewport& sdlViewport = dynamic_cast<SdlViewport&>(controller->GetViewport());
+
+  glEnable(GL_DEBUG_OUTPUT);
+  glDebugMessageCallback(OpenGLMessageCallback, 0);
+
+  controller->GetViewport().GetCompositor().SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT,
+                     BASIC_SCENE_FONT_SIZE, Orthanc::Encoding_Latin1);
+
+  controller->GetViewport().Refresh();
+  controller->FitContent();
+
+
+  bool stop = false;
+  while (!stop)
+  {
+    controller->GetViewport().Refresh();
+
+    SDL_Event event;
+    while (!stop &&
+           SDL_PollEvent(&event))
+    {
+      if (event.type == SDL_QUIT)
+      {
+        stop = true;
+        break;
+      }
+      else if (event.type == SDL_WINDOWEVENT &&
+               event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
+      {
+        sdlViewport.UpdateSize(event.window.data1, event.window.data2);
+      }
+      else if (event.type == SDL_KEYDOWN &&
+               event.key.repeat == 0 /* Ignore key bounce */)
+      {
+        switch (event.key.keysym.sym)
+        {
+          case SDLK_f:
+            sdlViewport.GetWindow().ToggleMaximize();
+            break;
+              
+          case SDLK_q:
+            stop = true;
+            break;
+
+          default:
+            break;
+        }
+      }
+      
+      HandleApplicationEvent(controller, event);
+    }
+
+    SDL_Delay(1);
+  }
+  interactor.reset();
+}
+
+
+
+
+/**
+ * IMPORTANT: The full arguments to "main()" are needed for SDL on
+ * Windows. Otherwise, one gets the linking error "undefined reference
+ * to `SDL_main'". https://wiki.libsdl.org/FAQWindows
+ **/
+int main(int argc, char* argv[])
+{
+  using namespace OrthancStone;
+  StoneInitialize();
+  Orthanc::Logging::EnableInfoLevel(true);
+
+  try
+  {
+    SdlOpenGLViewport viewport("Hello", 1024, 768);
+    MessageBroker broker;
+    boost::shared_ptr<UndoStack> undoStack(new UndoStack);
+    boost::shared_ptr<ViewportController> controller = boost::make_shared<ViewportController>(undoStack, boost::ref(broker), boost::ref(viewport));
+    interactor.reset(new BasicScene2DInteractor(controller));
+    PrepareScene(controller->GetScene());
+    Run(controller);
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    LOG(ERROR) << "EXCEPTION: " << e.What();
+  }
+
+  StoneFinalize();
+
+  return 0;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/Qt/BasicSceneWindow.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,53 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+#include "../../Framework/OpenGL/OpenGLIncludes.h"
+#include "BasicSceneWindow.h"
+
+/**
+ * Don't use "ui_MainWindow.h" instead of <ui_MainWindow.h> below, as
+ * this makes CMake unable to detect when the UI file changes.
+ **/
+#include <ui_BasicSceneWindow.h>
+#include "../../Applications/Samples/SampleApplicationBase.h"
+
+namespace OrthancStone
+{
+  namespace Samples
+  {
+
+    BasicSceneWindow::BasicSceneWindow(
+      QWidget *parent) :
+      ui_(new Ui::BasicSceneWindow)
+    {
+      ui_->setupUi(this);
+    }
+
+    BasicSceneWindow::~BasicSceneWindow()
+    {
+      delete ui_;
+    }
+
+    QStoneOpenGlWidget& BasicSceneWindow::GetOpenGlWidget()
+    {
+      return *(ui_->centralWidget);
+    }
+
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/Qt/BasicSceneWindow.h	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,53 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+#pragma once
+#include <QMainWindow>
+#include <QStoneOpenGlWidget.h>
+// #include "../../Qt/QCairoWidget.h"
+// #include "../../Qt/QStoneMainWindow.h"
+
+namespace Ui 
+{
+  class BasicSceneWindow;
+}
+
+namespace OrthancStone
+{
+  namespace Samples
+  {
+
+    //class SampleSingleCanvasApplicationBase;
+
+    class BasicSceneWindow : public QMainWindow
+    {
+      Q_OBJECT
+
+    private:
+      Ui::BasicSceneWindow*   ui_;
+      //SampleSingleCanvasApplicationBase&  stoneSampleApplication_;
+
+    public:
+      explicit BasicSceneWindow(QWidget *parent = 0);
+      ~BasicSceneWindow();
+
+      QStoneOpenGlWidget& GetOpenGlWidget();
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/Qt/BasicSceneWindow.ui	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>BasicSceneWindow</class>
+ <widget class="QMainWindow" name="BasicSceneWindow">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>903</width>
+    <height>634</height>
+   </rect>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>500</width>
+    <height>300</height>
+   </size>
+  </property>
+  <property name="baseSize">
+   <size>
+    <width>500</width>
+    <height>300</height>
+   </size>
+  </property>
+  <property name="windowTitle">
+   <string>Stone of Orthanc</string>
+  </property>
+  <property name="layoutDirection">
+   <enum>Qt::LeftToRight</enum>
+  </property>
+  <widget class="QWidget" name="mainWidget">
+   <property name="sizePolicy">
+    <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+     <horstretch>0</horstretch>
+     <verstretch>0</verstretch>
+    </sizepolicy>
+   </property>
+   <property name="layoutDirection">
+    <enum>Qt::LeftToRight</enum>
+   </property>
+   <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0">
+    <property name="sizeConstraint">
+     <enum>QLayout::SetDefaultConstraint</enum>
+    </property>
+    <item>
+     <widget class="OrthancStone::QStoneOpenGlWidget" name="centralWidget" native="true">
+      <property name="minimumSize">
+       <size>
+        <width>0</width>
+        <height>500</height>
+       </size>
+      </property>
+     </widget>
+    </item>
+   </layout>
+  </widget>
+  <widget class="QMenuBar" name="menubar">
+   <property name="geometry">
+    <rect>
+     <x>0</x>
+     <y>0</y>
+     <width>903</width>
+     <height>21</height>
+    </rect>
+   </property>
+   <widget class="QMenu" name="menuTest">
+    <property name="title">
+     <string>Test</string>
+    </property>
+   </widget>
+   <addaction name="menuTest"/>
+  </widget>
+  <widget class="QStatusBar" name="statusbar"/>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>QStoneOpenGlWidget</class>
+   <extends>QWidget</extends>
+   <header location="global">QStoneOpenGlWidget.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/Qt/CMakeLists.txt	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,83 @@
+cmake_minimum_required(VERSION 2.8.3)
+
+#####################################################################
+## Configuration of the Orthanc framework
+#####################################################################
+
+# This CMake file defines the "ORTHANC_STONE_VERSION" macro, so it
+# must be the first inclusion
+include(${CMAKE_SOURCE_DIR}/../../Resources/CMake/Version.cmake)
+
+if (ORTHANC_STONE_VERSION STREQUAL "mainline")
+  set(ORTHANC_FRAMEWORK_VERSION "mainline")
+  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
+else()
+  set(ORTHANC_FRAMEWORK_VERSION "1.5.7")
+  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
+endif()
+
+set(ORTHANC_FRAMEWORK_SOURCE "${ORTHANC_FRAMEWORK_DEFAULT_SOURCE}" CACHE STRING "Source of the Orthanc source code (can be \"hg\", \"archive\", \"web\" or \"path\")")
+set(ORTHANC_FRAMEWORK_ARCHIVE "" CACHE STRING "Path to the Orthanc archive, if ORTHANC_FRAMEWORK_SOURCE is \"archive\"")
+set(ORTHANC_FRAMEWORK_ROOT "" CACHE STRING "Path to the Orthanc source directory, if ORTHANC_FRAMEWORK_SOURCE is \"path\"")
+
+
+#####################################################################
+## Configuration of the Stone framework
+#####################################################################
+
+include(${CMAKE_SOURCE_DIR}/../../Resources/CMake/OrthancStoneParameters.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/DownloadPackage.cmake)
+
+DownloadPackage(
+  "a24b8136b8f3bb93f166baf97d9328de"
+  "http://orthanc.osimis.io/ThirdPartyDownloads/ubuntu-font-family-0.83.zip"
+  "${CMAKE_BINARY_DIR}/ubuntu-font-family-0.83")
+
+set(ORTHANC_STONE_APPLICATION_RESOURCES
+  UBUNTU_FONT  ${CMAKE_BINARY_DIR}/ubuntu-font-family-0.83/Ubuntu-R.ttf
+  )
+
+SET(ENABLE_GOOGLE_TEST OFF)
+SET(ENABLE_LOCALE ON)
+SET(ENABLE_QT ON)
+SET(ENABLE_SDL OFF)
+SET(ENABLE_WEB_CLIENT ON)
+SET(ORTHANC_SANDBOXED OFF)
+LIST(APPEND ORTHANC_BOOST_COMPONENTS program_options)
+
+include(${CMAKE_SOURCE_DIR}/../../Resources/CMake/OrthancStoneConfiguration.cmake)
+
+add_definitions(
+  -DORTHANC_ENABLE_LOGGING_PLUGIN=0
+  )
+#####################################################################
+## Build the samples
+#####################################################################
+
+add_library(OrthancStone STATIC
+  ${ORTHANC_STONE_SOURCES}
+  )
+
+list(APPEND BASIC_SCENE_APPLICATIONS_SOURCES
+  BasicSceneWindow.cpp
+  )
+
+ORTHANC_QT_WRAP_UI(BASIC_SCENE_APPLICATIONS_SOURCES
+  BasicSceneWindow.ui
+  )
+
+ORTHANC_QT_WRAP_CPP(BASIC_SCENE_APPLICATIONS_SOURCES
+  BasicSceneWindow.h
+  QStoneOpenGlWidget.h
+  )
+
+add_executable(MpBasicScene
+  ${CMAKE_CURRENT_LIST_DIR}/../MultiPlatform/BasicScene/BasicScene.h
+  ${CMAKE_CURRENT_LIST_DIR}/../MultiPlatform/BasicScene/BasicScene.cpp
+  ${CMAKE_CURRENT_LIST_DIR}/../MultiPlatform/BasicScene/mainQt.cpp
+  QStoneOpenGlWidget.cpp
+  ${BASIC_SCENE_APPLICATIONS_SOURCES}
+  )
+
+target_include_directories(MpBasicScene PUBLIC ${CMAKE_SOURCE_DIR} ${ORTHANC_STONE_ROOT})
+target_link_libraries(MpBasicScene OrthancStone)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/Qt/QStoneOpenGlWidget.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,171 @@
+#include "../../Framework/OpenGL/OpenGLIncludes.h"
+#include "QStoneOpenGlWidget.h"
+
+#include <QMouseEvent>
+
+using namespace OrthancStone;
+
+void QStoneOpenGlWidget::initializeGL()
+{
+  glewInit();
+}
+
+void QStoneOpenGlWidget::MakeCurrent()
+{
+  this->makeCurrent();
+}
+
+void QStoneOpenGlWidget::resizeGL(int w, int h)
+{
+
+}
+
+void QStoneOpenGlWidget::paintGL()
+{
+  if (compositor_)
+  {
+    compositor_->Refresh();
+  }
+  doneCurrent();
+}
+
+void ConvertFromPlatform(
+    OrthancStone::GuiAdapterMouseEvent& guiEvent,
+    PointerEvent& pointerEvent,
+    const QMouseEvent& qtEvent,
+    const IViewport& viewport)
+{
+  guiEvent.targetX = qtEvent.x();
+  guiEvent.targetY = qtEvent.y();
+  pointerEvent.AddPosition(viewport.GetPixelCenterCoordinates(guiEvent.targetX, guiEvent.targetY));
+
+  switch (qtEvent.button())
+  {
+  case Qt::LeftButton: guiEvent.button = OrthancStone::GUIADAPTER_MOUSEBUTTON_LEFT; break;
+  case Qt::MiddleButton: guiEvent.button = OrthancStone::GUIADAPTER_MOUSEBUTTON_MIDDLE; break;
+  case Qt::RightButton: guiEvent.button = OrthancStone::GUIADAPTER_MOUSEBUTTON_RIGHT; break;
+  default:
+    guiEvent.button = OrthancStone::GUIADAPTER_MOUSEBUTTON_LEFT;
+  }
+
+  if (qtEvent.modifiers().testFlag(Qt::ShiftModifier))
+  {
+    guiEvent.shiftKey = true;
+  }
+  if (qtEvent.modifiers().testFlag(Qt::ControlModifier))
+  {
+    guiEvent.ctrlKey = true;
+  }
+  if (qtEvent.modifiers().testFlag(Qt::AltModifier))
+  {
+    guiEvent.altKey = true;
+  }
+}
+
+void QStoneOpenGlWidget::mouseEvent(QMouseEvent* qtEvent, OrthancStone::GuiAdapterHidEventType guiEventType)
+{
+  OrthancStone::GuiAdapterMouseEvent guiEvent;
+  PointerEvent pointerEvent;
+  ConvertFromPlatform(guiEvent, pointerEvent, *qtEvent, *this);
+  guiEvent.type = guiEventType;
+
+  if (sceneInteractor_.get() != NULL && compositor_.get() != NULL)
+  {
+    sceneInteractor_->OnMouseEvent(guiEvent, pointerEvent);
+  }
+
+  // force redraw of the OpenGL widget
+  update();
+}
+
+void QStoneOpenGlWidget::mousePressEvent(QMouseEvent* qtEvent)
+{
+  mouseEvent(qtEvent, GUIADAPTER_EVENT_MOUSEDOWN);
+}
+
+void QStoneOpenGlWidget::mouseMoveEvent(QMouseEvent* qtEvent)
+{
+  mouseEvent(qtEvent, GUIADAPTER_EVENT_MOUSEMOVE);
+}
+
+void QStoneOpenGlWidget::mouseReleaseEvent(QMouseEvent* qtEvent)
+{
+  mouseEvent(qtEvent, GUIADAPTER_EVENT_MOUSEUP);
+}
+
+void ConvertFromPlatform(
+    OrthancStone::GuiAdapterKeyboardEvent& guiEvent,
+    const QKeyEvent& qtEvent)
+{
+  if (qtEvent.text().length() > 0)
+  {
+    guiEvent.sym[0] = qtEvent.text()[0].cell();
+  }
+  else
+  {
+    guiEvent.sym[0] = 0;
+  }
+  guiEvent.sym[1] = 0;
+
+  if (qtEvent.modifiers().testFlag(Qt::ShiftModifier))
+  {
+    guiEvent.shiftKey = true;
+  }
+  if (qtEvent.modifiers().testFlag(Qt::ControlModifier))
+  {
+    guiEvent.ctrlKey = true;
+  }
+  if (qtEvent.modifiers().testFlag(Qt::AltModifier))
+  {
+    guiEvent.altKey = true;
+  }
+
+}
+
+
+bool QStoneOpenGlWidget::keyEvent(QKeyEvent* qtEvent, OrthancStone::GuiAdapterHidEventType guiEventType)
+{
+  bool handled = false;
+  OrthancStone::GuiAdapterKeyboardEvent guiEvent;
+  ConvertFromPlatform(guiEvent, *qtEvent);
+  guiEvent.type = guiEventType;
+
+  if (sceneInteractor_.get() != NULL && compositor_.get() != NULL)
+  {
+    handled = sceneInteractor_->OnKeyboardEvent(guiEvent);
+
+    if (handled)
+    {
+      // force redraw of the OpenGL widget
+      update();
+    }
+  }
+  return handled;
+}
+
+void QStoneOpenGlWidget::keyPressEvent(QKeyEvent *qtEvent)
+{
+  bool handled = keyEvent(qtEvent, GUIADAPTER_EVENT_KEYDOWN);
+  if (!handled)
+  {
+    QOpenGLWidget::keyPressEvent(qtEvent);
+  }
+}
+
+void QStoneOpenGlWidget::keyReleaseEvent(QKeyEvent *qtEvent)
+{
+  bool handled = keyEvent(qtEvent, GUIADAPTER_EVENT_KEYUP);
+  if (!handled)
+  {
+    QOpenGLWidget::keyPressEvent(qtEvent);
+  }
+}
+
+void QStoneOpenGlWidget::wheelEvent(QWheelEvent *qtEvent)
+{
+  OrthancStone::GuiAdapterWheelEvent guiEvent;
+  throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+
+  // force redraw of the OpenGL widget
+  update();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/Qt/QStoneOpenGlWidget.h	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,90 @@
+#pragma once
+#include "../../Framework/OpenGL/OpenGLIncludes.h"
+#include <QOpenGLWidget>
+#include <QOpenGLFunctions>
+#include <QOpenGLContext>
+
+#include <boost/shared_ptr.hpp>
+#include "../../Framework/OpenGL/IOpenGLContext.h"
+#include "../../Framework/Scene2D/OpenGLCompositor.h"
+#include "../../Framework/Viewport/ViewportBase.h"
+#include "../../Applications/Generic/Scene2DInteractor.h"
+
+namespace OrthancStone
+{
+  class QStoneOpenGlWidget :
+      public QOpenGLWidget,
+      public OpenGL::IOpenGLContext,
+      public ViewportBase
+  {
+    std::unique_ptr<OrthancStone::OpenGLCompositor> compositor_;
+    boost::shared_ptr<Scene2DInteractor> sceneInteractor_;
+    QOpenGLContext                        openGlContext_;
+
+  public:
+    QStoneOpenGlWidget(QWidget *parent) :
+      QOpenGLWidget(parent),
+      ViewportBase("QtStoneOpenGlWidget")  // TODO: we shall be able to define a name but construction time is too early !
+    {
+      setFocusPolicy(Qt::StrongFocus);  // to enable keyPressEvent
+      setMouseTracking(true);           // to enable mouseMoveEvent event when no button is pressed
+    }
+
+    void Init()
+    {
+      QSurfaceFormat requestedFormat;
+      requestedFormat.setVersion( 2, 0 );
+      openGlContext_.setFormat( requestedFormat );
+      openGlContext_.create();
+      openGlContext_.makeCurrent(context()->surface());
+
+      compositor_.reset(new OpenGLCompositor(*this, GetScene()));
+    }
+
+  protected:
+
+    //**** QWidget overrides
+    void initializeGL() override;
+    void resizeGL(int w, int h) override;
+    void paintGL() override;
+
+    void mousePressEvent(QMouseEvent* event) override;
+    void mouseMoveEvent(QMouseEvent* event) override;
+    void mouseReleaseEvent(QMouseEvent* event) override;
+    void keyPressEvent(QKeyEvent* event) override;
+    void keyReleaseEvent(QKeyEvent *event) override;
+    void wheelEvent(QWheelEvent* event) override;
+
+    //**** IOpenGLContext overrides
+
+    virtual void MakeCurrent() override;
+    virtual void SwapBuffer() override {}
+
+    virtual unsigned int GetCanvasWidth() const override
+    {
+      return this->width();
+    }
+
+    virtual unsigned int GetCanvasHeight() const override
+    {
+      return this->height();
+    }
+
+  public:
+
+    void SetInteractor(boost::shared_ptr<Scene2DInteractor> sceneInteractor)
+    {
+      sceneInteractor_ = sceneInteractor;
+    }
+
+    virtual ICompositor& GetCompositor()
+    {
+      return *compositor_;
+    }
+
+  protected:
+    void mouseEvent(QMouseEvent* qtEvent, OrthancStone::GuiAdapterHidEventType guiEventType);
+    bool keyEvent(QKeyEvent* qtEvent, OrthancStone::GuiAdapterHidEventType guiEventType);
+
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/Qt/Scene2DInteractor.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,72 @@
+#include "Scene2DInteractor.h"
+
+#include "../../Framework/Scene2D/PanSceneTracker.h"
+#include "../../Framework/Scene2D/ZoomSceneTracker.h"
+#include "../../Framework/Scene2D/RotateSceneTracker.h"
+
+
+namespace OrthancStone
+{
+
+}
+
+using namespace OrthancStone;
+
+
+bool BasicScene2DInteractor::OnMouseEvent(const GuiAdapterMouseEvent& event, const PointerEvent& pointerEvent)
+{
+  if (currentTracker_.get() != NULL)
+  {
+    switch (event.type)
+    {
+    case GUIADAPTER_EVENT_MOUSEUP:
+    {
+      currentTracker_->PointerUp(pointerEvent);
+      if (!currentTracker_->IsAlive())
+      {
+        currentTracker_.reset();
+      }
+    };break;
+    case GUIADAPTER_EVENT_MOUSEMOVE:
+    {
+      currentTracker_->PointerMove(pointerEvent);
+    };break;
+    }
+    return true;
+  }
+  else
+  {
+    if (event.button == GUIADAPTER_MOUSEBUTTON_LEFT)
+    {
+      currentTracker_.reset(new RotateSceneTracker(viewportController_, pointerEvent));
+    }
+    else if (event.button == GUIADAPTER_MOUSEBUTTON_MIDDLE)
+    {
+      currentTracker_.reset(new PanSceneTracker(viewportController_, pointerEvent));
+    }
+    else if (event.button == GUIADAPTER_MOUSEBUTTON_RIGHT && compositor_.get() != NULL)
+    {
+      currentTracker_.reset(new ZoomSceneTracker(viewportController_, pointerEvent, compositor_->GetHeight()));
+    }
+    return true;
+  }
+  return false;
+}
+
+bool BasicScene2DInteractor::OnKeyboardEvent(const GuiAdapterKeyboardEvent& guiEvent)
+{
+  switch (guiEvent.sym[0])
+  {
+  case 's':
+  {
+    viewportController_->FitContent(compositor_->GetWidth(), compositor_->GetHeight());
+    return true;
+  };
+  }
+  return false;
+}
+
+bool BasicScene2DInteractor::OnWheelEvent(const GuiAdapterWheelEvent& guiEvent)
+{
+  return false;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/Qt/Scene2DInteractor.h	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "../../Applications/Generic/Scene2DInteractor.h"
+#include "../../Framework/Scene2DViewport/IFlexiblePointerTracker.h"
+
+
+class BasicScene2DInteractor : public OrthancStone::Scene2DInteractor
+{
+  boost::shared_ptr<OrthancStone::IFlexiblePointerTracker>  currentTracker_;
+public:
+  BasicScene2DInteractor(boost::shared_ptr<OrthancStone::ViewportController> viewportController) :
+    Scene2DInteractor(viewportController)
+  {}
+
+  virtual bool OnMouseEvent(const OrthancStone::GuiAdapterMouseEvent& event, const OrthancStone::PointerEvent& pointerEvent) override;
+  virtual bool OnKeyboardEvent(const OrthancStone::GuiAdapterKeyboardEvent& guiEvent);
+  virtual bool OnWheelEvent(const OrthancStone::GuiAdapterWheelEvent& guiEvent);
+};
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/README.md	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,195 @@
+
+**These samples are deprecated and not guaranteed to work. A migration to a 
+new version of the Stone API is underway.**
+
+Please read orthanc-stone/Samples/README.md first for general requirements.
+
+
+
+Deprecated -- to be sorted
+===========================
+
+The following assumes that the source code to be downloaded in
+`~/orthanc-stone` and Orthanc source code to be checked out in
+`~/orthanc`. 
+
+Building the WASM samples
+-------------------------------------
+
+```
+cd ~/orthanc-stone/Applications/Samples
+./build-wasm.sh
+```
+
+Serving the WASM samples
+------------------------------------
+```
+# launch an Orthanc listening on 8042 port:
+Orthanc
+
+# launch an nginx that will serve the WASM static files and reverse
+# proxy
+sudo nginx -p $(pwd) -c nginx.local.conf
+```
+
+You can now open the samples in http://localhost:9977
+
+Building the SDL native samples (SimpleViewer only)
+---------------------------------------------------
+
+The following also assumes that you have checked out the Orthanc
+source code in an `orthanc` folder next to the Stone of Orthanc
+repository, please enter the following:
+
+**Simple make generator with dynamic build**
+
+```
+# Please set $currentDir to the current folder
+mkdir -p ~/builds/orthanc-stone-build
+cd ~/builds/orthanc-stone-build
+cmake -DORTHANC_FRAMEWORK_SOURCE=path \
+    -DORTHANC_FRAMEWORK_ROOT=$currentDir/../../../orthanc \
+    -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON \
+    ~/orthanc-stone/Applications/Samples/
+```
+
+**Ninja generator with static SDL build (pwsh script)**
+
+```
+# Please yourself one level above the orthanc-stone and orthanc folders
+if( -not (test-path stone_build_sdl)) { mkdir stone_build_sdl }
+cd stone_build_sdl
+cmake -G Ninja -DSTATIC_BUILD=ON -DOPENSSL_NO_CAPIENG=ON -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT="$($pwd)\..\orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON ../orthanc-stone/Applications/Samples/
+```
+
+**Ninja generator with static SDL build (bash/zsh script)**
+
+```
+# Please yourself one level above the orthanc-stone and orthanc folders
+if( -not (test-path stone_build_sdl)) { mkdir stone_build_sdl }
+cd stone_build_sdl
+cmake -G Ninja -DSTATIC_BUILD=ON -DOPENSSL_NO_CAPIENG=ON -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT="`pwd`/../orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON ../orthanc-stone/Applications/Samples/
+```
+
+**Visual Studio 2017 generator with static SDL build  (pwsh script)**
+
+```
+# The following will use Visual Studio 2017 to build the SDL samples
+# in debug mode (with multiple compilers in parallel). NOTE: place 
+# yourself one level above the `orthanc-stone` and `orthanc` folders
+
+if( -not (test-path stone_build_sdl)) { mkdir stone_build_sdl }
+cd stone_build_sdl
+cmake -G "Visual Studio 15 2017 Win64" -DMSVC_MULTIPLE_PROCESSES=ON -DSTATIC_BUILD=ON -DOPENSSL_NO_CAPIENG=ON -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT="$($pwd)\..\orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON ../orthanc-stone/Applications/Samples/
+cmake --build . --config Debug
+```
+
+If you are working on Windows, add the correct generator option to
+cmake to, for instance, generate msbuild files for Visual Studio.
+
+Then, under Linux:
+```
+cmake --build . --target OrthancStoneSimpleViewer -- -j 5
+```
+
+Note: replace `$($pwd)` with the current directory when not using Powershell
+
+Building the Qt native samples (SimpleViewer only) under Windows:
+------------------------------------------------------------------
+
+**Visual Studio 2017 generator with static Qt build  (pwsh script)**
+
+For instance, if Qt is installed in `C:\Qt\5.12.0\msvc2017_64`
+
+```
+# The following will use Visual Studio 2017 to build the SDL samples
+# in debug mode (with multiple compilers in parallel). NOTE: place 
+# yourself one level above the `orthanc-stone` and `orthanc` folders
+
+if( -not (test-path stone_build_qt)) { mkdir stone_build_qt }
+cd stone_build_qt
+cmake -G "Visual Studio 15 2017 Win64" -DMSVC_MULTIPLE_PROCESSES=ON -DSTATIC_BUILD=ON -DOPENSSL_NO_CAPIENG=ON -DCMAKE_PREFIX_PATH=C:\Qt\5.12.0\msvc2017_64 -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT="$($pwd)\..\orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_QT=ON  ../orthanc-stone/Applications/Samples/
+cmake --build . --config Debug
+```
+
+Note: replace `$($pwd)` with the current directory when not using Powershell
+
+
+
+
+
+
+Building the SDL native samples (SimpleViewer only) under Windows:
+------------------------------------------------------------------
+`cmake -DSTATIC_BUILD=ON -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT="$($pwd)\..\orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON -G "Visual Studio 15 2017 Win64" ../orthanc-stone/Applications/Samples/`
+
+Note: replace `$($pwd)` with the current directory when not using Powershell
+
+Executing the native samples:
+--------------------------------
+```
+# launch an Orthanc listening on 8042 port:
+Orthanc
+
+# launch the sample
+./OrthancStoneSimpleViewer --studyId=XX
+``` 
+
+Build the Application Samples
+-----------------------------
+
+**Visual Studio 2008 (v90) **
+
+```
+cmake -G "Visual Studio 9 2008" -DUSE_LEGACY_JSONCPP=ON -DENABLE_OPENGL=ON -DSTATIC_BUILD=ON -DOPENSSL_NO_CAPIENG=ON -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT="$($pwd)\..\orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON ../orthanc-stone/Applications/Samples
+```
+
+**Visual Studio 2019 (v142) **
+
+```
+cmake -G "Visual Studio 16 2019" -A x64 -DMSVC_MULTIPLE_PROCESSES=ON -DENABLE_OPENGL=ON -DSTATIC_BUILD=ON -DOPENSSL_NO_CAPIENG=ON -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT="$($pwd)\..\orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON ../orthanc-stone/Applications/Samples
+```
+
+**Visual Studio 2017 (v140) **
+
+```
+cmake -G "Visual Studio 15 2017 Win64" -DMSVC_MULTIPLE_PROCESSES=ON -DENABLE_OPENGL=ON -DSTATIC_BUILD=ON -DOPENSSL_NO_CAPIENG=ON -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT="$($pwd)\..\orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON ../orthanc-stone/Applications/Samples
+```
+
+
+Build the core Samples
+---------------------------
+How to build the newest (2019-04-29) SDL samples under Windows, *inside* a
+folder that is sibling to the orthanc-stone folder: 
+
+**Visual Studio 2019 (v142) **
+
+```
+cmake -G "Visual Studio 16 2019" -A x64 -DMSVC_MULTIPLE_PROCESSES=ON -DENABLE_OPENGL=ON -DSTATIC_BUILD=ON -DOPENSSL_NO_CAPIENG=ON -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT="$($pwd)\..\orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON ../orthanc-stone/Samples/Sdl
+```
+
+**Visual Studio 2017 (v140) **
+
+```
+cmake -G "Visual Studio 15 2017 Win64" -DMSVC_MULTIPLE_PROCESSES=ON -DENABLE_OPENGL=ON -DSTATIC_BUILD=ON -DOPENSSL_NO_CAPIENG=ON -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT="$($pwd)\..\orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON ../orthanc-stone/Samples/Sdl
+```
+
+**Visual Studio 2008 (v90) **
+
+```
+cmake -G "Visual Studio 9 2008" -DUSE_LEGACY_JSONCPP=ON -DENABLE_OPENGL=ON -DSTATIC_BUILD=ON -DOPENSSL_NO_CAPIENG=ON -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT="$($pwd)\..\orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON ../orthanc-stone/Samples/Sdl
+```
+
+And under Ubuntu (note the /mnt/c/osi/dev/orthanc folder):
+```
+cmake -G "Ninja" -DENABLE_OPENGL=ON -DSTATIC_BUILD=OFF -DOPENSSL_NO_CAPIENG=ON -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT="/mnt/c/osi/dev/orthanc" -DALLOW_DOWNLOADS=ON -DENABLE_SDL=ON ../orthanc-stone/Samples/Sdl
+```
+
+TODO trackers:
+- CANCELLED (using outlined text now) text overlay 50% --> ColorTextureLayer 50%
+- DONE angle tracker: draw arcs
+- Handles on arc
+- Select measure tool with hit test --> Delete command
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/Sdl/BasicScene.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,418 @@
+/**
+ * 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/>.
+ **/
+
+
+// From Stone
+#include "../../Framework/Viewport/SdlViewport.h"
+#include "../../Framework/Scene2D/CairoCompositor.h"
+#include "../../Framework/Scene2D/ColorTextureSceneLayer.h"
+#include "../../Framework/Scene2D/OpenGLCompositor.h"
+#include "../../Framework/Scene2D/PanSceneTracker.h"
+#include "../../Framework/Scene2D/RotateSceneTracker.h"
+#include "../../Framework/Scene2D/ZoomSceneTracker.h"
+#include "../../Framework/Scene2DViewport/ViewportController.h"
+#include "../../Framework/Scene2DViewport/UndoStack.h"
+
+#include "../../Framework/StoneInitialization.h"
+#include "../../Framework/Messages/MessageBroker.h"
+
+// From Orthanc framework
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+#include <Core/Images/Image.h>
+#include <Core/Images/ImageProcessing.h>
+#include <Core/Images/PngWriter.h>
+
+#include <boost/make_shared.hpp>
+
+#include <SDL.h>
+#include <stdio.h>
+
+static const unsigned int FONT_SIZE = 32;
+static const int LAYER_POSITION = 150;
+
+#define OPENGL_ENABLED 0
+
+void PrepareScene(OrthancStone::Scene2D& scene)
+{
+  using namespace OrthancStone;
+
+  // Texture of 2x2 size
+  {
+    Orthanc::Image i(Orthanc::PixelFormat_RGB24, 2, 2, false);
+    
+    uint8_t *p = reinterpret_cast<uint8_t*>(i.GetRow(0));
+    p[0] = 255;
+    p[1] = 0;
+    p[2] = 0;
+
+    p[3] = 0;
+    p[4] = 255;
+    p[5] = 0;
+
+    p = reinterpret_cast<uint8_t*>(i.GetRow(1));
+    p[0] = 0;
+    p[1] = 0;
+    p[2] = 255;
+
+    p[3] = 255;
+    p[4] = 0;
+    p[5] = 0;
+
+    scene.SetLayer(12, new ColorTextureSceneLayer(i));
+
+    std::unique_ptr<ColorTextureSceneLayer> l(new ColorTextureSceneLayer(i));
+    l->SetOrigin(-3, 2);
+    l->SetPixelSpacing(1.5, 1);
+    l->SetAngle(20.0 / 180.0 * M_PI);
+    scene.SetLayer(14, l.release());
+  }
+
+  // Texture of 1x1 size
+  {
+    Orthanc::Image i(Orthanc::PixelFormat_RGB24, 1, 1, false);
+    
+    uint8_t *p = reinterpret_cast<uint8_t*>(i.GetRow(0));
+    p[0] = 255;
+    p[1] = 0;
+    p[2] = 0;
+
+    std::unique_ptr<ColorTextureSceneLayer> l(new ColorTextureSceneLayer(i));
+    l->SetOrigin(-2, 1);
+    l->SetAngle(20.0 / 180.0 * M_PI);
+    scene.SetLayer(13, l.release());
+  }
+
+  // Some lines
+  {
+    std::unique_ptr<PolylineSceneLayer> layer(new PolylineSceneLayer);
+
+    layer->SetThickness(10);
+
+    PolylineSceneLayer::Chain chain;
+    chain.push_back(ScenePoint2D(0 - 0.5, 0 - 0.5));
+    chain.push_back(ScenePoint2D(0 - 0.5, 2 - 0.5));
+    chain.push_back(ScenePoint2D(2 - 0.5, 2 - 0.5));
+    chain.push_back(ScenePoint2D(2 - 0.5, 0 - 0.5));
+    layer->AddChain(chain, true, 255, 0, 0);
+
+    chain.clear();
+    chain.push_back(ScenePoint2D(-5, -5));
+    chain.push_back(ScenePoint2D(5, -5));
+    chain.push_back(ScenePoint2D(5, 5));
+    chain.push_back(ScenePoint2D(-5, 5));
+    layer->AddChain(chain, true, 0, 255, 0);
+
+    double dy = 1.01;
+    chain.clear();
+    chain.push_back(ScenePoint2D(-4, -4));
+    chain.push_back(ScenePoint2D(4, -4 + dy));
+    chain.push_back(ScenePoint2D(-4, -4 + 2.0 * dy));
+    chain.push_back(ScenePoint2D(4, 2));
+    layer->AddChain(chain, false, 0, 0, 255);
+
+    scene.SetLayer(50, layer.release());
+  }
+
+  // Some text
+  {
+    std::unique_ptr<TextSceneLayer> layer(new TextSceneLayer);
+    layer->SetText("Hello");
+    scene.SetLayer(100, layer.release());
+  }
+}
+
+
+void TakeScreenshot(const std::string& target,
+                    const OrthancStone::Scene2D& scene,
+                    unsigned int canvasWidth,
+                    unsigned int canvasHeight)
+{
+  using namespace OrthancStone;
+  // Take a screenshot, then save it as PNG file
+  CairoCompositor compositor(scene, canvasWidth, canvasHeight);
+  compositor.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, FONT_SIZE, Orthanc::Encoding_Latin1);
+  compositor.Refresh();
+
+  Orthanc::ImageAccessor canvas;
+  compositor.GetCanvas().GetReadOnlyAccessor(canvas);
+
+  Orthanc::Image png(Orthanc::PixelFormat_RGB24, canvas.GetWidth(), canvas.GetHeight(), false);
+  Orthanc::ImageProcessing::Convert(png, canvas);
+        
+  Orthanc::PngWriter writer;
+  writer.WriteToFile(target, png);
+}
+
+
+void HandleApplicationEvent(const SDL_Event& event,
+                            boost::shared_ptr<OrthancStone::ViewportController>& controller,
+                            boost::shared_ptr<OrthancStone::IFlexiblePointerTracker>& activeTracker)
+{
+  using namespace OrthancStone;
+
+  Scene2D& scene = controller->GetScene();
+  IViewport& viewport = controller->GetViewport();
+
+  if (event.type == SDL_MOUSEMOTION)
+  {
+    int scancodeCount = 0;
+    const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount);
+
+    if (activeTracker.get() == NULL &&
+        SDL_SCANCODE_LCTRL < scancodeCount &&
+        keyboardState[SDL_SCANCODE_LCTRL])
+    {
+      // The "left-ctrl" key is down, while no tracker is present
+
+      PointerEvent e;
+      e.AddPosition(viewport.GetPixelCenterCoordinates(event.button.x, event.button.y));
+
+      ScenePoint2D p = e.GetMainPosition().Apply(scene.GetCanvasToSceneTransform());
+
+      char buf[64];
+      sprintf(buf, "(%0.02f,%0.02f)", p.GetX(), p.GetY());
+
+      if (scene.HasLayer(LAYER_POSITION))
+      {
+        TextSceneLayer& layer =
+          dynamic_cast<TextSceneLayer&>(scene.GetLayer(LAYER_POSITION));
+        layer.SetText(buf);
+        layer.SetPosition(p.GetX(), p.GetY());
+      }
+      else
+      {
+        std::unique_ptr<TextSceneLayer> 
+          layer(new TextSceneLayer);
+        layer->SetColor(0, 255, 0);
+        layer->SetText(buf);
+        layer->SetBorder(20);
+        layer->SetAnchor(BitmapAnchor_BottomCenter);
+        layer->SetPosition(p.GetX(), p.GetY());
+        scene.SetLayer(LAYER_POSITION, layer.release());
+      }
+    }
+    else
+    {
+      scene.DeleteLayer(LAYER_POSITION);
+    }
+  }
+  else if (event.type == SDL_MOUSEBUTTONDOWN)
+  {
+    PointerEvent e;
+    e.AddPosition(viewport.GetPixelCenterCoordinates(event.button.x, event.button.y));
+
+    switch (event.button.button)
+    {
+      case SDL_BUTTON_MIDDLE:
+        activeTracker = boost::make_shared<PanSceneTracker>(controller, e);
+        break;
+
+      case SDL_BUTTON_RIGHT:
+        activeTracker = boost::make_shared<ZoomSceneTracker>
+          (controller, e, viewport.GetCanvasHeight());
+        break;
+
+      case SDL_BUTTON_LEFT:
+        activeTracker = boost::make_shared<RotateSceneTracker>(controller, e);
+        break;
+
+      default:
+        break;
+    }
+  }
+  else if (event.type == SDL_KEYDOWN &&
+           event.key.repeat == 0 /* Ignore key bounce */)
+  {
+    switch (event.key.keysym.sym)
+    {
+      case SDLK_s:
+        controller->FitContent(viewport.GetCanvasWidth(), 
+                               viewport.GetCanvasHeight());
+        break;
+              
+      case SDLK_c:
+        TakeScreenshot("screenshot.png", scene, 
+                       viewport.GetCanvasWidth(), 
+                       viewport.GetCanvasHeight());
+        break;
+              
+      default:
+        break;
+    }
+  }
+}
+
+#if OPENGL_ENABLED==1
+static void GLAPIENTRY
+OpenGLMessageCallback(GLenum source,
+                      GLenum type,
+                      GLuint id,
+                      GLenum severity,
+                      GLsizei length,
+                      const GLchar* message,
+                      const void* userParam )
+{
+  if (severity != GL_DEBUG_SEVERITY_NOTIFICATION)
+  {
+    fprintf(stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n",
+            ( type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : "" ),
+            type, severity, message );
+  }
+}
+#endif
+
+void Run(OrthancStone::MessageBroker& broker,
+         OrthancStone::SdlViewport& viewport)
+{
+  using namespace OrthancStone;
+  
+  boost::shared_ptr<ViewportController> controller(
+    new ViewportController(boost::make_shared<UndoStack>(), broker, viewport));
+  
+#if OPENGL_ENABLED==1
+  glEnable(GL_DEBUG_OUTPUT);
+  glDebugMessageCallback(OpenGLMessageCallback, 0);
+#endif
+
+  boost::shared_ptr<IFlexiblePointerTracker> tracker;
+
+  bool firstShown = true;
+  bool stop = false;
+  while (!stop)
+  {
+    viewport.Refresh();
+
+    SDL_Event event;
+    while (!stop &&
+           SDL_PollEvent(&event))
+    {
+      if (event.type == SDL_QUIT)
+      {
+        stop = true;
+        break;
+      }
+      else if (event.type == SDL_MOUSEMOTION)
+      {
+        if (tracker)
+        {
+          PointerEvent e;
+          e.AddPosition(viewport.GetPixelCenterCoordinates(
+            event.button.x, event.button.y));
+          tracker->PointerMove(e);
+        }
+      }
+      else if (event.type == SDL_MOUSEBUTTONUP)
+      {
+        if (tracker)
+        {
+          PointerEvent e;
+          e.AddPosition(viewport.GetPixelCenterCoordinates(
+            event.button.x, event.button.y));
+          tracker->PointerUp(e);
+          if(!tracker->IsAlive())
+            tracker.reset();
+        }
+      }
+      else if (event.type == SDL_WINDOWEVENT)
+      {
+        switch (event.window.event)
+        {
+          case SDL_WINDOWEVENT_SIZE_CHANGED:
+            tracker.reset();
+            viewport.UpdateSize(event.window.data1, event.window.data2);
+            break;
+
+          case SDL_WINDOWEVENT_SHOWN:
+            if (firstShown)
+            {
+              // Once the window is first shown, fit the content to its size
+              controller->FitContent(viewport.GetCanvasWidth(), viewport.GetCanvasHeight());
+              firstShown = false;
+            }
+            
+            break;
+
+          default:
+            break;
+        }
+      }
+      else if (event.type == SDL_KEYDOWN &&
+               event.key.repeat == 0 /* Ignore key bounce */)
+      {
+        switch (event.key.keysym.sym)
+        {
+          case SDLK_f:
+            viewport.GetWindow().ToggleMaximize();
+            break;
+              
+          case SDLK_q:
+            stop = true;
+            break;
+
+          default:
+            break;
+        }
+      }
+      
+      HandleApplicationEvent(event, controller, tracker);
+    }
+
+    SDL_Delay(1);
+  }
+}
+
+
+
+
+/**
+ * IMPORTANT: The full arguments to "main()" are needed for SDL on
+ * Windows. Otherwise, one gets the linking error "undefined reference
+ * to `SDL_main'". https://wiki.libsdl.org/FAQWindows
+ **/
+int main(int argc, char* argv[])
+{
+  OrthancStone::StoneInitialize();
+  Orthanc::Logging::EnableInfoLevel(true);
+
+  try
+  {
+#if OPENGL_ENABLED==1
+    OrthancStone::SdlOpenGLViewport viewport("Hello", 1024, 768);
+#else
+    OrthancStone::SdlCairoViewport viewport("Hello", 1024, 768);
+#endif
+    PrepareScene(viewport.GetScene());
+
+    viewport.GetCompositor().SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, 
+                                     FONT_SIZE, Orthanc::Encoding_Latin1);
+    
+    OrthancStone::MessageBroker broker;
+    Run(broker, viewport);
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    LOG(ERROR) << "EXCEPTION: " << e.What();
+  }
+
+  OrthancStone::StoneFinalize();
+
+  return 0;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/Sdl/CMakeLists.txt	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,129 @@
+cmake_minimum_required(VERSION 2.8.3)
+
+#####################################################################
+## Configuration of the Orthanc framework
+#####################################################################
+
+# This CMake file defines the "ORTHANC_STONE_VERSION" macro, so it
+# must be the first inclusion
+include(${CMAKE_SOURCE_DIR}/../../Resources/CMake/Version.cmake)
+
+if (ORTHANC_STONE_VERSION STREQUAL "mainline")
+  set(ORTHANC_FRAMEWORK_VERSION "mainline")
+  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
+else()
+  set(ORTHANC_FRAMEWORK_VERSION "1.5.7")
+  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
+endif()
+
+set(ORTHANC_FRAMEWORK_SOURCE "${ORTHANC_FRAMEWORK_DEFAULT_SOURCE}" CACHE STRING "Source of the Orthanc source code (can be \"hg\", \"archive\", \"web\" or \"path\")")
+set(ORTHANC_FRAMEWORK_ARCHIVE "" CACHE STRING "Path to the Orthanc archive, if ORTHANC_FRAMEWORK_SOURCE is \"archive\"")
+set(ORTHANC_FRAMEWORK_ROOT "" CACHE STRING "Path to the Orthanc source directory, if ORTHANC_FRAMEWORK_SOURCE is \"path\"")
+
+
+#####################################################################
+## Configuration of the Stone framework
+#####################################################################
+
+include(${CMAKE_SOURCE_DIR}/../../Resources/CMake/OrthancStoneParameters.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/DownloadPackage.cmake)
+
+DownloadPackage(
+  "a24b8136b8f3bb93f166baf97d9328de"
+  "http://orthanc.osimis.io/ThirdPartyDownloads/ubuntu-font-family-0.83.zip"
+  "${CMAKE_BINARY_DIR}/ubuntu-font-family-0.83")
+
+set(ORTHANC_STONE_APPLICATION_RESOURCES
+  UBUNTU_FONT  ${CMAKE_BINARY_DIR}/ubuntu-font-family-0.83/Ubuntu-R.ttf
+  )
+
+SET(ENABLE_SDL_CONSOLE OFF CACHE BOOL "Enable the use of the MIT-licensed SDL_Console")
+SET(ENABLE_GOOGLE_TEST OFF)
+SET(ENABLE_LOCALE ON)
+SET(ENABLE_SDL ON)
+SET(ENABLE_WEB_CLIENT ON)
+SET(ORTHANC_SANDBOXED OFF)
+LIST(APPEND ORTHANC_BOOST_COMPONENTS program_options)
+
+include(${CMAKE_SOURCE_DIR}/../../Resources/CMake/OrthancStoneConfiguration.cmake)
+
+add_definitions(
+  -DORTHANC_ENABLE_LOGGING_PLUGIN=0
+  )
+
+
+#####################################################################
+## Build the samples
+#####################################################################
+
+add_library(OrthancStone STATIC
+  ${ORTHANC_STONE_SOURCES}
+  )
+
+#
+# BasicScene
+# 
+
+add_executable(BasicScene
+  BasicScene.cpp
+  )
+
+target_link_libraries(BasicScene OrthancStone)
+
+#
+# TrackerSample
+# 
+
+LIST(APPEND TRACKERSAMPLE_SOURCE "TrackerSample.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "TrackerSampleApp.cpp")
+LIST(APPEND TRACKERSAMPLE_SOURCE "TrackerSampleApp.h")
+
+if (MSVC AND MSVC_VERSION GREATER 1700)
+  LIST(APPEND TRACKERSAMPLE_SOURCE "cpp.hint")
+endif()
+
+add_executable(TrackerSample
+  ${TRACKERSAMPLE_SOURCE}
+  )
+
+target_link_libraries(TrackerSample OrthancStone)
+
+#
+# Loader
+# 
+
+add_executable(Loader
+  Loader.cpp
+  )
+
+target_link_libraries(Loader OrthancStone)
+
+#
+# FusionMprSdl
+# 
+
+add_executable(FusionMprSdl
+  FusionMprSdl.cpp
+  FusionMprSdl.h
+)
+
+target_link_libraries(FusionMprSdl OrthancStone)
+
+#
+# Multiplatform Basic Scene
+#
+
+LIST(APPEND MP_BASIC_SCENE_SOURCE "../MultiPlatform/BasicScene/BasicScene.cpp")
+LIST(APPEND MP_BASIC_SCENE_SOURCE "../MultiPlatform/BasicScene/BasicScene.h")
+LIST(APPEND MP_BASIC_SCENE_SOURCE "../MultiPlatform/BasicScene/mainSdl.cpp")
+
+if (MSVC AND MSVC_VERSION GREATER 1700)
+  LIST(APPEND MP_BASIC_SCENE_SOURCE "cpp.hint")
+endif()
+
+add_executable(MpBasicScene
+  ${MP_BASIC_SCENE_SOURCE}
+  )
+
+target_include_directories(MpBasicScene PUBLIC ${ORTHANC_STONE_ROOT})
+target_link_libraries(MpBasicScene OrthancStone)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/Sdl/FusionMprSdl.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,805 @@
+/**
+ * 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 "FusionMprSdl.h"
+
+#include "../../Framework/OpenGL/SdlOpenGLContext.h"
+
+#include "../../Framework/StoneInitialization.h"
+
+#include "../../Framework/Scene2D/CairoCompositor.h"
+#include "../../Framework/Scene2D/ColorTextureSceneLayer.h"
+#include "../../Framework/Scene2D/OpenGLCompositor.h"
+#include "../../Framework/Scene2D/PanSceneTracker.h"
+#include "../../Framework/Scene2D/ZoomSceneTracker.h"
+#include "../../Framework/Scene2D/RotateSceneTracker.h"
+
+#include "../../Framework/Scene2DViewport/UndoStack.h"
+#include "../../Framework/Scene2DViewport/CreateLineMeasureTracker.h"
+#include "../../Framework/Scene2DViewport/CreateAngleMeasureTracker.h"
+#include "../../Framework/Scene2DViewport/IFlexiblePointerTracker.h"
+#include "../../Framework/Scene2DViewport/MeasureTool.h"
+#include "../../Framework/Scene2DViewport/PredeclaredTypes.h"
+
+#include "../../Framework/Volumes/VolumeSceneLayerSource.h"
+
+#include <Core/Images/Image.h>
+#include <Core/Images/ImageProcessing.h>
+#include <Core/Images/PngWriter.h>
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+
+#include <boost/shared_ptr.hpp>
+#include <boost/weak_ptr.hpp>
+#include <boost/make_shared.hpp>
+
+#include <stdio.h>
+#include "../../Framework/Oracle/GetOrthancWebViewerJpegCommand.h"
+#include "../../Framework/Oracle/ThreadedOracle.h"
+#include "../../Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h"
+#include "../../Framework/Loaders/OrthancMultiframeVolumeLoader.h"
+#include "../../Framework/Loaders/DicomStructureSetLoader.h"
+#include "../../Framework/Scene2D/GrayscaleStyleConfigurator.h"
+#include "../../Framework/Scene2D/LookupTableStyleConfigurator.h"
+#include "../../Framework/Volumes/DicomVolumeImageMPRSlicer.h"
+#include "Core/SystemToolbox.h"
+
+namespace OrthancStone
+{
+  const char* FusionMprMeasureToolToString(size_t i)
+  {
+    static const char* descs[] = {
+      "FusionMprGuiTool_Rotate",
+      "FusionMprGuiTool_Pan",
+      "FusionMprGuiTool_Zoom",
+      "FusionMprGuiTool_LineMeasure",
+      "FusionMprGuiTool_CircleMeasure",
+      "FusionMprGuiTool_AngleMeasure",
+      "FusionMprGuiTool_EllipseMeasure",
+      "FusionMprGuiTool_LAST"
+    };
+    if (i >= FusionMprGuiTool_LAST)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "Wrong tool index");
+    }
+    return descs[i];
+  }
+
+  Scene2D& FusionMprSdlApp::GetScene()
+  {
+    return controller_->GetScene();
+  }
+
+  const Scene2D& FusionMprSdlApp::GetScene() const
+  {
+    return controller_->GetScene();
+  }
+
+  void FusionMprSdlApp::SelectNextTool()
+  {
+    currentTool_ = static_cast<FusionMprGuiTool>(currentTool_ + 1);
+    if (currentTool_ == FusionMprGuiTool_LAST)
+      currentTool_ = static_cast<FusionMprGuiTool>(0);;
+    printf("Current tool is now: %s\n", FusionMprMeasureToolToString(currentTool_));
+  }
+
+  void FusionMprSdlApp::DisplayInfoText()
+  {
+    // do not try to use stuff too early!
+    ICompositor* pCompositor = &(viewport_.GetCompositor());
+    if (pCompositor == NULL)
+      return;
+
+    std::stringstream msg;
+	
+	for (std::map<std::string, std::string>::const_iterator kv = infoTextMap_.begin();
+		kv != infoTextMap_.end(); ++kv)
+    {
+      msg << kv->first << " : " << kv->second << std::endl;
+    }
+	std::string msgS = msg.str();
+
+    TextSceneLayer* layerP = NULL;
+    if (GetScene().HasLayer(FIXED_INFOTEXT_LAYER_ZINDEX))
+    {
+      TextSceneLayer& layer = dynamic_cast<TextSceneLayer&>(
+        GetScene().GetLayer(FIXED_INFOTEXT_LAYER_ZINDEX));
+      layerP = &layer;
+    }
+    else
+    {
+      std::unique_ptr<TextSceneLayer> layer(new TextSceneLayer);
+      layerP = layer.get();
+      layer->SetColor(0, 255, 0);
+      layer->SetFontIndex(1);
+      layer->SetBorder(20);
+      layer->SetAnchor(BitmapAnchor_TopLeft);
+      //layer->SetPosition(0,0);
+      GetScene().SetLayer(FIXED_INFOTEXT_LAYER_ZINDEX, layer.release());
+    }
+    // position the fixed info text in the upper right corner
+    layerP->SetText(msgS.c_str());
+    double cX = viewport_.GetCompositor().GetCanvasWidth() * (-0.5);
+    double cY = viewport_.GetCompositor().GetCanvasHeight() * (-0.5);
+    GetScene().GetCanvasToSceneTransform().Apply(cX,cY);
+    layerP->SetPosition(cX, cY);
+  }
+
+  void FusionMprSdlApp::DisplayFloatingCtrlInfoText(const PointerEvent& e)
+  {
+    ScenePoint2D p = e.GetMainPosition().Apply(GetScene().GetCanvasToSceneTransform());
+
+    char buf[128];
+    sprintf(buf, "S:(%0.02f,%0.02f) C:(%0.02f,%0.02f)", 
+      p.GetX(), p.GetY(), 
+      e.GetMainPosition().GetX(), e.GetMainPosition().GetY());
+
+    if (GetScene().HasLayer(FLOATING_INFOTEXT_LAYER_ZINDEX))
+    {
+      TextSceneLayer& layer =
+        dynamic_cast<TextSceneLayer&>(GetScene().GetLayer(FLOATING_INFOTEXT_LAYER_ZINDEX));
+      layer.SetText(buf);
+      layer.SetPosition(p.GetX(), p.GetY());
+    }
+    else
+    {
+      std::unique_ptr<TextSceneLayer> layer(new TextSceneLayer);
+      layer->SetColor(0, 255, 0);
+      layer->SetText(buf);
+      layer->SetBorder(20);
+      layer->SetAnchor(BitmapAnchor_BottomCenter);
+      layer->SetPosition(p.GetX(), p.GetY());
+      GetScene().SetLayer(FLOATING_INFOTEXT_LAYER_ZINDEX, layer.release());
+    }
+  }
+
+  void FusionMprSdlApp::HideInfoText()
+  {
+    GetScene().DeleteLayer(FLOATING_INFOTEXT_LAYER_ZINDEX);
+  }
+
+  void FusionMprSdlApp::HandleApplicationEvent(
+    const SDL_Event & event)
+  {
+    DisplayInfoText();
+
+    if (event.type == SDL_MOUSEMOTION)
+    {
+      int scancodeCount = 0;
+      const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount);
+
+      if (activeTracker_.get() == NULL &&
+        SDL_SCANCODE_LALT < scancodeCount &&
+        keyboardState[SDL_SCANCODE_LALT])
+      {
+        // The "left-ctrl" key is down, while no tracker is present
+        // Let's display the info text
+        PointerEvent e;
+        e.AddPosition(controller_->GetViewport().GetPixelCenterCoordinates(
+          event.button.x, event.button.y));
+
+        DisplayFloatingCtrlInfoText(e);
+      }
+      else
+      {
+        HideInfoText();
+        //LOG(TRACE) << "(event.type == SDL_MOUSEMOTION)";
+        if (activeTracker_.get() != NULL)
+        {
+          //LOG(TRACE) << "(activeTracker_.get() != NULL)";
+          PointerEvent e;
+          e.AddPosition(controller_->GetViewport().GetPixelCenterCoordinates(
+            event.button.x, event.button.y));
+          
+          //LOG(TRACE) << "event.button.x = " << event.button.x << "     " <<
+          //  "event.button.y = " << event.button.y;
+          LOG(TRACE) << "activeTracker_->PointerMove(e); " <<
+            e.GetMainPosition().GetX() << " " << e.GetMainPosition().GetY();
+          
+          activeTracker_->PointerMove(e);
+          if (!activeTracker_->IsAlive())
+            activeTracker_.reset();
+        }
+      }
+    }
+    else if (event.type == SDL_MOUSEBUTTONUP)
+    {
+      if (activeTracker_)
+      {
+        PointerEvent e;
+        e.AddPosition(controller_->GetViewport().GetPixelCenterCoordinates(event.button.x, event.button.y));
+        activeTracker_->PointerUp(e);
+        if (!activeTracker_->IsAlive())
+          activeTracker_.reset();
+      }
+    }
+    else if (event.type == SDL_MOUSEBUTTONDOWN)
+    {
+      PointerEvent e;
+      e.AddPosition(controller_->GetViewport().GetPixelCenterCoordinates(
+        event.button.x, event.button.y));
+      if (activeTracker_)
+      {
+        activeTracker_->PointerDown(e);
+        if (!activeTracker_->IsAlive())
+          activeTracker_.reset();
+      }
+      else
+      {
+        // we ATTEMPT to create a tracker if need be
+        activeTracker_ = CreateSuitableTracker(event, e);
+      }
+    }
+    else if (event.type == SDL_KEYDOWN &&
+      event.key.repeat == 0 /* Ignore key bounce */)
+    {
+      switch (event.key.keysym.sym)
+      {
+      case SDLK_ESCAPE:
+        if (activeTracker_)
+        {
+          activeTracker_->Cancel();
+          if (!activeTracker_->IsAlive())
+            activeTracker_.reset();
+        }
+        break;
+
+      case SDLK_t:
+        if (!activeTracker_)
+          SelectNextTool();
+        else
+        {
+          LOG(WARNING) << "You cannot change the active tool when an interaction"
+            " is taking place";
+        }
+        break;
+      case SDLK_s:
+        controller_->FitContent(viewport_.GetCompositor().GetCanvasWidth(),
+          viewport_.GetCompositor().GetCanvasHeight());
+        break;
+
+      case SDLK_z:
+        LOG(TRACE) << "SDLK_z has been pressed. event.key.keysym.mod == " << event.key.keysym.mod;
+        if (event.key.keysym.mod & KMOD_CTRL)
+        {
+          if (controller_->CanUndo())
+          {
+            LOG(TRACE) << "Undoing...";
+            controller_->Undo();
+          }
+          else
+          {
+            LOG(WARNING) << "Nothing to undo!!!";
+          }
+        }
+        break;
+
+      case SDLK_y:
+        LOG(TRACE) << "SDLK_y has been pressed. event.key.keysym.mod == " << event.key.keysym.mod;
+        if (event.key.keysym.mod & KMOD_CTRL)
+        {
+          if (controller_->CanRedo())
+          {
+            LOG(TRACE) << "Redoing...";
+            controller_->Redo();
+          }
+          else
+          {
+            LOG(WARNING) << "Nothing to redo!!!";
+          }
+        }
+        break;
+
+      case SDLK_c:
+        TakeScreenshot(
+          "screenshot.png",
+          viewport_.GetCompositor().GetCanvasWidth(),
+          viewport_.GetCompositor().GetCanvasHeight());
+        break;
+
+      default:
+        break;
+      }
+    }
+  }
+
+
+  void FusionMprSdlApp::OnSceneTransformChanged(
+    const ViewportController::SceneTransformChanged& message)
+  {
+    DisplayInfoText();
+  }
+
+  boost::shared_ptr<IFlexiblePointerTracker> FusionMprSdlApp::CreateSuitableTracker(
+    const SDL_Event & event,
+    const PointerEvent & e)
+  {
+    using namespace Orthanc;
+
+    switch (event.button.button)
+    {
+    case SDL_BUTTON_MIDDLE:
+      return boost::shared_ptr<IFlexiblePointerTracker>(new PanSceneTracker
+        (controller_, e));
+
+    case SDL_BUTTON_RIGHT:
+      return boost::shared_ptr<IFlexiblePointerTracker>(new ZoomSceneTracker
+        (controller_, e, viewport_.GetCompositor().GetCanvasHeight()));
+
+    case SDL_BUTTON_LEFT:
+    {
+      //LOG(TRACE) << "CreateSuitableTracker: case SDL_BUTTON_LEFT:";
+      // TODO: we need to iterate on the set of measuring tool and perform
+      // a hit test to check if a tracker needs to be created for edition.
+      // Otherwise, depending upon the active tool, we might want to create
+      // a "measuring tool creation" tracker
+
+      // TODO: if there are conflicts, we should prefer a tracker that 
+      // pertains to the type of measuring tool currently selected (TBD?)
+      boost::shared_ptr<IFlexiblePointerTracker> hitTestTracker = TrackerHitTest(e);
+
+      if (hitTestTracker != NULL)
+      {
+        //LOG(TRACE) << "hitTestTracker != NULL";
+        return hitTestTracker;
+      }
+      else
+      {
+        switch (currentTool_)
+        {
+        case FusionMprGuiTool_Rotate:
+          //LOG(TRACE) << "Creating RotateSceneTracker";
+          return boost::shared_ptr<IFlexiblePointerTracker>(new RotateSceneTracker(
+            controller_, e));
+        case FusionMprGuiTool_Pan:
+          return boost::shared_ptr<IFlexiblePointerTracker>(new PanSceneTracker(
+            controller_, e));
+        case FusionMprGuiTool_Zoom:
+          return boost::shared_ptr<IFlexiblePointerTracker>(new ZoomSceneTracker(
+            controller_, e, viewport_.GetCompositor().GetCanvasHeight()));
+        //case GuiTool_AngleMeasure:
+        //  return new AngleMeasureTracker(GetScene(), e);
+        //case GuiTool_CircleMeasure:
+        //  return new CircleMeasureTracker(GetScene(), e);
+        //case GuiTool_EllipseMeasure:
+        //  return new EllipseMeasureTracker(GetScene(), e);
+        case FusionMprGuiTool_LineMeasure:
+          return boost::shared_ptr<IFlexiblePointerTracker>(new CreateLineMeasureTracker(
+            IObserver::GetBroker(), controller_, e));
+        case FusionMprGuiTool_AngleMeasure:
+          return boost::shared_ptr<IFlexiblePointerTracker>(new CreateAngleMeasureTracker(
+            IObserver::GetBroker(), controller_, e));
+        case FusionMprGuiTool_CircleMeasure:
+          LOG(ERROR) << "Not implemented yet!";
+          return boost::shared_ptr<IFlexiblePointerTracker>();
+        case FusionMprGuiTool_EllipseMeasure:
+          LOG(ERROR) << "Not implemented yet!";
+          return boost::shared_ptr<IFlexiblePointerTracker>();
+        default:
+          throw OrthancException(ErrorCode_InternalError, "Wrong tool!");
+        }
+      }
+    }
+    default:
+      return boost::shared_ptr<IFlexiblePointerTracker>();
+    }
+  }
+
+
+  FusionMprSdlApp::FusionMprSdlApp(MessageBroker& broker)
+    : IObserver(broker)
+    , broker_(broker)
+    , oracleObservable_(broker)
+    , oracle_(*this)
+    , currentTool_(FusionMprGuiTool_Rotate)
+    , undoStack_(new UndoStack)
+    , viewport_("Hello", 1024, 1024, false) // False means we do NOT let Windows treat this as a legacy application that needs to be scaled
+  {
+    //oracleObservable.RegisterObserverCallback
+    //(new Callable
+    //  <FusionMprSdlApp, SleepOracleCommand::TimeoutMessage>(*this, &FusionMprSdlApp::Handle));
+
+    //oracleObservable.RegisterObserverCallback
+    //(new Callable
+    //  <Toto, GetOrthancImageCommand::SuccessMessage>(*this, &FusionMprSdlApp::Handle));
+
+    //oracleObservable.RegisterObserverCallback
+    //(new Callable
+    //  <FusionMprSdlApp, GetOrthancWebViewerJpegCommand::SuccessMessage>(*this, &ToFusionMprSdlAppto::Handle));
+
+    oracleObservable_.RegisterObserverCallback
+    (new Callable
+      <FusionMprSdlApp, OracleCommandExceptionMessage>(*this, &FusionMprSdlApp::Handle));
+    
+    controller_ = boost::shared_ptr<ViewportController>(
+      new ViewportController(undoStack_, broker_, viewport_));
+
+    controller_->RegisterObserverCallback(
+      new Callable<FusionMprSdlApp, ViewportController::SceneTransformChanged>
+      (*this, &FusionMprSdlApp::OnSceneTransformChanged));
+
+    TEXTURE_2x2_1_ZINDEX = 1;
+    TEXTURE_1x1_ZINDEX = 2;
+    TEXTURE_2x2_2_ZINDEX = 3;
+    LINESET_1_ZINDEX = 4;
+    LINESET_2_ZINDEX = 5;
+    FLOATING_INFOTEXT_LAYER_ZINDEX = 6;
+    FIXED_INFOTEXT_LAYER_ZINDEX = 7;
+  }
+
+  void FusionMprSdlApp::PrepareScene()
+  {
+    // Texture of 2x2 size
+    {
+      Orthanc::Image i(Orthanc::PixelFormat_RGB24, 2, 2, false);
+
+      uint8_t* p = reinterpret_cast<uint8_t*>(i.GetRow(0));
+      p[0] = 255;
+      p[1] = 0;
+      p[2] = 0;
+
+      p[3] = 0;
+      p[4] = 255;
+      p[5] = 0;
+
+      p = reinterpret_cast<uint8_t*>(i.GetRow(1));
+      p[0] = 0;
+      p[1] = 0;
+      p[2] = 255;
+
+      p[3] = 255;
+      p[4] = 0;
+      p[5] = 0;
+
+      GetScene().SetLayer(TEXTURE_2x2_1_ZINDEX, new ColorTextureSceneLayer(i));
+    }
+  }
+
+  void FusionMprSdlApp::DisableTracker()
+  {
+    if (activeTracker_)
+    {
+      activeTracker_->Cancel();
+      activeTracker_.reset();
+    }
+  }
+
+  void FusionMprSdlApp::TakeScreenshot(const std::string& target,
+    unsigned int canvasWidth,
+    unsigned int canvasHeight)
+  {
+    CairoCompositor compositor(GetScene(), canvasWidth, canvasHeight);
+    compositor.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, FONT_SIZE_0, Orthanc::Encoding_Latin1);
+    compositor.Refresh();
+
+    Orthanc::ImageAccessor canvas;
+    compositor.GetCanvas().GetReadOnlyAccessor(canvas);
+
+    Orthanc::Image png(Orthanc::PixelFormat_RGB24, canvas.GetWidth(), canvas.GetHeight(), false);
+    Orthanc::ImageProcessing::Convert(png, canvas);
+
+    Orthanc::PngWriter writer;
+    writer.WriteToFile(target, png);
+  }
+
+
+  boost::shared_ptr<IFlexiblePointerTracker> FusionMprSdlApp::TrackerHitTest(const PointerEvent & e)
+  {
+    // std::vector<boost::shared_ptr<MeasureTool>> measureTools_;
+    return boost::shared_ptr<IFlexiblePointerTracker>();
+  }
+
+  static void GLAPIENTRY
+    OpenGLMessageCallback(GLenum source,
+      GLenum type,
+      GLuint id,
+      GLenum severity,
+      GLsizei length,
+      const GLchar* message,
+      const void* userParam)
+  {
+    if (severity != GL_DEBUG_SEVERITY_NOTIFICATION)
+    {
+      fprintf(stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n",
+        (type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : ""),
+        type, severity, message);
+    }
+  }
+
+  static bool g_stopApplication = false;
+  
+
+  void FusionMprSdlApp::Handle(const DicomVolumeImage::GeometryReadyMessage& message)
+  {
+    printf("Geometry ready\n");
+
+    //plane_ = message.GetOrigin().GetGeometry().GetSagittalGeometry();
+    //plane_ = message.GetOrigin().GetGeometry().GetAxialGeometry();
+    plane_ = message.GetOrigin().GetGeometry().GetCoronalGeometry();
+    plane_.SetOrigin(message.GetOrigin().GetGeometry().GetCoordinates(0.5f, 0.5f, 0.5f));
+
+    //Refresh();
+  }
+
+
+  void FusionMprSdlApp::Handle(const OracleCommandExceptionMessage& message)
+  {
+    printf("EXCEPTION: [%s] on command type %d\n", message.GetException().What(), message.GetCommand().GetType());
+
+    switch (message.GetCommand().GetType())
+    {
+    case IOracleCommand::Type_GetOrthancWebViewerJpeg:
+      printf("URI: [%s]\n", dynamic_cast<const GetOrthancWebViewerJpegCommand&>
+        (message.GetCommand()).GetUri().c_str());
+      break;
+
+    default:
+      break;
+    }
+  }
+
+  void FusionMprSdlApp::SetVolume1(int depth,
+    const boost::shared_ptr<OrthancStone::IVolumeSlicer>& volume,
+    OrthancStone::ILayerStyleConfigurator* style)
+  {
+    source1_.reset(new OrthancStone::VolumeSceneLayerSource(controller_->GetScene(), depth, volume));
+
+    if (style != NULL)
+    {
+      source1_->SetConfigurator(style);
+    }
+  }
+
+  void FusionMprSdlApp::SetVolume2(int depth,
+    const boost::shared_ptr<OrthancStone::IVolumeSlicer>& volume,
+    OrthancStone::ILayerStyleConfigurator* style)
+  {
+    source2_.reset(new OrthancStone::VolumeSceneLayerSource(controller_->GetScene(), depth, volume));
+
+    if (style != NULL)
+    {
+      source2_->SetConfigurator(style);
+    }
+  }
+
+  void FusionMprSdlApp::SetStructureSet(int depth,
+    const boost::shared_ptr<OrthancStone::DicomStructureSetLoader>& volume)
+  {
+    source3_.reset(new OrthancStone::VolumeSceneLayerSource(controller_->GetScene(), depth, volume));
+  }
+  
+  void FusionMprSdlApp::Run()
+  {
+    // False means we do NOT let Windows treat this as a legacy application
+    // that needs to be scaled
+    controller_->FitContent(viewport_.GetCanvasWidth(), viewport_.GetCanvasHeight());
+
+    glEnable(GL_DEBUG_OUTPUT);
+    glDebugMessageCallback(OpenGLMessageCallback, 0);
+
+    viewport_.GetCompositor().SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT,
+      FONT_SIZE_0, Orthanc::Encoding_Latin1);
+    viewport_.GetCompositor().SetFont(1, Orthanc::EmbeddedResources::UBUNTU_FONT,
+      FONT_SIZE_1, Orthanc::Encoding_Latin1);
+
+
+    //////// from loader
+    {
+      Orthanc::WebServiceParameters p;
+      //p.SetUrl("http://localhost:8043/");
+      p.SetCredentials("orthanc", "orthanc");
+      oracle_.SetOrthancParameters(p);
+    }
+
+    //////// from Run
+
+    boost::shared_ptr<DicomVolumeImage>  ct(new DicomVolumeImage);
+    boost::shared_ptr<DicomVolumeImage>  dose(new DicomVolumeImage);
+
+
+    boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> ctLoader;
+    boost::shared_ptr<OrthancMultiframeVolumeLoader> doseLoader;
+    boost::shared_ptr<DicomStructureSetLoader>  rtstructLoader;
+
+    {
+      ctLoader.reset(new OrthancSeriesVolumeProgressiveLoader(ct, oracle_, oracleObservable_));
+      doseLoader.reset(new OrthancMultiframeVolumeLoader(dose, oracle_, oracleObservable_));
+      rtstructLoader.reset(new DicomStructureSetLoader(oracle_, oracleObservable_));
+    }
+
+    //toto->SetReferenceLoader(*ctLoader);
+    //doseLoader->RegisterObserverCallback
+    //(new Callable
+    //  <FusionMprSdlApp, DicomVolumeImage::GeometryReadyMessage>(*this, &FusionMprSdlApp::Handle));
+    ctLoader->RegisterObserverCallback
+    (new Callable
+      <FusionMprSdlApp, DicomVolumeImage::GeometryReadyMessage>(*this, &FusionMprSdlApp::Handle));
+
+    this->SetVolume1(0, ctLoader, new GrayscaleStyleConfigurator);
+
+    {
+      std::unique_ptr<LookupTableStyleConfigurator> config(new LookupTableStyleConfigurator);
+      config->SetLookupTable(Orthanc::EmbeddedResources::COLORMAP_HOT);
+
+      boost::shared_ptr<DicomVolumeImageMPRSlicer> tmp(new DicomVolumeImageMPRSlicer(dose));
+      this->SetVolume2(1, tmp, config.release());
+    }
+
+    this->SetStructureSet(2, rtstructLoader);
+
+#if 1
+    /*
+    BGO data
+    http://localhost:8042/twiga-orthanc-viewer-demo/twiga-orthanc-viewer-demo.html?ct-series=a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa
+    &
+    dose-instance=830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb
+    &
+    struct-instance=54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9
+    */
+    ctLoader->LoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");  // CT
+    doseLoader->LoadInstance("830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb");  // RT-DOSE
+    rtstructLoader->LoadInstance("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9");  // RT-STRUCT
+#else
+    //ctLoader->LoadSeries("cb3ea4d1-d08f3856-ad7b6314-74d88d77-60b05618");  // CT
+    //doseLoader->LoadInstance("41029085-71718346-811efac4-420e2c15-d39f99b6");  // RT-DOSE
+    //rtstructLoader->LoadInstance("83d9c0c3-913a7fee-610097d7-cbf0522d-fd75bee6");  // RT-STRUCT
+
+    // 2017-05-16
+    ctLoader->LoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");  // CT
+    doseLoader->LoadInstance("eac822ef-a395f94e-e8121fe0-8411fef8-1f7bffad");  // RT-DOSE
+    rtstructLoader->LoadInstance("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9");  // RT-STRUCT
+#endif
+
+    oracle_.Start();
+
+//// END from loader
+
+    while (!g_stopApplication)
+    {
+      viewport_.GetCompositor().Refresh();
+
+//////// from loader
+      if (source1_.get() != NULL)
+      {
+        source1_->Update(plane_);
+      }
+
+      if (source2_.get() != NULL)
+      {
+        source2_->Update(plane_);
+      }
+
+      if (source3_.get() != NULL)
+      {
+        source3_->Update(plane_);
+      }
+//// END from loader
+
+      SDL_Event event;
+      while (!g_stopApplication && SDL_PollEvent(&event))
+      {
+        if (event.type == SDL_QUIT)
+        {
+          g_stopApplication = true;
+          break;
+        }
+        else if (event.type == SDL_WINDOWEVENT &&
+          event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
+        {
+          DisableTracker(); // was: tracker.reset(NULL);
+        }
+        else if (event.type == SDL_KEYDOWN &&
+          event.key.repeat == 0 /* Ignore key bounce */)
+        {
+          switch (event.key.keysym.sym)
+          {
+          case SDLK_f:
+            viewport_.GetWindow().ToggleMaximize();
+            break;
+
+          case SDLK_s:
+            controller_->FitContent(viewport_.GetCanvasWidth(), viewport_.GetCanvasHeight());
+            break;
+
+          case SDLK_q:
+            g_stopApplication = true;
+            break;
+          default:
+            break;
+          }
+        }
+        HandleApplicationEvent(event);
+      }
+      SDL_Delay(1);
+    }
+
+    //// from loader
+
+    //Orthanc::SystemToolbox::ServerBarrier();
+
+    /**
+     * WARNING => The oracle must be stopped BEFORE the objects using
+     * it are destroyed!!! This forces to wait for the completion of
+     * the running callback methods. Otherwise, the callbacks methods
+     * might still be running while their parent object is destroyed,
+     * resulting in crashes. This is very visible if adding a sleep(),
+     * as in (*).
+     **/
+
+    oracle_.Stop();
+    //// END from loader
+  }
+
+  void FusionMprSdlApp::SetInfoDisplayMessage(
+    std::string key, std::string value)
+  {
+    if (value == "")
+      infoTextMap_.erase(key);
+    else
+      infoTextMap_[key] = value;
+    DisplayInfoText();
+  }
+
+}
+
+
+boost::weak_ptr<OrthancStone::FusionMprSdlApp> g_app;
+
+void FusionMprSdl_SetInfoDisplayMessage(std::string key, std::string value)
+{
+  boost::shared_ptr<OrthancStone::FusionMprSdlApp> app = g_app.lock();
+  if (app)
+  {
+    app->SetInfoDisplayMessage(key, value);
+  }
+}
+
+/**
+ * IMPORTANT: The full arguments to "main()" are needed for SDL on
+ * Windows. Otherwise, one gets the linking error "undefined reference
+ * to `SDL_main'". https://wiki.libsdl.org/FAQWindows
+ **/
+int main(int argc, char* argv[])
+{
+  using namespace OrthancStone;
+
+  StoneInitialize();
+  Orthanc::Logging::EnableInfoLevel(true);
+//  Orthanc::Logging::EnableTraceLevel(true);
+
+  try
+  {
+    OrthancStone::MessageBroker broker;
+    boost::shared_ptr<FusionMprSdlApp> app(new FusionMprSdlApp(broker));
+    g_app = app;
+    app->PrepareScene();
+    app->Run();
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    LOG(ERROR) << "EXCEPTION: " << e.What();
+  }
+
+  StoneFinalize();
+
+  return 0;
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/Sdl/FusionMprSdl.h	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,206 @@
+/**
+ * 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 "../../Framework/Viewport/SdlViewport.h"
+
+#include "../../Framework/Messages/IObserver.h"
+#include "../../Framework/Messages/IMessageEmitter.h"
+#include "../../Framework/Oracle/OracleCommandExceptionMessage.h"
+#include "../../Framework/Scene2DViewport/ViewportController.h"
+#include "../../Framework/Volumes/DicomVolumeImage.h"
+#include "../../Framework/Oracle/ThreadedOracle.h"
+
+#include <boost/enable_shared_from_this.hpp>
+#include <boost/thread.hpp>
+#include <boost/noncopyable.hpp>
+
+#include <SDL.h>
+
+namespace OrthancStone
+{
+  class OpenGLCompositor;
+  class IVolumeSlicer;
+  class ILayerStyleConfigurator;
+  class DicomStructureSetLoader;
+  class IOracle;
+  class ThreadedOracle;
+  class VolumeSceneLayerSource;
+  class NativeFusionMprApplicationContext;
+  class SdlOpenGLViewport;
+   
+  enum FusionMprGuiTool
+  {
+    FusionMprGuiTool_Rotate = 0,
+    FusionMprGuiTool_Pan,
+    FusionMprGuiTool_Zoom,
+    FusionMprGuiTool_LineMeasure,
+    FusionMprGuiTool_CircleMeasure,
+    FusionMprGuiTool_AngleMeasure,
+    FusionMprGuiTool_EllipseMeasure,
+    FusionMprGuiTool_LAST
+  };
+
+  const char* MeasureToolToString(size_t i);
+
+  static const unsigned int FONT_SIZE_0 = 32;
+  static const unsigned int FONT_SIZE_1 = 24;
+
+  class Scene2D;
+  class UndoStack;
+
+  /**
+  This application subclasses IMessageEmitter to use a mutex before forwarding Oracle messages (that
+  can be sent from multiple threads)
+  */
+  class FusionMprSdlApp : public IObserver
+    , public boost::enable_shared_from_this<FusionMprSdlApp>
+    , public IMessageEmitter
+  {
+  public:
+    // 12 because.
+    FusionMprSdlApp(MessageBroker& broker);
+
+    void PrepareScene();
+    void Run();
+    void SetInfoDisplayMessage(std::string key, std::string value);
+    void DisableTracker();
+
+    Scene2D&       GetScene();
+    const Scene2D& GetScene() const;
+
+    void HandleApplicationEvent(const SDL_Event& event);
+
+    /**
+    This method is called when the scene transform changes. It allows to
+    recompute the visual elements whose content depend upon the scene transform
+    */
+    void OnSceneTransformChanged(
+      const ViewportController::SceneTransformChanged& message);
+
+
+    virtual void EmitMessage(const IObserver& observer,
+      const IMessage& message) ORTHANC_OVERRIDE
+    {
+      try
+      {
+        boost::unique_lock<boost::shared_mutex>  lock(mutex_);
+        oracleObservable_.EmitMessage(observer, message);
+      }
+      catch (Orthanc::OrthancException& e)
+      {
+        LOG(ERROR) << "Exception while emitting a message: " << e.What();
+        throw;
+      }
+    }
+    
+  private:
+#if 1
+    // if threaded (not wasm)
+    MessageBroker& broker_;
+    IObservable oracleObservable_;
+    ThreadedOracle oracle_;
+    boost::shared_mutex mutex_; // to serialize messages from the ThreadedOracle
+#endif
+
+    void SelectNextTool();
+
+    /**
+    This returns a random point in the canvas part of the scene, but in
+    scene coordinates
+    */
+    ScenePoint2D GetRandomPointInScene() const;
+
+    boost::shared_ptr<IFlexiblePointerTracker> TrackerHitTest(const PointerEvent& e);
+
+    boost::shared_ptr<IFlexiblePointerTracker> CreateSuitableTracker(
+      const SDL_Event& event,
+      const PointerEvent& e);
+
+    void TakeScreenshot(
+      const std::string& target,
+      unsigned int canvasWidth,
+      unsigned int canvasHeight);
+
+    /**
+      This adds the command at the top of the undo stack
+    */
+    void Commit(boost::shared_ptr<TrackerCommand> cmd);
+    void Undo();
+    void Redo();
+
+
+    // TODO private
+    void Handle(const DicomVolumeImage::GeometryReadyMessage& message);
+    void Handle(const OracleCommandExceptionMessage& message);
+
+    void SetVolume1(
+      int depth,
+      const boost::shared_ptr<IVolumeSlicer>& volume,
+      ILayerStyleConfigurator* style);
+    
+    void SetVolume2(
+      int depth,
+      const boost::shared_ptr<IVolumeSlicer>& volume,
+      ILayerStyleConfigurator* style);
+
+    void SetStructureSet(
+      int depth, 
+      const boost::shared_ptr<DicomStructureSetLoader>& volume);
+
+
+
+  private:
+    void DisplayFloatingCtrlInfoText(const PointerEvent& e);
+    void DisplayInfoText();
+    void HideInfoText();
+
+  private:
+    CoordinateSystem3D  plane_;
+
+    boost::shared_ptr<VolumeSceneLayerSource>  source1_, source2_, source3_;
+
+    /**
+    WARNING: the measuring tools do store a reference to the scene, and it
+    paramount that the scene gets destroyed AFTER the measurement tools.
+    */
+    boost::shared_ptr<ViewportController> controller_;
+
+    std::map<std::string, std::string> infoTextMap_;
+    boost::shared_ptr<IFlexiblePointerTracker> activeTracker_;
+
+    //static const int LAYER_POSITION = 150;
+
+    int TEXTURE_2x2_1_ZINDEX;
+    int TEXTURE_1x1_ZINDEX;
+    int TEXTURE_2x2_2_ZINDEX;
+    int LINESET_1_ZINDEX;
+    int LINESET_2_ZINDEX;
+    int FLOATING_INFOTEXT_LAYER_ZINDEX;
+    int FIXED_INFOTEXT_LAYER_ZINDEX;
+
+    FusionMprGuiTool currentTool_;
+    boost::shared_ptr<UndoStack> undoStack_;
+    SdlOpenGLViewport viewport_;
+  };
+
+}
+
+
+ 
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/Sdl/Loader.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,518 @@
+/**
+ * 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 "../../Framework/Loaders/DicomStructureSetLoader.h"
+#include "../../Framework/Loaders/OrthancMultiframeVolumeLoader.h"
+#include "../../Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h"
+#include "../../Framework/Oracle/SleepOracleCommand.h"
+#include "../../Framework/Oracle/ThreadedOracle.h"
+#include "../../Framework/Scene2D/CairoCompositor.h"
+#include "../../Framework/Scene2D/GrayscaleStyleConfigurator.h"
+#include "../../Framework/Scene2D/LookupTableStyleConfigurator.h"
+#include "../../Framework/StoneInitialization.h"
+#include "../../Framework/Volumes/VolumeSceneLayerSource.h"
+#include "../../Framework/Volumes/DicomVolumeImageMPRSlicer.h"
+#include "../../Framework/Volumes/DicomVolumeImageReslicer.h"
+
+// From Orthanc framework
+#include <Core/Images/ImageProcessing.h>
+#include <Core/Images/PngWriter.h>
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+#include <Core/SystemToolbox.h>
+
+
+namespace OrthancStone
+{
+  class NativeApplicationContext : public IMessageEmitter
+  {
+  private:
+    boost::shared_mutex  mutex_;
+    MessageBroker        broker_;
+    IObservable          oracleObservable_;
+
+  public:
+    NativeApplicationContext() :
+      oracleObservable_(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:
+      NativeApplicationContext&                that_;
+      boost::shared_lock<boost::shared_mutex>  lock_;
+
+    public:
+      ReaderLock(NativeApplicationContext& that) : 
+        that_(that),
+        lock_(that.mutex_)
+      {
+      }
+    };
+
+
+    class WriterLock : public boost::noncopyable
+    {
+    private:
+      NativeApplicationContext&                that_;
+      boost::unique_lock<boost::shared_mutex>  lock_;
+
+    public:
+      WriterLock(NativeApplicationContext& that) : 
+        that_(that),
+        lock_(that.mutex_)
+      {
+      }
+
+      MessageBroker& GetBroker() 
+      {
+        return that_.broker_;
+      }
+
+      IObservable& GetOracleObservable()
+      {
+        return that_.oracleObservable_;
+      }
+    };
+  };
+}
+
+
+
+class Toto : public OrthancStone::IObserver
+{
+private:
+  OrthancStone::CoordinateSystem3D  plane_;
+  OrthancStone::IOracle&            oracle_;
+  OrthancStone::Scene2D             scene_;
+  std::unique_ptr<OrthancStone::VolumeSceneLayerSource>  source1_, source2_, source3_;
+
+
+  void Refresh()
+  {
+    if (source1_.get() != NULL)
+    {
+      source1_->Update(plane_);
+    }
+      
+    if (source2_.get() != NULL)
+    {
+      source2_->Update(plane_);
+    }
+
+    if (source3_.get() != NULL)
+    {
+      source3_->Update(plane_);
+    }
+
+    scene_.FitContent(1024, 768);
+      
+    {
+      OrthancStone::CairoCompositor compositor(scene_, 1024, 768);
+      compositor.Refresh();
+        
+      Orthanc::ImageAccessor accessor;
+      compositor.GetCanvas().GetReadOnlyAccessor(accessor);
+
+      Orthanc::Image tmp(Orthanc::PixelFormat_RGB24, accessor.GetWidth(), accessor.GetHeight(), false);
+      Orthanc::ImageProcessing::Convert(tmp, accessor);
+        
+      static unsigned int count = 0;
+      char buf[64];
+      sprintf(buf, "scene-%06d.png", count++);
+        
+      Orthanc::PngWriter writer;
+      writer.WriteToFile(buf, tmp);
+    }
+  }
+
+  
+  void Handle(const OrthancStone::DicomVolumeImage::GeometryReadyMessage& message)
+  {
+    printf("Geometry ready\n");
+    
+    plane_ = message.GetOrigin().GetGeometry().GetSagittalGeometry();
+    //plane_ = message.GetOrigin().GetGeometry().GetAxialGeometry();
+    //plane_ = message.GetOrigin().GetGeometry().GetCoronalGeometry();
+    plane_.SetOrigin(message.GetOrigin().GetGeometry().GetCoordinates(0.5f, 0.5f, 0.5f));
+
+    Refresh();
+  }
+  
+  
+  void Handle(const OrthancStone::SleepOracleCommand::TimeoutMessage& message)
+  {
+    if (message.GetOrigin().HasPayload())
+    {
+      printf("TIMEOUT! %d\n", dynamic_cast<const Orthanc::SingleValueObject<unsigned int>& >(message.GetOrigin().GetPayload()).GetValue());
+    }
+    else
+    {
+      printf("TIMEOUT\n");
+
+      Refresh();
+
+      /**
+       * The sleep() leads to a crash if the oracle is still running,
+       * while this object is destroyed. Always stop the oracle before
+       * destroying active objects.  (*)
+       **/
+      // boost::this_thread::sleep(boost::posix_time::seconds(2));
+
+      oracle_.Schedule(*this, new OrthancStone::SleepOracleCommand(message.GetOrigin().GetDelay()));
+    }
+  }
+
+  void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message)
+  {
+    Json::Value v;
+    message.ParseJsonBody(v);
+
+    printf("ICI [%s]\n", v.toStyledString().c_str());
+  }
+
+  void Handle(const OrthancStone::GetOrthancImageCommand::SuccessMessage& message)
+  {
+    printf("IMAGE %dx%d\n", message.GetImage().GetWidth(), message.GetImage().GetHeight());
+  }
+
+  void Handle(const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message)
+  {
+    printf("WebViewer %dx%d\n", message.GetImage().GetWidth(), message.GetImage().GetHeight());
+  }
+
+  void Handle(const OrthancStone::OracleCommandExceptionMessage& message)
+  {
+    printf("EXCEPTION: [%s] on command type %d\n", message.GetException().What(), message.GetCommand().GetType());
+
+    switch (message.GetCommand().GetType())
+    {
+      case OrthancStone::IOracleCommand::Type_GetOrthancWebViewerJpeg:
+        printf("URI: [%s]\n", dynamic_cast<const OrthancStone::GetOrthancWebViewerJpegCommand&>
+               (message.GetCommand()).GetUri().c_str());
+        break;
+      
+      default:
+        break;
+    }
+  }
+
+public:
+  Toto(OrthancStone::IOracle& oracle,
+       OrthancStone::IObservable& oracleObservable) :
+    IObserver(oracleObservable.GetBroker()),
+    oracle_(oracle)
+  {
+    oracleObservable.RegisterObserverCallback
+      (new OrthancStone::Callable
+       <Toto, OrthancStone::SleepOracleCommand::TimeoutMessage>(*this, &Toto::Handle));
+
+    oracleObservable.RegisterObserverCallback
+      (new OrthancStone::Callable
+       <Toto, OrthancStone::OrthancRestApiCommand::SuccessMessage>(*this, &Toto::Handle));
+
+    oracleObservable.RegisterObserverCallback
+      (new OrthancStone::Callable
+       <Toto, OrthancStone::GetOrthancImageCommand::SuccessMessage>(*this, &Toto::Handle));
+
+    oracleObservable.RegisterObserverCallback
+      (new OrthancStone::Callable
+       <Toto, OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage>(*this, &Toto::Handle));
+
+    oracleObservable.RegisterObserverCallback
+      (new OrthancStone::Callable
+       <Toto, OrthancStone::OracleCommandExceptionMessage>(*this, &Toto::Handle));
+  }
+
+  void SetReferenceLoader(OrthancStone::IObservable& loader)
+  {
+    loader.RegisterObserverCallback
+      (new OrthancStone::Callable
+       <Toto, OrthancStone::DicomVolumeImage::GeometryReadyMessage>(*this, &Toto::Handle));
+  }
+
+  void SetVolume1(int depth,
+                  const boost::shared_ptr<OrthancStone::IVolumeSlicer>& volume,
+                  OrthancStone::ILayerStyleConfigurator* style)
+  {
+    source1_.reset(new OrthancStone::VolumeSceneLayerSource(scene_, depth, volume));
+
+    if (style != NULL)
+    {
+      source1_->SetConfigurator(style);
+    }
+  }
+
+  void SetVolume2(int depth,
+                  const boost::shared_ptr<OrthancStone::IVolumeSlicer>& volume,
+                  OrthancStone::ILayerStyleConfigurator* style)
+  {
+    source2_.reset(new OrthancStone::VolumeSceneLayerSource(scene_, depth, volume));
+
+    if (style != NULL)
+    {
+      source2_->SetConfigurator(style);
+    }
+  }
+
+  void SetStructureSet(int depth,
+                       const boost::shared_ptr<OrthancStone::DicomStructureSetLoader>& volume)
+  {
+    source3_.reset(new OrthancStone::VolumeSceneLayerSource(scene_, depth, volume));
+  }
+                       
+};
+
+
+void Run(OrthancStone::NativeApplicationContext& context,
+         OrthancStone::ThreadedOracle& oracle)
+{
+  // the oracle has been supplied with the context (as an IEmitter) upon
+  // creation
+  boost::shared_ptr<OrthancStone::DicomVolumeImage>  ct(new OrthancStone::DicomVolumeImage);
+  boost::shared_ptr<OrthancStone::DicomVolumeImage>  dose(new OrthancStone::DicomVolumeImage);
+
+  
+  boost::shared_ptr<Toto> toto;
+  boost::shared_ptr<OrthancStone::OrthancSeriesVolumeProgressiveLoader> ctLoader;
+  boost::shared_ptr<OrthancStone::OrthancMultiframeVolumeLoader> doseLoader;
+  boost::shared_ptr<OrthancStone::DicomStructureSetLoader>  rtstructLoader;
+
+  {
+    OrthancStone::NativeApplicationContext::WriterLock lock(context);
+    toto.reset(new Toto(oracle, lock.GetOracleObservable()));
+
+    // the oracle is used to schedule commands
+    // the oracleObservable is used by the loaders to:
+    // - request the broker (lifetime mgmt)
+    // - register the loader callbacks (called indirectly by the oracle)
+    ctLoader.reset(new OrthancStone::OrthancSeriesVolumeProgressiveLoader(ct, oracle, lock.GetOracleObservable()));
+    doseLoader.reset(new OrthancStone::OrthancMultiframeVolumeLoader(dose, oracle, lock.GetOracleObservable()));
+    rtstructLoader.reset(new OrthancStone::DicomStructureSetLoader(oracle, lock.GetOracleObservable()));
+  }
+
+
+  //toto->SetReferenceLoader(*ctLoader);
+  toto->SetReferenceLoader(*doseLoader);
+
+
+#if 1
+  toto->SetVolume1(0, ctLoader, new OrthancStone::GrayscaleStyleConfigurator);
+#else
+  {
+    boost::shared_ptr<OrthancStone::IVolumeSlicer> reslicer(new OrthancStone::DicomVolumeImageReslicer(ct));
+    toto->SetVolume1(0, reslicer, new OrthancStone::GrayscaleStyleConfigurator);
+  }
+#endif  
+  
+
+  {
+    std::unique_ptr<OrthancStone::LookupTableStyleConfigurator> config(new OrthancStone::LookupTableStyleConfigurator);
+    config->SetLookupTable(Orthanc::EmbeddedResources::COLORMAP_HOT);
+
+    boost::shared_ptr<OrthancStone::DicomVolumeImageMPRSlicer> tmp(new OrthancStone::DicomVolumeImageMPRSlicer(dose));
+    toto->SetVolume2(1, tmp, config.release());
+  }
+
+  toto->SetStructureSet(2, rtstructLoader);
+  
+  oracle.Schedule(*toto, new OrthancStone::SleepOracleCommand(100));
+
+  if (0)
+  {
+    Json::Value v = Json::objectValue;
+    v["Level"] = "Series";
+    v["Query"] = Json::objectValue;
+
+    std::unique_ptr<OrthancStone::OrthancRestApiCommand>  command(new OrthancStone::OrthancRestApiCommand);
+    command->SetMethod(Orthanc::HttpMethod_Post);
+    command->SetUri("/tools/find");
+    command->SetBody(v);
+
+    oracle.Schedule(*toto, command.release());
+  }
+  
+  if(0)
+  {
+    if (0)
+    {
+      std::unique_ptr<OrthancStone::GetOrthancImageCommand>  command(new OrthancStone::GetOrthancImageCommand);
+      command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Jpeg)));
+      command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/preview");
+      oracle.Schedule(*toto, command.release());
+    }
+    
+    if (0)
+    {
+      std::unique_ptr<OrthancStone::GetOrthancImageCommand>  command(new OrthancStone::GetOrthancImageCommand);
+      command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Png)));
+      command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/preview");
+      oracle.Schedule(*toto, command.release());
+    }
+    
+    if (0)
+    {
+      std::unique_ptr<OrthancStone::GetOrthancImageCommand>  command(new OrthancStone::GetOrthancImageCommand);
+      command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Png)));
+      command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/image-uint16");
+      oracle.Schedule(*toto, command.release());
+    }
+    
+    if (0)
+    {
+      std::unique_ptr<OrthancStone::GetOrthancImageCommand>  command(new OrthancStone::GetOrthancImageCommand);
+      command->SetHttpHeader("Accept-Encoding", "gzip");
+      command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam)));
+      command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/image-uint16");
+      oracle.Schedule(*toto, command.release());
+    }
+    
+    if (0)
+    {
+      std::unique_ptr<OrthancStone::GetOrthancImageCommand>  command(new OrthancStone::GetOrthancImageCommand);
+      command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam)));
+      command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/image-uint16");
+      oracle.Schedule(*toto, command.release());
+    }
+
+    if (0)
+    {
+      std::unique_ptr<OrthancStone::GetOrthancWebViewerJpegCommand>  command(new OrthancStone::GetOrthancWebViewerJpegCommand);
+      command->SetHttpHeader("Accept-Encoding", "gzip");
+      command->SetInstance("e6c7c20b-c9f65d7e-0d76f2e2-830186f2-3e3c600e");
+      command->SetQuality(90);
+      oracle.Schedule(*toto, command.release());
+    }
+
+
+    if (0)
+    {
+      for (unsigned int i = 0; i < 10; i++)
+      {
+        std::unique_ptr<OrthancStone::SleepOracleCommand> command(new OrthancStone::SleepOracleCommand(i * 1000));
+        command->SetPayload(new Orthanc::SingleValueObject<unsigned int>(42 * i));
+        oracle.Schedule(*toto, command.release());
+      }
+    }
+  }
+  
+  // 2017-11-17-Anonymized
+#if 0
+  // BGO data
+  ctLoader->LoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");  // CT
+  doseLoader->LoadInstance("830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb");  // RT-DOSE
+  //rtstructLoader->LoadInstance("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9");  // RT-STRUCT
+#else
+  //ctLoader->LoadSeries("cb3ea4d1-d08f3856-ad7b6314-74d88d77-60b05618");  // CT
+  //doseLoader->LoadInstance("41029085-71718346-811efac4-420e2c15-d39f99b6");  // RT-DOSE
+  //rtstructLoader->LoadInstance("83d9c0c3-913a7fee-610097d7-cbf0522d-fd75bee6");  // RT-STRUCT
+
+  // 2017-05-16
+  ctLoader->LoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");  // CT
+  doseLoader->LoadInstance("eac822ef-a395f94e-e8121fe0-8411fef8-1f7bffad");  // RT-DOSE
+  rtstructLoader->LoadInstance("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9");  // RT-STRUCT
+#endif
+  // 2015-01-28-Multiframe
+  //doseLoader->LoadInstance("88f71e2a-5fad1c61-96ed14d6-5b3d3cf7-a5825279");  // Multiframe CT
+  
+  // Delphine
+  //ctLoader->LoadSeries("5990e39c-51e5f201-fe87a54c-31a55943-e59ef80e");  // CT
+  //ctLoader->LoadSeries("67f1b334-02c16752-45026e40-a5b60b6b-030ecab5");  // Lung 1/10mm
+
+
+  {
+    LOG(WARNING) << "...Waiting for Ctrl-C...";
+
+    oracle.Start();
+
+    Orthanc::SystemToolbox::ServerBarrier();
+
+    /**
+     * WARNING => The oracle must be stopped BEFORE the objects using
+     * it are destroyed!!! This forces to wait for the completion of
+     * the running callback methods. Otherwise, the callbacks methods
+     * might still be running while their parent object is destroyed,
+     * resulting in crashes. This is very visible if adding a sleep(),
+     * as in (*).
+     **/
+
+    oracle.Stop();
+  }
+}
+
+
+
+/**
+ * IMPORTANT: The full arguments to "main()" are needed for SDL on
+ * Windows. Otherwise, one gets the linking error "undefined reference
+ * to `SDL_main'". https://wiki.libsdl.org/FAQWindows
+ **/
+int main(int argc, char* argv[])
+{
+  OrthancStone::StoneInitialize();
+  //Orthanc::Logging::EnableInfoLevel(true);
+
+  try
+  {
+    OrthancStone::NativeApplicationContext context;
+
+    OrthancStone::ThreadedOracle oracle(context);
+    //oracle.SetThreadsCount(1);
+
+    {
+      Orthanc::WebServiceParameters p;
+      //p.SetUrl("http://localhost:8043/");
+      p.SetCredentials("orthanc", "orthanc");
+      oracle.SetOrthancParameters(p);
+    }
+
+    //oracle.Start();
+
+    Run(context, oracle);
+    
+    //oracle.Stop();
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    LOG(ERROR) << "EXCEPTION: " << e.What();
+  }
+
+  OrthancStone::StoneFinalize();
+
+  return 0;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/Sdl/RadiographyEditor.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,267 @@
+/**
+ * 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 "../Shared/RadiographyEditorApp.h"
+
+// From Stone
+#include "../../Framework/Oracle/SleepOracleCommand.h"
+#include "../../Framework/Oracle/ThreadedOracle.h"
+#include "../../Applications/Sdl/SdlOpenGLWindow.h"
+#include "../../Framework/Scene2D/OpenGLCompositor.h"
+#include "../../Framework/Scene2D/CairoCompositor.h"
+#include "../../Framework/Scene2D/ColorTextureSceneLayer.h"
+#include "../../Framework/Scene2D/OpenGLCompositor.h"
+#include "../../Framework/StoneInitialization.h"
+
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+
+
+#include <boost/shared_ptr.hpp>
+#include <boost/weak_ptr.hpp>
+
+#include <SDL.h>
+#include <stdio.h>
+
+using namespace OrthancStone;
+
+namespace OrthancStone
+{
+  class NativeApplicationContext : public IMessageEmitter
+  {
+  private:
+    boost::shared_mutex  mutex_;
+    MessageBroker        broker_;
+    IObservable          oracleObservable_;
+
+  public:
+    NativeApplicationContext() :
+      oracleObservable_(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:
+      NativeApplicationContext&                that_;
+      boost::shared_lock<boost::shared_mutex>  lock_;
+
+    public:
+      ReaderLock(NativeApplicationContext& that) :
+        that_(that),
+        lock_(that.mutex_)
+      {
+      }
+    };
+
+
+    class WriterLock : public boost::noncopyable
+    {
+    private:
+      NativeApplicationContext&                that_;
+      boost::unique_lock<boost::shared_mutex>  lock_;
+
+    public:
+      WriterLock(NativeApplicationContext& that) :
+        that_(that),
+        lock_(that.mutex_)
+      {
+      }
+
+      MessageBroker& GetBroker()
+      {
+        return that_.broker_;
+      }
+
+      IObservable& GetOracleObservable()
+      {
+        return that_.oracleObservable_;
+      }
+    };
+  };
+}
+
+class OpenGlSdlCompositorFactory : public ICompositorFactory
+{
+  OpenGL::IOpenGLContext& openGlContext_;
+
+public:
+  OpenGlSdlCompositorFactory(OpenGL::IOpenGLContext& openGlContext) :
+    openGlContext_(openGlContext)
+  {}
+
+  ICompositor* GetCompositor(const Scene2D& scene)
+  {
+
+    OpenGLCompositor* compositor = new OpenGLCompositor(openGlContext_, scene);
+    compositor->SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT,
+                         FONT_SIZE_0, Orthanc::Encoding_Latin1);
+    compositor->SetFont(1, Orthanc::EmbeddedResources::UBUNTU_FONT,
+                         FONT_SIZE_1, Orthanc::Encoding_Latin1);
+    return compositor;
+  }
+};
+
+static void GLAPIENTRY
+OpenGLMessageCallback(GLenum source,
+                      GLenum type,
+                      GLuint id,
+                      GLenum severity,
+                      GLsizei length,
+                      const GLchar* message,
+                      const void* userParam)
+{
+  if (severity != GL_DEBUG_SEVERITY_NOTIFICATION)
+  {
+    fprintf(stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n",
+            (type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : ""),
+            type, severity, message);
+  }
+}
+
+
+/**
+ * IMPORTANT: The full arguments to "main()" are needed for SDL on
+ * Windows. Otherwise, one gets the linking error "undefined reference
+ * to `SDL_main'". https://wiki.libsdl.org/FAQWindows
+ **/
+int main(int argc, char* argv[])
+{
+  using namespace OrthancStone;
+
+  StoneInitialize();
+  Orthanc::Logging::EnableInfoLevel(true);
+  //  Orthanc::Logging::EnableTraceLevel(true);
+
+  try
+  {
+    OrthancStone::NativeApplicationContext context;
+    OrthancStone::NativeApplicationContext::WriterLock lock(context);
+    OrthancStone::ThreadedOracle oracle(context);
+
+    // False means we do NOT let Windows treat this as a legacy application
+    // that needs to be scaled
+    SdlOpenGLWindow window("Hello", 1024, 1024, false);
+
+    glEnable(GL_DEBUG_OUTPUT);
+    glDebugMessageCallback(OpenGLMessageCallback, 0);
+
+    std::unique_ptr<OpenGlSdlCompositorFactory> compositorFactory(new OpenGlSdlCompositorFactory(window));
+    boost::shared_ptr<RadiographyEditorApp> app(new RadiographyEditorApp(oracle, lock.GetOracleObservable(), compositorFactory.release()));
+    app->PrepareScene();
+    app->FitContent(window.GetCanvasWidth(), window.GetCanvasHeight());
+
+    bool stopApplication = false;
+
+    while (!stopApplication)
+    {
+      app->Refresh();
+
+      SDL_Event event;
+      while (!stopApplication && SDL_PollEvent(&event))
+      {
+        OrthancStone::KeyboardModifiers modifiers = OrthancStone::KeyboardModifiers_None;
+        if (event.key.keysym.mod & KMOD_CTRL)
+          modifiers = static_cast<OrthancStone::KeyboardModifiers>(static_cast<int>(modifiers) | static_cast<int>(OrthancStone::KeyboardModifiers_Control));
+        if (event.key.keysym.mod & KMOD_ALT)
+          modifiers = static_cast<OrthancStone::KeyboardModifiers>(static_cast<int>(modifiers) | static_cast<int>(OrthancStone::KeyboardModifiers_Alt));
+        if (event.key.keysym.mod & KMOD_SHIFT)
+          modifiers = static_cast<OrthancStone::KeyboardModifiers>(static_cast<int>(modifiers) | static_cast<int>(OrthancStone::KeyboardModifiers_Shift));
+
+        OrthancStone::MouseButton button;
+        if (event.button.button == SDL_BUTTON_LEFT)
+          button = OrthancStone::MouseButton_Left;
+        else if (event.button.button == SDL_BUTTON_MIDDLE)
+          button = OrthancStone::MouseButton_Middle;
+        else if (event.button.button == SDL_BUTTON_RIGHT)
+          button = OrthancStone::MouseButton_Right;
+
+        if (event.type == SDL_QUIT)
+        {
+          stopApplication = true;
+          break;
+        }
+        else if (event.type == SDL_WINDOWEVENT &&
+                 event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
+        {
+          app->DisableTracker(); // was: tracker.reset(NULL);
+          app->UpdateSize();
+        }
+        else if (event.type == SDL_KEYDOWN &&
+                 event.key.repeat == 0 /* Ignore key bounce */)
+        {
+          switch (event.key.keysym.sym)
+          {
+          case SDLK_f:
+            window.GetWindow().ToggleMaximize();
+            break;
+
+          case SDLK_q:
+            stopApplication = true;
+            break;
+          default:
+          {
+            app->OnKeyPressed(event.key.keysym.sym, modifiers);
+           }
+          }
+        }
+        else if (event.type == SDL_MOUSEBUTTONDOWN)
+        {
+          app->OnMouseDown(event.button.x, event.button.y, modifiers, button);
+        }
+        else if (event.type == SDL_MOUSEMOTION)
+        {
+          app->OnMouseMove(event.button.x, event.button.y, modifiers);
+        }
+        else if (event.type == SDL_MOUSEBUTTONUP)
+        {
+          app->OnMouseUp(event.button.x, event.button.y, modifiers, button);
+        }
+      }
+      SDL_Delay(1);
+    }
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    LOG(ERROR) << "EXCEPTION: " << e.What();
+  }
+
+  StoneFinalize();
+
+  return 0;
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/Sdl/TrackerSample.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,93 @@
+/**
+ * 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 "TrackerSampleApp.h"
+
+ // From Stone
+#include "../../Framework/OpenGL/SdlOpenGLContext.h"
+#include "../../Framework/Scene2D/CairoCompositor.h"
+#include "../../Framework/Scene2D/ColorTextureSceneLayer.h"
+#include "../../Framework/Scene2D/OpenGLCompositor.h"
+#include "../../Framework/StoneInitialization.h"
+
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+
+
+#include <boost/shared_ptr.hpp>
+#include <boost/weak_ptr.hpp>
+
+#include <SDL.h>
+#include <stdio.h>
+
+/*
+TODO:
+
+- to decouple the trackers from the sample, we need to supply them with
+  the scene rather than the app
+
+- in order to do that, we need a GetNextFreeZIndex function (or something 
+  along those lines) in the scene object
+
+*/
+
+boost::weak_ptr<OrthancStone::TrackerSampleApp> g_app;
+
+void TrackerSample_SetInfoDisplayMessage(std::string key, std::string value)
+{
+  boost::shared_ptr<OrthancStone::TrackerSampleApp> app = g_app.lock();
+  if (app)
+  {
+    app->SetInfoDisplayMessage(key, value);
+  }
+}
+
+/**
+ * IMPORTANT: The full arguments to "main()" are needed for SDL on
+ * Windows. Otherwise, one gets the linking error "undefined reference
+ * to `SDL_main'". https://wiki.libsdl.org/FAQWindows
+ **/
+int main(int argc, char* argv[])
+{
+  using namespace OrthancStone;
+
+  StoneInitialize();
+  Orthanc::Logging::EnableInfoLevel(true);
+//  Orthanc::Logging::EnableTraceLevel(true);
+
+  try
+  {
+    MessageBroker broker;
+    boost::shared_ptr<TrackerSampleApp> app(new TrackerSampleApp(broker));
+    g_app = app;
+    app->PrepareScene();
+    app->Run();
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    LOG(ERROR) << "EXCEPTION: " << e.What();
+  }
+
+  StoneFinalize();
+
+  return 0;
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/Sdl/TrackerSampleApp.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,733 @@
+/**
+ * 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 "TrackerSampleApp.h"
+
+#include "../../Framework/OpenGL/SdlOpenGLContext.h"
+#include "../../Framework/Scene2D/CairoCompositor.h"
+#include "../../Framework/Scene2D/ColorTextureSceneLayer.h"
+#include "../../Framework/Scene2D/OpenGLCompositor.h"
+#include "../../Framework/Scene2D/PanSceneTracker.h"
+#include "../../Framework/Scene2D/RotateSceneTracker.h"
+#include "../../Framework/Scene2D/Scene2D.h"
+#include "../../Framework/Scene2D/ZoomSceneTracker.h"
+#include "../../Framework/Scene2DViewport/UndoStack.h"
+#include "../../Framework/Scene2DViewport/CreateAngleMeasureTracker.h"
+#include "../../Framework/Scene2DViewport/CreateLineMeasureTracker.h"
+#include "../../Framework/StoneInitialization.h"
+
+// From Orthanc framework
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+#include <Core/Images/Image.h>
+#include <Core/Images/ImageProcessing.h>
+#include <Core/Images/PngWriter.h>
+
+#include <boost/ref.hpp>
+#include <boost/make_shared.hpp>
+#include <SDL.h>
+
+#include <stdio.h>
+
+namespace OrthancStone
+{
+  const char* MeasureToolToString(size_t i)
+  {
+    static const char* descs[] = {
+      "GuiTool_Rotate",
+      "GuiTool_Pan",
+      "GuiTool_Zoom",
+      "GuiTool_LineMeasure",
+      "GuiTool_CircleMeasure",
+      "GuiTool_AngleMeasure",
+      "GuiTool_EllipseMeasure",
+      "GuiTool_LAST"
+    };
+    if (i >= GuiTool_LAST)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "Wrong tool index");
+    }
+    return descs[i];
+  }
+
+  void TrackerSampleApp::SelectNextTool()
+  {
+    currentTool_ = static_cast<GuiTool>(currentTool_ + 1);
+    if (currentTool_ == GuiTool_LAST)
+      currentTool_ = static_cast<GuiTool>(0);;
+    printf("Current tool is now: %s\n", MeasureToolToString(currentTool_));
+  }
+
+  void TrackerSampleApp::DisplayInfoText()
+  {
+    // do not try to use stuff too early!
+    std::stringstream msg;
+	
+	for (std::map<std::string, std::string>::const_iterator kv = infoTextMap_.begin();
+		kv != infoTextMap_.end(); ++kv)
+    {
+      msg << kv->first << " : " << kv->second << std::endl;
+    }
+	std::string msgS = msg.str();
+
+    TextSceneLayer* layerP = NULL;
+    if (controller_->GetScene().HasLayer(FIXED_INFOTEXT_LAYER_ZINDEX))
+    {
+      TextSceneLayer& layer = dynamic_cast<TextSceneLayer&>(
+        controller_->GetScene().GetLayer(FIXED_INFOTEXT_LAYER_ZINDEX));
+      layerP = &layer;
+    }
+    else
+    {
+      std::unique_ptr<TextSceneLayer> layer(new TextSceneLayer);
+      layerP = layer.get();
+      layer->SetColor(0, 255, 0);
+      layer->SetFontIndex(1);
+      layer->SetBorder(20);
+      layer->SetAnchor(BitmapAnchor_TopLeft);
+      //layer->SetPosition(0,0);
+      controller_->GetScene().SetLayer(FIXED_INFOTEXT_LAYER_ZINDEX, layer.release());
+    }
+    // position the fixed info text in the upper right corner
+    layerP->SetText(msgS.c_str());
+    double cX = GetCompositor().GetCanvasWidth() * (-0.5);
+    double cY = GetCompositor().GetCanvasHeight() * (-0.5);
+    controller_->GetScene().GetCanvasToSceneTransform().Apply(cX,cY);
+    layerP->SetPosition(cX, cY);
+  }
+
+  void TrackerSampleApp::DisplayFloatingCtrlInfoText(const PointerEvent& e)
+  {
+    ScenePoint2D p = e.GetMainPosition().Apply(controller_->GetScene().GetCanvasToSceneTransform());
+
+    char buf[128];
+    sprintf(buf, "S:(%0.02f,%0.02f) C:(%0.02f,%0.02f)", 
+      p.GetX(), p.GetY(), 
+      e.GetMainPosition().GetX(), e.GetMainPosition().GetY());
+
+    if (controller_->GetScene().HasLayer(FLOATING_INFOTEXT_LAYER_ZINDEX))
+    {
+      TextSceneLayer& layer =
+        dynamic_cast<TextSceneLayer&>(controller_->GetScene().GetLayer(FLOATING_INFOTEXT_LAYER_ZINDEX));
+      layer.SetText(buf);
+      layer.SetPosition(p.GetX(), p.GetY());
+    }
+    else
+    {
+      std::unique_ptr<TextSceneLayer> layer(new TextSceneLayer);
+      layer->SetColor(0, 255, 0);
+      layer->SetText(buf);
+      layer->SetBorder(20);
+      layer->SetAnchor(BitmapAnchor_BottomCenter);
+      layer->SetPosition(p.GetX(), p.GetY());
+      controller_->GetScene().SetLayer(FLOATING_INFOTEXT_LAYER_ZINDEX, layer.release());
+    }
+  }
+
+  void TrackerSampleApp::HideInfoText()
+  {
+    controller_->GetScene().DeleteLayer(FLOATING_INFOTEXT_LAYER_ZINDEX);
+  }
+
+  ScenePoint2D TrackerSampleApp::GetRandomPointInScene() const
+  {
+    unsigned int w = GetCompositor().GetCanvasWidth();
+    LOG(TRACE) << "GetCompositor().GetCanvasWidth() = " << 
+      GetCompositor().GetCanvasWidth();
+    unsigned int h = GetCompositor().GetCanvasHeight();
+    LOG(TRACE) << "GetCompositor().GetCanvasHeight() = " << 
+      GetCompositor().GetCanvasHeight();
+
+    if ((w >= RAND_MAX) || (h >= RAND_MAX))
+      LOG(WARNING) << "Canvas is too big : tools will not be randomly placed";
+
+    int x = rand() % w;
+    int y = rand() % h;
+    LOG(TRACE) << "random x = " << x << "random y = " << y;
+
+    ScenePoint2D p = controller_->GetViewport().GetPixelCenterCoordinates(x, y);
+    LOG(TRACE) << "--> p.GetX() = " << p.GetX() << " p.GetY() = " << p.GetY();
+
+    ScenePoint2D r = p.Apply(controller_->GetScene().GetCanvasToSceneTransform());
+    LOG(TRACE) << "--> r.GetX() = " << r.GetX() << " r.GetY() = " << r.GetY();
+    return r;
+  }
+
+  void TrackerSampleApp::CreateRandomMeasureTool()
+  {
+    static bool srandCalled = false;
+    if (!srandCalled)
+    {
+      srand(42);
+      srandCalled = true;
+    }
+
+    int i = rand() % 2;
+    LOG(TRACE) << "random i = " << i;
+    switch (i)
+    {
+    case 0:
+      // line measure
+      {
+        boost::shared_ptr<CreateLineMeasureCommand> cmd = 
+          boost::make_shared<CreateLineMeasureCommand>(
+		  boost::ref(IObserver::GetBroker()),
+            controller_,
+            GetRandomPointInScene());
+        cmd->SetEnd(GetRandomPointInScene());
+        controller_->PushCommand(cmd);
+      }
+      break;
+    case 1:
+      // angle measure
+      {
+      boost::shared_ptr<CreateAngleMeasureCommand> cmd =
+        boost::make_shared<CreateAngleMeasureCommand>(
+          boost::ref(IObserver::GetBroker()),
+          controller_,
+          GetRandomPointInScene());
+        cmd->SetCenter(GetRandomPointInScene());
+        cmd->SetSide2End(GetRandomPointInScene());
+        controller_->PushCommand(cmd);
+      }
+      break;
+    }
+  }
+
+  void TrackerSampleApp::HandleApplicationEvent(
+    const SDL_Event & event)
+  {
+    DisplayInfoText();
+
+    if (event.type == SDL_MOUSEMOTION)
+    {
+      int scancodeCount = 0;
+      const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount);
+
+      if (activeTracker_.get() == NULL &&
+        SDL_SCANCODE_LALT < scancodeCount &&
+        keyboardState[SDL_SCANCODE_LALT])
+      {
+        // The "left-ctrl" key is down, while no tracker is present
+        // Let's display the info text
+        PointerEvent e;
+        e.AddPosition(controller_->GetViewport().GetPixelCenterCoordinates(
+          event.button.x, event.button.y));
+
+        DisplayFloatingCtrlInfoText(e);
+      }
+      else if (activeTracker_.get() != NULL)
+      {
+        HideInfoText();
+        //LOG(TRACE) << "(event.type == SDL_MOUSEMOTION)";
+        if (activeTracker_.get() != NULL)
+        {
+          //LOG(TRACE) << "(activeTracker_.get() != NULL)";
+          PointerEvent e;
+          e.AddPosition(controller_->GetViewport().GetPixelCenterCoordinates(
+            event.button.x, event.button.y));
+          
+          //LOG(TRACE) << "event.button.x = " << event.button.x << "     " <<
+          //  "event.button.y = " << event.button.y;
+          LOG(TRACE) << "activeTracker_->PointerMove(e); " <<
+            e.GetMainPosition().GetX() << " " << e.GetMainPosition().GetY();
+          
+          activeTracker_->PointerMove(e);
+          if (!activeTracker_->IsAlive())
+            activeTracker_.reset();
+        }
+      }
+      else
+      {
+        HideInfoText();
+
+        PointerEvent e;
+        e.AddPosition(controller_->GetViewport().GetPixelCenterCoordinates(event.button.x, event.button.y));
+
+        ScenePoint2D scenePos = e.GetMainPosition().Apply(
+          controller_->GetScene().GetCanvasToSceneTransform());
+        //auto measureTools = GetController()->HitTestMeasureTools(scenePos);
+        //LOG(TRACE) << "# of hit tests: " << measureTools.size();
+        
+        // this returns the collection of measuring tools where hit test is true
+        std::vector<boost::shared_ptr<MeasureTool> > measureTools = controller_->HitTestMeasureTools(scenePos);
+
+        // let's refresh the measuring tools highlighted state
+        // first let's tag them as "unhighlighted"
+        controller_->ResetMeasuringToolsHighlight();
+
+        // then immediately take the first one and ask it to highlight the 
+        // measuring tool UI part that is hot
+        if (measureTools.size() > 0)
+        {
+          measureTools[0]->Highlight(scenePos);
+        }
+      }
+    }
+    else if (event.type == SDL_MOUSEBUTTONUP)
+    {
+      if (activeTracker_)
+      {
+        PointerEvent e;
+        e.AddPosition(controller_->GetViewport().GetPixelCenterCoordinates(event.button.x, event.button.y));
+        activeTracker_->PointerUp(e);
+        if (!activeTracker_->IsAlive())
+          activeTracker_.reset();
+      }
+    }
+    else if (event.type == SDL_MOUSEBUTTONDOWN)
+    {
+      PointerEvent e;
+      e.AddPosition(controller_->GetViewport().GetPixelCenterCoordinates(
+        event.button.x, event.button.y));
+      if (activeTracker_)
+      {
+        activeTracker_->PointerDown(e);
+        if (!activeTracker_->IsAlive())
+          activeTracker_.reset();
+      }
+      else
+      {
+        // we ATTEMPT to create a tracker if need be
+        activeTracker_ = CreateSuitableTracker(event, e);
+      }
+    }
+    else if (event.type == SDL_KEYDOWN &&
+      event.key.repeat == 0 /* Ignore key bounce */)
+    {
+      switch (event.key.keysym.sym)
+      {
+      case SDLK_ESCAPE:
+        if (activeTracker_)
+        {
+          activeTracker_->Cancel();
+          if (!activeTracker_->IsAlive())
+            activeTracker_.reset();
+        }
+        break;
+
+      case SDLK_t:
+        if (!activeTracker_)
+          SelectNextTool();
+        else
+        {
+          LOG(WARNING) << "You cannot change the active tool when an interaction"
+            " is taking place";
+        }
+        break;
+
+      case SDLK_m:
+        CreateRandomMeasureTool();
+        break;
+      case SDLK_s:
+        controller_->FitContent(GetCompositor().GetCanvasWidth(),
+          GetCompositor().GetCanvasHeight());
+        break;
+
+      case SDLK_z:
+        LOG(TRACE) << "SDLK_z has been pressed. event.key.keysym.mod == " << event.key.keysym.mod;
+        if (event.key.keysym.mod & KMOD_CTRL)
+        {
+          if (controller_->CanUndo())
+          {
+            LOG(TRACE) << "Undoing...";
+            controller_->Undo();
+          }
+          else
+          {
+            LOG(WARNING) << "Nothing to undo!!!";
+          }
+        }
+        break;
+
+      case SDLK_y:
+        LOG(TRACE) << "SDLK_y has been pressed. event.key.keysym.mod == " << event.key.keysym.mod;
+        if (event.key.keysym.mod & KMOD_CTRL)
+        {
+          if (controller_->CanRedo())
+          {
+            LOG(TRACE) << "Redoing...";
+            controller_->Redo();
+          }
+          else
+          {
+            LOG(WARNING) << "Nothing to redo!!!";
+          }
+        }
+        break;
+
+      case SDLK_c:
+        TakeScreenshot(
+          "screenshot.png",
+          GetCompositor().GetCanvasWidth(),
+          GetCompositor().GetCanvasHeight());
+        break;
+
+      default:
+        break;
+      }
+    }
+  }
+
+
+  void TrackerSampleApp::OnSceneTransformChanged(
+    const ViewportController::SceneTransformChanged& message)
+  {
+    DisplayInfoText();
+  }
+
+  boost::shared_ptr<IFlexiblePointerTracker> TrackerSampleApp::CreateSuitableTracker(
+    const SDL_Event & event,
+    const PointerEvent & e)
+  {
+    using namespace Orthanc;
+
+    switch (event.button.button)
+    {
+    case SDL_BUTTON_MIDDLE:
+      return boost::shared_ptr<IFlexiblePointerTracker>(new PanSceneTracker
+        (controller_, e));
+
+    case SDL_BUTTON_RIGHT:
+      return boost::shared_ptr<IFlexiblePointerTracker>(new ZoomSceneTracker
+        (controller_, e, GetCompositor().GetCanvasHeight()));
+
+    case SDL_BUTTON_LEFT:
+    {
+      //LOG(TRACE) << "CreateSuitableTracker: case SDL_BUTTON_LEFT:";
+      // TODO: we need to iterate on the set of measuring tool and perform
+      // a hit test to check if a tracker needs to be created for edition.
+      // Otherwise, depending upon the active tool, we might want to create
+      // a "measuring tool creation" tracker
+
+      // TODO: if there are conflicts, we should prefer a tracker that 
+      // pertains to the type of measuring tool currently selected (TBD?)
+      boost::shared_ptr<IFlexiblePointerTracker> hitTestTracker = TrackerHitTest(e);
+
+      if (hitTestTracker != NULL)
+      {
+        //LOG(TRACE) << "hitTestTracker != NULL";
+        return hitTestTracker;
+      }
+      else
+      {
+        switch (currentTool_)
+        {
+        case GuiTool_Rotate:
+          //LOG(TRACE) << "Creating RotateSceneTracker";
+          return boost::shared_ptr<IFlexiblePointerTracker>(new RotateSceneTracker(
+            controller_, e));
+        case GuiTool_Pan:
+          return boost::shared_ptr<IFlexiblePointerTracker>(new PanSceneTracker(
+            controller_, e));
+        case GuiTool_Zoom:
+          return boost::shared_ptr<IFlexiblePointerTracker>(new ZoomSceneTracker(
+            controller_, e, GetCompositor().GetCanvasHeight()));
+        //case GuiTool_AngleMeasure:
+        //  return new AngleMeasureTracker(GetScene(), e);
+        //case GuiTool_CircleMeasure:
+        //  return new CircleMeasureTracker(GetScene(), e);
+        //case GuiTool_EllipseMeasure:
+        //  return new EllipseMeasureTracker(GetScene(), e);
+        case GuiTool_LineMeasure:
+          return boost::shared_ptr<IFlexiblePointerTracker>(new CreateLineMeasureTracker(
+            IObserver::GetBroker(), controller_, e));
+        case GuiTool_AngleMeasure:
+          return boost::shared_ptr<IFlexiblePointerTracker>(new CreateAngleMeasureTracker(
+            IObserver::GetBroker(), controller_, e));
+        case GuiTool_CircleMeasure:
+          LOG(ERROR) << "Not implemented yet!";
+          return boost::shared_ptr<IFlexiblePointerTracker>();
+        case GuiTool_EllipseMeasure:
+          LOG(ERROR) << "Not implemented yet!";
+          return boost::shared_ptr<IFlexiblePointerTracker>();
+        default:
+          throw OrthancException(ErrorCode_InternalError, "Wrong tool!");
+        }
+      }
+    }
+    default:
+      return boost::shared_ptr<IFlexiblePointerTracker>();
+    }
+  }
+
+
+  TrackerSampleApp::TrackerSampleApp(MessageBroker& broker) : IObserver(broker)
+    , currentTool_(GuiTool_Rotate)
+    , undoStack_(new UndoStack)
+    , viewport_("Hello", 1024, 1024, false) // False means we do NOT let Windows treat this as a legacy application that needs to be scaled
+  {
+    controller_ = boost::shared_ptr<ViewportController>(
+      new ViewportController(undoStack_, broker, viewport_));
+
+    controller_->RegisterObserverCallback(
+      new Callable<TrackerSampleApp, ViewportController::SceneTransformChanged>
+      (*this, &TrackerSampleApp::OnSceneTransformChanged));
+
+    TEXTURE_2x2_1_ZINDEX = 1;
+    TEXTURE_1x1_ZINDEX = 2;
+    TEXTURE_2x2_2_ZINDEX = 3;
+    LINESET_1_ZINDEX = 4;
+    LINESET_2_ZINDEX = 5;
+    FLOATING_INFOTEXT_LAYER_ZINDEX = 6;
+    FIXED_INFOTEXT_LAYER_ZINDEX = 7;
+  }
+
+  void TrackerSampleApp::PrepareScene()
+  {
+    // Texture of 2x2 size
+    {
+      Orthanc::Image i(Orthanc::PixelFormat_RGB24, 2, 2, false);
+
+      uint8_t* p = reinterpret_cast<uint8_t*>(i.GetRow(0));
+      p[0] = 255;
+      p[1] = 0;
+      p[2] = 0;
+
+      p[3] = 0;
+      p[4] = 255;
+      p[5] = 0;
+
+      p = reinterpret_cast<uint8_t*>(i.GetRow(1));
+      p[0] = 0;
+      p[1] = 0;
+      p[2] = 255;
+
+      p[3] = 255;
+      p[4] = 0;
+      p[5] = 0;
+
+      controller_->GetScene().SetLayer(TEXTURE_2x2_1_ZINDEX, new ColorTextureSceneLayer(i));
+
+      std::unique_ptr<ColorTextureSceneLayer> l(new ColorTextureSceneLayer(i));
+      l->SetOrigin(-3, 2);
+      l->SetPixelSpacing(1.5, 1);
+      l->SetAngle(20.0 / 180.0 * M_PI);
+      controller_->GetScene().SetLayer(TEXTURE_2x2_2_ZINDEX, l.release());
+    }
+
+    // Texture of 1x1 size
+    {
+      Orthanc::Image i(Orthanc::PixelFormat_RGB24, 1, 1, false);
+
+      uint8_t* p = reinterpret_cast<uint8_t*>(i.GetRow(0));
+      p[0] = 255;
+      p[1] = 0;
+      p[2] = 0;
+
+      std::unique_ptr<ColorTextureSceneLayer> l(new ColorTextureSceneLayer(i));
+      l->SetOrigin(-2, 1);
+      l->SetAngle(20.0 / 180.0 * M_PI);
+      controller_->GetScene().SetLayer(TEXTURE_1x1_ZINDEX, l.release());
+    }
+
+    // Some lines
+    {
+      std::unique_ptr<PolylineSceneLayer> layer(new PolylineSceneLayer);
+
+      layer->SetThickness(1);
+
+      PolylineSceneLayer::Chain chain;
+      chain.push_back(ScenePoint2D(0 - 0.5, 0 - 0.5));
+      chain.push_back(ScenePoint2D(0 - 0.5, 2 - 0.5));
+      chain.push_back(ScenePoint2D(2 - 0.5, 2 - 0.5));
+      chain.push_back(ScenePoint2D(2 - 0.5, 0 - 0.5));
+      layer->AddChain(chain, true, 255, 0, 0);
+
+      chain.clear();
+      chain.push_back(ScenePoint2D(-5, -5));
+      chain.push_back(ScenePoint2D(5, -5));
+      chain.push_back(ScenePoint2D(5, 5));
+      chain.push_back(ScenePoint2D(-5, 5));
+      layer->AddChain(chain, true, 0, 255, 0);
+
+      double dy = 1.01;
+      chain.clear();
+      chain.push_back(ScenePoint2D(-4, -4));
+      chain.push_back(ScenePoint2D(4, -4 + dy));
+      chain.push_back(ScenePoint2D(-4, -4 + 2.0 * dy));
+      chain.push_back(ScenePoint2D(4, 2));
+      layer->AddChain(chain, false, 0, 0, 255);
+
+      controller_->GetScene().SetLayer(LINESET_1_ZINDEX, layer.release());
+    }
+
+    // Some text
+    {
+      std::unique_ptr<TextSceneLayer> layer(new TextSceneLayer);
+      layer->SetText("Hello");
+      controller_->GetScene().SetLayer(LINESET_2_ZINDEX, layer.release());
+    }
+  }
+
+
+  void TrackerSampleApp::DisableTracker()
+  {
+    if (activeTracker_)
+    {
+      activeTracker_->Cancel();
+      activeTracker_.reset();
+    }
+  }
+
+  void TrackerSampleApp::TakeScreenshot(const std::string& target,
+    unsigned int canvasWidth,
+    unsigned int canvasHeight)
+  {
+    CairoCompositor compositor(controller_->GetScene(), canvasWidth, canvasHeight);
+    compositor.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, FONT_SIZE_0, Orthanc::Encoding_Latin1);
+    compositor.Refresh();
+
+    Orthanc::ImageAccessor canvas;
+    compositor.GetCanvas().GetReadOnlyAccessor(canvas);
+
+    Orthanc::Image png(Orthanc::PixelFormat_RGB24, canvas.GetWidth(), canvas.GetHeight(), false);
+    Orthanc::ImageProcessing::Convert(png, canvas);
+
+    Orthanc::PngWriter writer;
+    writer.WriteToFile(target, png);
+  }
+
+
+  boost::shared_ptr<IFlexiblePointerTracker> TrackerSampleApp::TrackerHitTest(const PointerEvent & e)
+  {
+    // std::vector<boost::shared_ptr<MeasureTool>> measureTools_;
+    ScenePoint2D scenePos = e.GetMainPosition().Apply(
+      controller_->GetScene().GetCanvasToSceneTransform());
+
+    std::vector<boost::shared_ptr<MeasureTool> > measureTools = controller_->HitTestMeasureTools(scenePos);
+
+    if (measureTools.size() > 0)
+    {
+      return measureTools[0]->CreateEditionTracker(e);
+    }
+    return boost::shared_ptr<IFlexiblePointerTracker>();
+  }
+
+  static void GLAPIENTRY
+    OpenGLMessageCallback(GLenum source,
+      GLenum type,
+      GLuint id,
+      GLenum severity,
+      GLsizei length,
+      const GLchar* message,
+      const void* userParam)
+  {
+    if (severity != GL_DEBUG_SEVERITY_NOTIFICATION)
+    {
+      fprintf(stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n",
+        (type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : ""),
+        type, severity, message);
+    }
+  }
+
+  static bool g_stopApplication = false;
+  
+  ICompositor& TrackerSampleApp::GetCompositor()
+  {
+    using namespace Orthanc;
+    try
+    {
+      SdlViewport& viewport = dynamic_cast<SdlViewport&>(viewport_);
+      return viewport.GetCompositor();
+    }
+    catch (std::bad_cast e)
+    {
+      throw OrthancException(ErrorCode_InternalError, "Wrong viewport type!");
+     }
+  }
+
+  const ICompositor& TrackerSampleApp::GetCompositor() const
+  {
+    using namespace Orthanc;
+    try
+    {
+      SdlViewport& viewport = const_cast<SdlViewport&>(dynamic_cast<const SdlViewport&>(viewport_));
+      return viewport.GetCompositor();
+    }
+    catch (std::bad_cast e)
+    {
+      throw OrthancException(ErrorCode_InternalError, "Wrong viewport type!");
+    }
+  }
+
+
+  void TrackerSampleApp::Run()
+  {
+    controller_->FitContent(viewport_.GetCanvasWidth(), viewport_.GetCanvasHeight());
+
+    glEnable(GL_DEBUG_OUTPUT);
+    glDebugMessageCallback(OpenGLMessageCallback, 0);
+
+    GetCompositor().SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT,
+      FONT_SIZE_0, Orthanc::Encoding_Latin1);
+    GetCompositor().SetFont(1, Orthanc::EmbeddedResources::UBUNTU_FONT,
+      FONT_SIZE_1, Orthanc::Encoding_Latin1);
+
+    while (!g_stopApplication)
+    {
+      GetCompositor().Refresh();
+
+      SDL_Event event;
+      while (!g_stopApplication && SDL_PollEvent(&event))
+      {
+        if (event.type == SDL_QUIT)
+        {
+          g_stopApplication = true;
+          break;
+        }
+        else if (event.type == SDL_WINDOWEVENT &&
+          event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
+        {
+          DisableTracker(); // was: tracker.reset(NULL);
+        }
+        else if (event.type == SDL_KEYDOWN &&
+          event.key.repeat == 0 /* Ignore key bounce */)
+        {
+          switch (event.key.keysym.sym)
+          {
+          case SDLK_f:
+            viewport_.GetWindow().ToggleMaximize();
+            break;
+
+          case SDLK_q:
+            g_stopApplication = true;
+            break;
+          default:
+            break;
+          }
+        }
+        HandleApplicationEvent(event);
+      }
+      SDL_Delay(1);
+    }
+  }
+
+  void TrackerSampleApp::SetInfoDisplayMessage(
+    std::string key, std::string value)
+  {
+    if (value == "")
+      infoTextMap_.erase(key);
+    else
+      infoTextMap_[key] = value;
+    DisplayInfoText();
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/Sdl/TrackerSampleApp.h	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,148 @@
+/**
+ * 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 "../../Framework/Messages/IObserver.h"
+#include "../../Framework/Scene2D/OpenGLCompositor.h"
+#include "../../Framework/Scene2DViewport/IFlexiblePointerTracker.h"
+#include "../../Framework/Scene2DViewport/MeasureTool.h"
+#include "../../Framework/Scene2DViewport/PredeclaredTypes.h"
+#include "../../Framework/Scene2DViewport/ViewportController.h"
+#include "../../Framework/Viewport/SdlViewport.h"
+
+#include <SDL.h>
+
+#include <boost/make_shared.hpp>
+#include <boost/shared_ptr.hpp>
+#include <boost/enable_shared_from_this.hpp>
+
+namespace OrthancStone
+{
+  enum GuiTool
+  {
+    GuiTool_Rotate = 0,
+    GuiTool_Pan,
+    GuiTool_Zoom,
+    GuiTool_LineMeasure,
+    GuiTool_CircleMeasure,
+    GuiTool_AngleMeasure,
+    GuiTool_EllipseMeasure,
+    GuiTool_LAST
+  };
+
+  const char* MeasureToolToString(size_t i);
+
+  static const unsigned int FONT_SIZE_0 = 32;
+  static const unsigned int FONT_SIZE_1 = 24;
+
+  class Scene2D;
+  class UndoStack;
+
+  class TrackerSampleApp : public IObserver
+    , public boost::enable_shared_from_this<TrackerSampleApp>
+  {
+  public:
+    // 12 because.
+    TrackerSampleApp(MessageBroker& broker);
+    void PrepareScene();
+    void Run();
+    void SetInfoDisplayMessage(std::string key, std::string value);
+    void DisableTracker();
+
+    void HandleApplicationEvent(const SDL_Event& event);
+
+    /**
+    This method is called when the scene transform changes. It allows to
+    recompute the visual elements whose content depend upon the scene transform
+    */
+    void OnSceneTransformChanged(
+      const ViewportController::SceneTransformChanged& message);
+
+  private:
+    void SelectNextTool();
+    void CreateRandomMeasureTool();
+
+
+    /**
+    In the case of this app, the viewport is an SDL viewport and it has 
+    a OpenGLCompositor& GetCompositor() method
+    */
+    ICompositor& GetCompositor();
+
+    /**
+    See the other overload
+    */
+    const ICompositor& GetCompositor() const;
+
+    /**
+    This returns a random point in the canvas part of the scene, but in
+    scene coordinates
+    */
+    ScenePoint2D GetRandomPointInScene() const;
+
+    boost::shared_ptr<IFlexiblePointerTracker> TrackerHitTest(const PointerEvent& e);
+
+    boost::shared_ptr<IFlexiblePointerTracker> CreateSuitableTracker(
+      const SDL_Event& event,
+      const PointerEvent& e);
+
+    void TakeScreenshot(
+      const std::string& target,
+      unsigned int canvasWidth,
+      unsigned int canvasHeight);
+
+    /**
+      This adds the command at the top of the undo stack
+    */
+    void Commit(boost::shared_ptr<TrackerCommand> cmd);
+    void Undo();
+    void Redo();
+
+  private:
+    void DisplayFloatingCtrlInfoText(const PointerEvent& e);
+    void DisplayInfoText();
+    void HideInfoText();
+
+  private:
+    /**
+    WARNING: the measuring tools do store a reference to the scene, and it 
+    paramount that the scene gets destroyed AFTER the measurement tools.
+    */
+    boost::shared_ptr<ViewportController> controller_;
+
+    std::map<std::string, std::string> infoTextMap_;
+    boost::shared_ptr<IFlexiblePointerTracker> activeTracker_;
+
+    //static const int LAYER_POSITION = 150;
+
+    int TEXTURE_2x2_1_ZINDEX;
+    int TEXTURE_1x1_ZINDEX;
+    int TEXTURE_2x2_2_ZINDEX;
+    int LINESET_1_ZINDEX;
+    int LINESET_2_ZINDEX;
+    int FLOATING_INFOTEXT_LAYER_ZINDEX;
+    int FIXED_INFOTEXT_LAYER_ZINDEX;
+
+    GuiTool currentTool_;
+    boost::shared_ptr<UndoStack> undoStack_;
+    SdlOpenGLViewport viewport_;
+  };
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/Sdl/cpp.hint	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,2 @@
+#define ORTHANC_OVERRIDE
+#define ORTHANC_STONE_DEFINE_EMPTY_MESSAGE(FILE, LINE, NAME) class NAME : public ::OrthancStone::IMessage {};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/WebAssembly/BasicMPR.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,427 @@
+/**
+ * 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 "dev.h"
+
+#include <emscripten.h>
+
+#include "../../Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h"
+#include "../../Framework/Oracle/SleepOracleCommand.h"
+#include "../../Framework/Oracle/WebAssemblyOracle.h"
+#include "../../Framework/Scene2D/GrayscaleStyleConfigurator.h"
+#include "../../Framework/StoneInitialization.h"
+#include "../../Framework/Volumes/VolumeSceneLayerSource.h"
+
+
+namespace OrthancStone
+{
+  class VolumeSlicerWidget : public IObserver
+  {
+  private:
+    OrthancStone::WebAssemblyViewport      viewport_;
+    std::unique_ptr<VolumeSceneLayerSource>  source_;
+    VolumeProjection                       projection_;
+    std::vector<CoordinateSystem3D>        planes_;
+    size_t                                 currentPlane_;
+
+    void Handle(const DicomVolumeImage::GeometryReadyMessage& message)
+    {
+      LOG(INFO) << "Geometry is available";
+
+      const VolumeImageGeometry& geometry = message.GetOrigin().GetGeometry();
+
+      const unsigned int depth = geometry.GetProjectionDepth(projection_);
+      currentPlane_ = depth / 2;
+      
+      planes_.resize(depth);
+
+      for (unsigned int z = 0; z < depth; z++)
+      {
+        planes_[z] = geometry.GetProjectionSlice(projection_, z);
+      }
+
+      Refresh();
+
+      viewport_.FitContent();
+    }
+    
+  public:
+    VolumeSlicerWidget(MessageBroker& broker,
+                         const std::string& canvas,
+                         VolumeProjection projection) :
+      IObserver(broker),
+      viewport_(broker, canvas),
+      projection_(projection),
+      currentPlane_(0)
+    {
+    }
+
+    void UpdateSize()
+    {
+      viewport_.UpdateSize();
+    }
+
+    void SetSlicer(int layerDepth,
+                   const boost::shared_ptr<IVolumeSlicer>& slicer,
+                   IObservable& loader,
+                   ILayerStyleConfigurator* configurator)
+    {
+      if (source_.get() != NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls,
+                                        "Only one slicer can be registered");
+      }
+      
+      loader.RegisterObserverCallback(
+        new Callable<VolumeSlicerWidget, DicomVolumeImage::GeometryReadyMessage>
+        (*this, &VolumeSlicerWidget::Handle));
+
+      source_.reset(new VolumeSceneLayerSource(viewport_.GetScene(), layerDepth, slicer));
+
+      if (configurator != NULL)
+      {
+        source_->SetConfigurator(configurator);
+      }
+    }    
+
+    void Refresh()
+    {
+      if (source_.get() != NULL &&
+          currentPlane_ < planes_.size())
+      {
+        source_->Update(planes_[currentPlane_]);
+        viewport_.Refresh();
+      }
+    }
+
+    size_t GetSlicesCount() const
+    {
+      return planes_.size();
+    }
+
+    void Scroll(int delta)
+    {
+      if (!planes_.empty())
+      {
+        int tmp = static_cast<int>(currentPlane_) + delta;
+        unsigned int next;
+
+        if (tmp < 0)
+        {
+          next = 0;
+        }
+        else if (tmp >= static_cast<int>(planes_.size()))
+        {
+          next = planes_.size() - 1;
+        }
+        else
+        {
+          next = static_cast<size_t>(tmp);
+        }
+
+        if (next != currentPlane_)
+        {
+          currentPlane_ = next;
+          Refresh();
+        }
+      }
+    }
+  };
+}
+
+
+
+
+boost::shared_ptr<OrthancStone::DicomVolumeImage>  ct_(new OrthancStone::DicomVolumeImage);
+
+boost::shared_ptr<OrthancStone::OrthancSeriesVolumeProgressiveLoader>  loader_;
+
+std::unique_ptr<OrthancStone::VolumeSlicerWidget>  widget1_;
+std::unique_ptr<OrthancStone::VolumeSlicerWidget>  widget2_;
+std::unique_ptr<OrthancStone::VolumeSlicerWidget>  widget3_;
+
+OrthancStone::MessageBroker  broker_;
+OrthancStone::WebAssemblyOracle  oracle_(broker_);
+
+
+EM_BOOL OnWindowResize(int eventType, const EmscriptenUiEvent *uiEvent, void *userData)
+{
+  try
+  {
+    if (widget1_.get() != NULL)
+    {
+      widget1_->UpdateSize();
+    }
+  
+    if (widget2_.get() != NULL)
+    {
+      widget2_->UpdateSize();
+    }
+  
+    if (widget3_.get() != NULL)
+    {
+      widget3_->UpdateSize();
+    }
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    LOG(ERROR) << "Exception while updating canvas size: " << e.What();
+  }
+  
+  return true;
+}
+
+
+
+
+EM_BOOL OnAnimationFrame(double time, void *userData)
+{
+  try
+  {
+    if (widget1_.get() != NULL)
+    {
+      widget1_->Refresh();
+    }
+  
+    if (widget2_.get() != NULL)
+    {
+      widget2_->Refresh();
+    }
+  
+    if (widget3_.get() != NULL)
+    {
+      widget3_->Refresh();
+    }
+
+    return true;
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    LOG(ERROR) << "Exception in the animation loop, stopping now: " << e.What();
+    return false;
+  }  
+}
+
+
+static bool ctrlDown_ = false;
+
+
+EM_BOOL OnMouseWheel(int eventType,
+                     const EmscriptenWheelEvent *wheelEvent,
+                     void *userData)
+{
+  try
+  {
+    if (userData != NULL)
+    {
+      int delta = 0;
+
+      if (wheelEvent->deltaY < 0)
+      {
+        delta = -1;
+      }
+           
+      if (wheelEvent->deltaY > 0)
+      {
+        delta = 1;
+      }
+
+      OrthancStone::VolumeSlicerWidget& widget =
+        *reinterpret_cast<OrthancStone::VolumeSlicerWidget*>(userData);
+      
+      if (ctrlDown_)
+      {
+        delta *= static_cast<int>(widget.GetSlicesCount() / 10);
+      }
+
+      widget.Scroll(delta);
+    }
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    LOG(ERROR) << "Exception in the wheel event: " << e.What();
+  }
+  
+  return true;
+}
+
+
+EM_BOOL OnKeyDown(int eventType,
+                  const EmscriptenKeyboardEvent *keyEvent,
+                  void *userData)
+{
+  ctrlDown_ = keyEvent->ctrlKey;
+  return false;
+}
+
+
+EM_BOOL OnKeyUp(int eventType,
+                const EmscriptenKeyboardEvent *keyEvent,
+                void *userData)
+{
+  ctrlDown_ = false;
+  return false;
+}
+
+
+
+
+namespace OrthancStone
+{
+  class TestSleep : public IObserver
+  {
+  private:
+    WebAssemblyOracle&  oracle_;
+
+    void Schedule()
+    {
+      oracle_.Schedule(*this, new OrthancStone::SleepOracleCommand(2000));
+    }
+    
+    void Handle(const SleepOracleCommand::TimeoutMessage& message)
+    {
+      LOG(INFO) << "TIMEOUT";
+      Schedule();
+    }
+    
+  public:
+    TestSleep(MessageBroker& broker,
+              WebAssemblyOracle& oracle) :
+      IObserver(broker),
+      oracle_(oracle)
+    {
+      oracle.RegisterObserverCallback(
+        new Callable<TestSleep, SleepOracleCommand::TimeoutMessage>
+        (*this, &TestSleep::Handle));
+
+      LOG(INFO) << "STARTING";
+      Schedule();
+    }
+  };
+
+  //static TestSleep testSleep(broker_, oracle_);
+}
+
+
+
+static std::map<std::string, std::string> arguments_;
+
+static bool GetArgument(std::string& value,
+                        const std::string& key)
+{
+  std::map<std::string, std::string>::const_iterator found = arguments_.find(key);
+
+  if (found == arguments_.end())
+  {
+    return false;
+  }
+  else
+  {
+    value = found->second;
+    return true;
+  }
+}
+
+
+extern "C"
+{
+  int main(int argc, char const *argv[]) 
+  {
+    OrthancStone::StoneInitialize();
+    Orthanc::Logging::EnableInfoLevel(true);
+    // Orthanc::Logging::EnableTraceLevel(true);
+    EM_ASM(window.dispatchEvent(new CustomEvent("WebAssemblyLoaded")););
+  }
+
+  EMSCRIPTEN_KEEPALIVE
+  void SetArgument(const char* key, const char* value)
+  {
+    // This is called for each GET argument (cf. "app.js")
+    LOG(INFO) << "Received GET argument: [" << key << "] = [" << value << "]";
+    arguments_[key] = value;
+  }
+
+  EMSCRIPTEN_KEEPALIVE
+  void Initialize()
+  {
+    try
+    {
+      oracle_.SetOrthancRoot("..");
+      
+      loader_.reset(new OrthancStone::OrthancSeriesVolumeProgressiveLoader(ct_, oracle_, oracle_));
+    
+      widget1_.reset(new OrthancStone::VolumeSlicerWidget(broker_, "mycanvas1", OrthancStone::VolumeProjection_Axial));
+      {
+        std::unique_ptr<OrthancStone::GrayscaleStyleConfigurator> style(new OrthancStone::GrayscaleStyleConfigurator);
+        style->SetLinearInterpolation(true);
+        style->SetWindowing(OrthancStone::ImageWindowing_Bone);
+        widget1_->SetSlicer(0, loader_, *loader_, style.release());
+      }
+      widget1_->UpdateSize();
+
+      widget2_.reset(new OrthancStone::VolumeSlicerWidget(broker_, "mycanvas2", OrthancStone::VolumeProjection_Coronal));
+      {
+        std::unique_ptr<OrthancStone::GrayscaleStyleConfigurator> style(new OrthancStone::GrayscaleStyleConfigurator);
+        style->SetLinearInterpolation(true);
+        style->SetWindowing(OrthancStone::ImageWindowing_Bone);
+        widget2_->SetSlicer(0, loader_, *loader_, style.release());
+      }
+      widget2_->UpdateSize();
+
+      widget3_.reset(new OrthancStone::VolumeSlicerWidget(broker_, "mycanvas3", OrthancStone::VolumeProjection_Sagittal));
+      {
+        std::unique_ptr<OrthancStone::GrayscaleStyleConfigurator> style(new OrthancStone::GrayscaleStyleConfigurator);
+        style->SetLinearInterpolation(true);
+        style->SetWindowing(OrthancStone::ImageWindowing_Bone);
+        widget3_->SetSlicer(0, loader_, *loader_, style.release());
+      }
+      widget3_->UpdateSize();
+
+      emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, OnWindowResize); // DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1 !!
+
+      emscripten_set_wheel_callback("#mycanvas1", widget1_.get(), false, OnMouseWheel);
+      emscripten_set_wheel_callback("#mycanvas2", widget2_.get(), false, OnMouseWheel);
+      emscripten_set_wheel_callback("#mycanvas3", widget3_.get(), false, OnMouseWheel);
+
+      emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, OnKeyDown);
+      emscripten_set_keyup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, OnKeyUp);
+    
+      emscripten_request_animation_frame_loop(OnAnimationFrame, NULL);
+
+
+      std::string ct;
+      if (GetArgument(ct, "ct"))
+      {
+        //loader_->LoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");
+        loader_->LoadSeries(ct);
+      }
+      else
+      {
+        LOG(ERROR) << "No Orthanc identifier for the CT series was provided";
+      }
+    }
+    catch (Orthanc::OrthancException& e)
+    {
+      LOG(ERROR) << "Exception during Initialize(): " << e.What();
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/WebAssembly/BasicMPR.html	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,60 @@
+<!doctype html>
+<html lang="en-us">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+    <!-- Disable pinch zoom on mobile devices -->
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+    <meta name="HandheldFriendly" content="true" />
+    
+    
+    <title>Stone of Orthanc</title>
+
+    <style>
+      html, body {
+      width: 100%;
+      height: 100%;
+      margin: 0px;
+      border: 0;
+      overflow: hidden; /*  Disable scrollbars */
+      display: block;  /* No floating content on sides */
+      }
+
+      #mycanvas1 {
+      position:absolute;
+      left:0%;
+      top:0%;
+      background-color: red;
+      width: 50%;
+      height: 100%;
+      }
+
+      #mycanvas2 {
+      position:absolute;
+      left:50%;
+      top:0%;
+      background-color: green;
+      width: 50%;
+      height: 50%;
+      }
+
+      #mycanvas3 {
+      position:absolute;
+      left:50%;
+      top:50%;
+      background-color: blue;
+      width: 50%;
+      height: 50%;
+      }
+    </style>
+  </head>
+  <body>
+    <canvas id="mycanvas1" oncontextmenu="return false;"></canvas>
+    <canvas id="mycanvas2" oncontextmenu="return false;"></canvas>
+    <canvas id="mycanvas3" oncontextmenu="return false;"></canvas>
+
+    <script type="text/javascript" src="app.js"></script>
+    <script type="text/javascript" async src="BasicMPR.js"></script>
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/WebAssembly/BasicScene.cpp	Wed Apr 22 14:05:47 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/>.
+ **/
+
+
+#include "dev.h"
+
+#include <emscripten.h>
+#include <emscripten/html5.h>
+
+// From Stone
+#include "../../Framework/Scene2D/ColorTextureSceneLayer.h"
+#include "../../Framework/StoneInitialization.h"
+
+// From Orthanc framework
+#include <Core/Images/Image.h>
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+
+void PrepareScene(OrthancStone::Scene2D& scene)
+{
+  using namespace OrthancStone;
+
+  // Texture of 2x2 size
+  if (1)
+  {
+    Orthanc::Image i(Orthanc::PixelFormat_RGB24, 2, 2, false);
+    
+    uint8_t *p = reinterpret_cast<uint8_t*>(i.GetRow(0));
+    p[0] = 255;
+    p[1] = 0;
+    p[2] = 0;
+
+    p[3] = 0;
+    p[4] = 255;
+    p[5] = 0;
+
+    p = reinterpret_cast<uint8_t*>(i.GetRow(1));
+    p[0] = 0;
+    p[1] = 0;
+    p[2] = 255;
+
+    p[3] = 255;
+    p[4] = 0;
+    p[5] = 0;
+
+    scene.SetLayer(12, new ColorTextureSceneLayer(i));
+
+    std::unique_ptr<ColorTextureSceneLayer> l(new ColorTextureSceneLayer(i));
+    l->SetOrigin(-3, 2);
+    l->SetPixelSpacing(1.5, 1);
+    l->SetAngle(20.0 / 180.0 * M_PI);
+    scene.SetLayer(14, l.release());
+  }
+
+  // Texture of 1x1 size
+  if (1)
+  {
+    Orthanc::Image i(Orthanc::PixelFormat_RGB24, 1, 1, false);
+    
+    uint8_t *p = reinterpret_cast<uint8_t*>(i.GetRow(0));
+    p[0] = 255;
+    p[1] = 0;
+    p[2] = 0;
+
+    std::unique_ptr<ColorTextureSceneLayer> l(new ColorTextureSceneLayer(i));
+    l->SetOrigin(-2, 1);
+    l->SetAngle(20.0 / 180.0 * M_PI);
+    scene.SetLayer(13, l.release());
+  }
+
+  // Some lines
+  if (1)
+  {
+    std::unique_ptr<PolylineSceneLayer> layer(new PolylineSceneLayer);
+
+    layer->SetThickness(1);
+
+    PolylineSceneLayer::Chain chain;
+    chain.push_back(ScenePoint2D(0 - 0.5, 0 - 0.5));
+    chain.push_back(ScenePoint2D(0 - 0.5, 2 - 0.5));
+    chain.push_back(ScenePoint2D(2 - 0.5, 2 - 0.5));
+    chain.push_back(ScenePoint2D(2 - 0.5, 0 - 0.5));
+    layer->AddChain(chain, true, 255, 0, 0);
+
+    chain.clear();
+    chain.push_back(ScenePoint2D(-5, -5));
+    chain.push_back(ScenePoint2D(5, -5));
+    chain.push_back(ScenePoint2D(5, 5));
+    chain.push_back(ScenePoint2D(-5, 5));
+    layer->AddChain(chain, true, 0, 255, 0);
+
+    double dy = 1.01;
+    chain.clear();
+    chain.push_back(ScenePoint2D(-4, -4));
+    chain.push_back(ScenePoint2D(4, -4 + dy));
+    chain.push_back(ScenePoint2D(-4, -4 + 2.0 * dy));
+    chain.push_back(ScenePoint2D(4, 2));
+    layer->AddChain(chain, false, 0, 0, 255);
+
+    scene.SetLayer(50, layer.release());
+  }
+
+  // Some text
+  if (1)
+  {
+    std::unique_ptr<TextSceneLayer> layer(new TextSceneLayer);
+    layer->SetText("Hello");
+    scene.SetLayer(100, layer.release());
+  }
+}
+
+
+std::unique_ptr<OrthancStone::WebAssemblyViewport>  viewport1_;
+std::unique_ptr<OrthancStone::WebAssemblyViewport>  viewport2_;
+std::unique_ptr<OrthancStone::WebAssemblyViewport>  viewport3_;
+boost::shared_ptr<OrthancStone::ViewportController>   controller1_;
+boost::shared_ptr<OrthancStone::ViewportController>   controller2_;
+boost::shared_ptr<OrthancStone::ViewportController>   controller3_;
+OrthancStone::MessageBroker broker_;
+
+
+EM_BOOL OnWindowResize(
+  int eventType, const EmscriptenUiEvent *uiEvent, void *userData)
+{
+  if (viewport1_.get() != NULL)
+  {
+    viewport1_->UpdateSize();
+  }
+  
+  if (viewport2_.get() != NULL)
+  {
+    viewport2_->UpdateSize();
+  }
+  
+  if (viewport3_.get() != NULL)
+  {
+    viewport3_->UpdateSize();
+  }
+  
+  return true;
+}
+
+extern "C"
+{
+  int main(int argc, char const *argv[]) 
+  {
+    OrthancStone::StoneInitialize();
+    // Orthanc::Logging::EnableInfoLevel(true);
+    // Orthanc::Logging::EnableTraceLevel(true);
+    EM_ASM(window.dispatchEvent(new CustomEvent("WebAssemblyLoaded")););
+  }
+
+  EMSCRIPTEN_KEEPALIVE
+  void Initialize()
+  {
+    viewport1_.reset(new OrthancStone::WebAssemblyViewport("mycanvas1"));
+    PrepareScene(viewport1_->GetScene());
+    viewport1_->UpdateSize();
+
+    viewport2_.reset(new OrthancStone::WebAssemblyViewport("mycanvas2"));
+    PrepareScene(viewport2_->GetScene());
+    viewport2_->UpdateSize();
+
+    viewport3_.reset(new OrthancStone::WebAssemblyViewport("mycanvas3"));
+    PrepareScene(viewport3_->GetScene());
+    viewport3_->UpdateSize();
+
+    viewport1_->GetCompositor().SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, 
+                                        FONT_SIZE, Orthanc::Encoding_Latin1);
+    viewport2_->GetCompositor().SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, 
+                                        FONT_SIZE, Orthanc::Encoding_Latin1);
+    viewport3_->GetCompositor().SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, 
+                                        FONT_SIZE, Orthanc::Encoding_Latin1);
+
+    controller1_.reset(new OrthancStone::ViewportController(boost::make_shared<OrthancStone::UndoStack>(), broker_, *viewport1_));
+    controller2_.reset(new OrthancStone::ViewportController(boost::make_shared<OrthancStone::UndoStack>(), broker_, *viewport2_));
+    controller3_.reset(new OrthancStone::ViewportController(boost::make_shared<OrthancStone::UndoStack>(), broker_, *viewport3_));
+
+    controller1_->FitContent(viewport1_->GetCanvasWidth(), viewport1_->GetCanvasHeight());
+    controller2_->FitContent(viewport2_->GetCanvasWidth(), viewport2_->GetCanvasHeight());
+    controller3_->FitContent(viewport3_->GetCanvasWidth(), viewport3_->GetCanvasHeight());
+
+    viewport1_->Refresh();
+    viewport2_->Refresh();
+    viewport3_->Refresh();
+
+    SetupEvents("mycanvas1", controller1_);
+    SetupEvents("mycanvas2", controller2_);
+    SetupEvents("mycanvas3", controller3_);
+
+    emscripten_set_resize_callback("#window", NULL, false, OnWindowResize);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/WebAssembly/BasicScene.html	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,69 @@
+<!doctype html>
+<html lang="en-us">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+    <!-- Disable pinch zoom on mobile devices -->
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+    <meta name="HandheldFriendly" content="true" />
+    
+    
+    <title>Stone of Orthanc</title>
+
+    <style>
+      html, body {
+      width: 100%;
+      height: 100%;
+      margin: 0px;
+      border: 0;
+      overflow: hidden; /*  Disable scrollbars */
+      display: block;  /* No floating content on sides */
+      }
+
+      #mycanvas1 {
+      position:absolute;
+      left:0%;
+      top:0%;
+      background-color: red;
+      width: 50%;
+      height: 100%;
+      }
+
+      #mycanvas2 {
+      position:absolute;
+      left:50%;
+      top:0%;
+      background-color: green;
+      width: 50%;
+      height: 50%;
+      }
+
+      #mycanvas3 {
+      position:absolute;
+      left:50%;
+      top:50%;
+      background-color: blue;
+      width: 50%;
+      height: 50%;
+      }
+    </style>
+  </head>
+  <body>
+    <canvas id="mycanvas1" oncontextmenu="return false;"></canvas>
+    <canvas id="mycanvas2" oncontextmenu="return false;"></canvas>
+    <canvas id="mycanvas3" oncontextmenu="return false;"></canvas>
+
+    <script type="text/javascript">
+      if (!('WebAssembly' in window)) {
+      alert('Sorry, your browser does not support WebAssembly :(');
+      } else {
+      window.addEventListener('WebAssemblyLoaded', function() {
+      Module.ccall('Initialize', null, null, null);
+      });
+      }
+    </script>
+
+    <script type="text/javascript" async src="BasicScene.js"></script>
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/WebAssembly/CMakeLists.txt	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,119 @@
+cmake_minimum_required(VERSION 2.8.3)
+
+
+#####################################################################
+## Configuration of the Emscripten compiler for WebAssembly target
+#####################################################################
+
+set(WASM_FLAGS "-s WASM=1 -s FETCH=1")
+
+set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WASM_FLAGS}")
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WASM_FLAGS}")
+set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\"]'")
+set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ERROR_ON_UNDEFINED_SYMBOLS=1")
+set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1")
+set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ASSERTIONS=1 -s DISABLE_EXCEPTION_CATCHING=0")
+#set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s EXIT_RUNTIME=1")
+
+#set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1")
+
+
+#####################################################################
+## Configuration of the Orthanc framework
+#####################################################################
+
+# This CMake file defines the "ORTHANC_STONE_VERSION" macro, so it
+# must be the first inclusion
+include(${CMAKE_SOURCE_DIR}/../../Resources/CMake/Version.cmake)
+
+if (ORTHANC_STONE_VERSION STREQUAL "mainline")
+  set(ORTHANC_FRAMEWORK_VERSION "mainline")
+  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
+else()
+  set(ORTHANC_FRAMEWORK_VERSION "1.5.7")
+  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
+endif()
+
+set(ORTHANC_FRAMEWORK_SOURCE "${ORTHANC_FRAMEWORK_DEFAULT_SOURCE}" CACHE STRING "Source of the Orthanc source code (can be \"hg\", \"archive\", \"web\" or \"path\")")
+set(ORTHANC_FRAMEWORK_ARCHIVE "" CACHE STRING "Path to the Orthanc archive, if ORTHANC_FRAMEWORK_SOURCE is \"archive\"")
+set(ORTHANC_FRAMEWORK_ROOT "" CACHE STRING "Path to the Orthanc source directory, if ORTHANC_FRAMEWORK_SOURCE is \"path\"")
+
+
+#####################################################################
+## Configuration of the Stone framework
+#####################################################################
+
+include(${CMAKE_SOURCE_DIR}/../../Resources/CMake/OrthancStoneParameters.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/DownloadPackage.cmake)
+
+DownloadPackage(
+  "a24b8136b8f3bb93f166baf97d9328de"
+  "http://orthanc.osimis.io/ThirdPartyDownloads/ubuntu-font-family-0.83.zip"
+  "${CMAKE_BINARY_DIR}/ubuntu-font-family-0.83")
+
+set(ORTHANC_STONE_APPLICATION_RESOURCES
+  UBUNTU_FONT  ${CMAKE_BINARY_DIR}/ubuntu-font-family-0.83/Ubuntu-R.ttf
+  )
+
+SET(ENABLE_GOOGLE_TEST OFF)
+SET(ENABLE_LOCALE ON)
+SET(ORTHANC_SANDBOXED ON)
+SET(ENABLE_WASM ON)
+
+include(${CMAKE_SOURCE_DIR}/../../Resources/CMake/OrthancStoneConfiguration.cmake)
+
+add_definitions(
+  -DORTHANC_ENABLE_LOGGING_PLUGIN=0
+  )
+
+
+#####################################################################
+## Build the samples
+#####################################################################
+
+add_library(OrthancStone STATIC
+  ${ORTHANC_STONE_SOURCES}
+  )
+
+
+if (ON)
+  add_executable(BasicScene
+    BasicScene.cpp
+    #${CMAKE_CURRENT_LIST_DIR}/../Shared/SharedBasicScene.h
+    ${CMAKE_CURRENT_LIST_DIR}/../Shared/SharedBasicScene.cpp
+    )
+
+  target_link_libraries(BasicScene OrthancStone)
+
+  install(
+    TARGETS BasicScene
+    RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}
+    )
+endif()
+
+
+if (ON)
+  add_executable(BasicMPR
+    BasicMPR.cpp
+    )
+
+  target_link_libraries(BasicMPR OrthancStone)
+
+  install(
+    TARGETS BasicMPR
+    RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}
+    )
+endif()
+  
+
+install(
+  FILES
+  ${CMAKE_CURRENT_BINARY_DIR}/BasicMPR.wasm
+  ${CMAKE_CURRENT_BINARY_DIR}/BasicScene.wasm
+  ${CMAKE_SOURCE_DIR}/BasicMPR.html
+  ${CMAKE_SOURCE_DIR}/BasicScene.html
+  ${CMAKE_SOURCE_DIR}/Configuration.json
+  ${CMAKE_SOURCE_DIR}/app.js
+  ${CMAKE_SOURCE_DIR}/index.html
+  DESTINATION ${CMAKE_INSTALL_PREFIX}
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/WebAssembly/Configuration.json	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,17 @@
+{
+  "Plugins": [
+    "/usr/local/share/orthanc/plugins/libOrthancWebViewer.so",
+    "/usr/local/share/orthanc/plugins/libServeFolders.so"
+  ],
+  "StorageDirectory" : "/var/lib/orthanc/db",
+  "IndexDirectory" : "/var/lib/orthanc/db",
+  "RemoteAccessAllowed" : true,
+  "AuthenticationEnabled" : false,
+  "ServeFolders" : {
+    "AllowCache" : false,
+    "GenerateETag" : true,
+    "Folders" : {
+      "/stone" : "/root/stone"
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/WebAssembly/ConfigurationLocalSJO.json	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,20 @@
+{
+  "Plugins": [
+    "/home/jodogne/Subversion/orthanc-webviewer/r/libOrthancWebViewer.so",
+    "/home/jodogne/Subversion/orthanc/r/libServeFolders.so"
+  ],
+  "StorageDirectory" : "/tmp/orthanc-db",
+  "IndexDirectory" : "/tmp/orthanc-db",
+  "RemoteAccessAllowed" : true,
+  "AuthenticationEnabled" : false,
+  "ServeFolders" : {
+    "AllowCache" : false,
+    "GenerateETag" : true,
+    "Folders" : {
+      "/stone" : "/tmp/stone"
+    }
+  },
+  "WebViewer" : {
+    "CachePath" : "/tmp/orthanc-db/WebViewerCache"
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/WebAssembly/NOTES.txt	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,76 @@
+Docker SJO
+==========
+
+$ source ~/Downloads/emsdk/emsdk_env.sh
+$ cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_BUILD_TYPE=Release -DALLOW_DOWNLOADS=ON .. -DCMAKE_INSTALL_PREFIX=/tmp/stone
+$ ninja install
+$ docker run -p 4242:4242 -p 8042:8042 --rm -v /tmp/stone:/root/stone:ro -v /tmp/stone-db/:/var/lib/orthanc/db/ jodogne/orthanc-plugins:latest /root/stone/Configuration.json --verbose
+
+WARNING: This won't work using "orthanc-plugins:1.5.6", as support for
+PAM is mandatatory in "/instances/.../image-uint16".
+
+
+Docker BGO
+==========
+
+On Ubuntu WSL
+-------------
+. ~/apps/emsdk/emsdk_env.sh
+cd /mnt/c/osi/dev/
+mkdir -p build_stone_newsamples_wasm_wsl
+mkdir -p build_install_stone_newsamples_wasm_wsl
+cd build_stone_newsamples_wasm_wsl
+cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_BUILD_TYPE=Release -DALLOW_DOWNLOADS=ON /mnt/c/osi/dev/orthanc-stone/Samples/WebAssembly -DCMAKE_INSTALL_PREFIX=/mnt/c/osi/dev/build_install_stone_newsamples_wasm_wsl
+ninja install
+
+Then, on Windows
+-----------------
+docker run -p 4242:4242 -p 8042:8042 --rm -v "C:/osi/dev/build_install_stone_newsamples_wasm_wsl:/root/stone:ro" jodogne/orthanc-plugins:1.5.6 /root/stone/Configuration.json --verbose
+
+# WAIT A COUPLE OF SECS
+# if the archive has NOT already been unzipped, unzip it
+# upload dicom files to running orthanc
+
+cd C:\osi\dev\twiga-orthanc-viewer\demo\dicomfiles
+if (-not (test-path RTVIEWER-c8febcc6-eb9e22a4-130f208c-e0a6a4cd-4d432c57)) { unzip RTVIEWER-c8febcc6-eb9e22a4-130f208c-e0a6a4cd-4d432c57.zip}
+ImportDicomFiles.ps1 127.0.0.1 8042 .\RTVIEWER-c8febcc6-eb9e22a4-130f208c-e0a6a4cd-4d432c57\
+
+--> localhost:8042 --> Plugins --> serve-folders --> stone --> ...
+
+Local BGO
+==========
+
+. ~/apps/emsdk/emsdk_env.sh
+cd /mnt/c/osi/dev/
+mkdir -p build_stone_newsamples_wasm_wsl
+mkdir -p build_install_stone_newsamples_wasm_wsl
+cd build_stone_newsamples_wasm_wsl
+cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_BUILD_TYPE=Release -DALLOW_DOWNLOADS=ON /mnt/c/osi/dev/orthanc-stone/Samples/WebAssembly -DCMAKE_INSTALL_PREFIX=/mnt/c/osi/dev/build_install_stone_newsamples_wasm_wsl
+
+
+
+TODO: Orthanc.exe 
+
+
+Local SJO
+==========
+
+$ source ~/Downloads/emsdk/emsdk_env.sh
+$ cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_BUILD_TYPE=Release -DALLOW_DOWNLOADS=ON .. -DCMAKE_INSTALL_PREFIX=/tmp/stone
+$ ninja install
+
+$ make -C ~/Subversion/orthanc/r -j4
+$ make -C ~/Subversion/orthanc-webviewer/r -j4
+$ ~/Subversion/orthanc/r/Orthanc ../ConfigurationLocalSJO.json
+
+
+Local AM
+========
+
+. ~/apps/emsdk/emsdk_env.sh
+cd /mnt/c/o/
+mkdir -p build_stone_newsamples_wasm_wsl
+mkdir -p build_install_stone_newsamples_wasm_wsl
+cd build_stone_newsamples_wasm_wsl
+cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=${EMSDK}/fastcomp/emscripten/cmake/Modules/Platform/Emscripten.cmake -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT=/mnt/c/o/orthanc/ -DCMAKE_BUILD_TYPE=Release -DALLOW_DOWNLOADS=ON /mnt/c/o/orthanc-stone/Samples/WebAssembly -DCMAKE_INSTALL_PREFIX=/mnt/c/o/build_install_stone_newsamples_wasm_wsl
+ninja
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/WebAssembly/app.js	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,33 @@
+/**
+ * This is a generic bootstrap code that is shared by all the Stone
+ * sample applications.
+ **/
+
+// Check support for WebAssembly
+if (!('WebAssembly' in window)) {
+  alert('Sorry, your browser does not support WebAssembly :(');
+} else {
+
+  // Wait for the module to be loaded (the event "WebAssemblyLoaded"
+  // must be emitted by the "main" function)
+  window.addEventListener('WebAssemblyLoaded', function() {
+
+    // Loop over the GET arguments
+    var parameters = window.location.search.substr(1);
+    if (parameters != null && parameters != '') {
+      var tokens = parameters.split('&');
+      for (var i = 0; i < tokens.length; i++) {
+        var arg = tokens[i].split('=');
+        if (arg.length == 2) {
+
+          // Send each GET argument to WebAssembly
+          Module.ccall('SetArgument', null, [ 'string', 'string' ],
+                       [ arg[0], decodeURIComponent(arg[1]) ]);
+        }
+      }
+    }
+
+    // Inform the WebAssembly module that it can start
+    Module.ccall('Initialize', null, null, null);
+  });
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/WebAssembly/dev.h	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,202 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-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 "../../Framework/Viewport/WebAssemblyViewport.h"
+#include "../../Framework/Scene2D/OpenGLCompositor.h"
+#include "../../Framework/Scene2D/PanSceneTracker.h"
+#include "../../Framework/Scene2D/RotateSceneTracker.h"
+#include "../../Framework/Scene2D/ZoomSceneTracker.h"
+#include "../../Framework/Scene2DViewport/UndoStack.h"
+#include "../../Framework/Scene2DViewport/ViewportController.h"
+
+#include <Core/OrthancException.h>
+
+#include <emscripten/html5.h>
+#include <boost/make_shared.hpp>
+
+static const unsigned int FONT_SIZE = 32;
+
+namespace OrthancStone
+{
+  class ActiveTracker : public boost::noncopyable
+  {
+  private:
+    boost::shared_ptr<IFlexiblePointerTracker> tracker_;
+    std::string                             canvasIdentifier_;
+    bool                                    insideCanvas_;
+    
+  public:
+    ActiveTracker(const boost::shared_ptr<IFlexiblePointerTracker>& tracker,
+                  const std::string& canvasId) :
+      tracker_(tracker),
+      canvasIdentifier_(canvasId),
+      insideCanvas_(true)
+    {
+      if (tracker_.get() == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+    }
+
+    bool IsAlive() const
+    {
+      return tracker_->IsAlive();
+    }
+
+    void PointerMove(const PointerEvent& event)
+    {
+      tracker_->PointerMove(event);
+    }
+
+    void PointerUp(const PointerEvent& event)
+    {
+      tracker_->PointerUp(event);
+    }
+  };
+}
+
+static OrthancStone::PointerEvent* ConvertMouseEvent(
+  const EmscriptenMouseEvent&        source,
+  OrthancStone::IViewport& viewport)
+{
+  std::unique_ptr<OrthancStone::PointerEvent> target(
+    new OrthancStone::PointerEvent);
+
+  target->AddPosition(viewport.GetPixelCenterCoordinates(
+                        source.targetX, source.targetY));
+  target->SetAltModifier(source.altKey);
+  target->SetControlModifier(source.ctrlKey);
+  target->SetShiftModifier(source.shiftKey);
+
+  return target.release();
+}
+
+std::unique_ptr<OrthancStone::ActiveTracker> tracker_;
+
+EM_BOOL OnMouseEvent(int eventType, 
+                     const EmscriptenMouseEvent *mouseEvent, 
+                     void *userData)
+{
+  if (mouseEvent != NULL &&
+      userData != NULL)
+  {
+    boost::shared_ptr<OrthancStone::ViewportController>& controller = 
+      *reinterpret_cast<boost::shared_ptr<OrthancStone::ViewportController>*>(userData);
+
+    switch (eventType)
+    {
+      case EMSCRIPTEN_EVENT_CLICK:
+      {
+        static unsigned int count = 0;
+        char buf[64];
+        sprintf(buf, "click %d", count++);
+
+        std::unique_ptr<OrthancStone::TextSceneLayer> layer(new OrthancStone::TextSceneLayer);
+        layer->SetText(buf);
+        controller->GetViewport().GetScene().SetLayer(100, layer.release());
+        controller->GetViewport().Refresh();
+        break;
+      }
+
+      case EMSCRIPTEN_EVENT_MOUSEDOWN:
+      {
+        boost::shared_ptr<OrthancStone::IFlexiblePointerTracker> t;
+
+        {
+          std::unique_ptr<OrthancStone::PointerEvent> event(
+            ConvertMouseEvent(*mouseEvent, controller->GetViewport()));
+
+          switch (mouseEvent->button)
+          {
+            case 0:  // Left button
+              emscripten_console_log("Creating RotateSceneTracker");
+              t.reset(new OrthancStone::RotateSceneTracker(
+                        controller, *event));
+              break;
+
+            case 1:  // Middle button
+              emscripten_console_log("Creating PanSceneTracker");
+              LOG(INFO) << "Creating PanSceneTracker" ;
+              t.reset(new OrthancStone::PanSceneTracker(
+                        controller, *event));
+              break;
+
+            case 2:  // Right button
+              emscripten_console_log("Creating ZoomSceneTracker");
+              t.reset(new OrthancStone::ZoomSceneTracker(
+                        controller, *event, controller->GetViewport().GetCanvasWidth()));
+              break;
+
+            default:
+              break;
+          }
+        }
+
+        if (t.get() != NULL)
+        {
+          tracker_.reset(
+            new OrthancStone::ActiveTracker(t, controller->GetViewport().GetCanvasIdentifier()));
+          controller->GetViewport().Refresh();
+        }
+
+        break;
+      }
+
+      case EMSCRIPTEN_EVENT_MOUSEMOVE:
+        if (tracker_.get() != NULL)
+        {
+          std::unique_ptr<OrthancStone::PointerEvent> event(
+            ConvertMouseEvent(*mouseEvent, controller->GetViewport()));
+          tracker_->PointerMove(*event);
+          controller->GetViewport().Refresh();
+        }
+        break;
+
+      case EMSCRIPTEN_EVENT_MOUSEUP:
+        if (tracker_.get() != NULL)
+        {
+          std::unique_ptr<OrthancStone::PointerEvent> event(
+            ConvertMouseEvent(*mouseEvent, controller->GetViewport()));
+          tracker_->PointerUp(*event);
+          controller->GetViewport().Refresh();
+          if (!tracker_->IsAlive())
+            tracker_.reset();
+        }
+        break;
+
+      default:
+        break;
+    }
+  }
+
+  return true;
+}
+
+
+void SetupEvents(const std::string& canvas,
+                 boost::shared_ptr<OrthancStone::ViewportController>& controller)
+{
+  emscripten_set_mousedown_callback(canvas.c_str(), &controller, false, OnMouseEvent);
+  emscripten_set_mousemove_callback(canvas.c_str(), &controller, false, OnMouseEvent);
+  emscripten_set_mouseup_callback(canvas.c_str(), &controller, false, OnMouseEvent);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Deprecated/WebAssembly/index.html	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,16 @@
+<!doctype html>
+<html lang="en-us">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+    <title>Stone of Orthanc</title>
+  </head>
+  <body>
+    <h1>Available samples</h1>
+    <ul>
+      <li><a href="BasicScene.html">Basic scene</a></li>
+      <li><a href="BasicMPR.html">Basic MPR display</a></li>
+    </ul>
+  </body>
+</html>
--- a/Samples/MultiPlatform/BasicScene/BasicScene.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,275 +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 "BasicScene.h"
-
-// From Stone
-#include "Framework/Scene2D/Scene2D.h"
-#include "Framework/Scene2D/ColorTextureSceneLayer.h"
-#include "Framework/Scene2D/PolylineSceneLayer.h"
-#include "Framework/Scene2D/TextSceneLayer.h"
-
-#include "Framework/Scene2D/PanSceneTracker.h"
-#include "Framework/Scene2D/ZoomSceneTracker.h"
-#include "Framework/Scene2D/RotateSceneTracker.h"
-
-#include "Framework/Scene2D/CairoCompositor.h"
-
-// From Orthanc framework
-#include <Core/Images/Image.h>
-#include <Core/Images/ImageProcessing.h>
-#include <Core/Images/PngWriter.h>
-
-using namespace OrthancStone;
-
-const unsigned int BASIC_SCENE_FONT_SIZE = 32;
-const int BASIC_SCENE_LAYER_POSITION = 150;
-
-void PrepareScene(Scene2D& scene)
-{
-  //Scene2D& scene(*controller->GetScene());
-  // Texture of 2x2 size
-  {
-    Orthanc::Image i(Orthanc::PixelFormat_RGB24, 2, 2, false);
-
-    uint8_t *p = reinterpret_cast<uint8_t*>(i.GetRow(0));
-    p[0] = 255;
-    p[1] = 0;
-    p[2] = 0;
-
-    p[3] = 0;
-    p[4] = 255;
-    p[5] = 0;
-
-    p = reinterpret_cast<uint8_t*>(i.GetRow(1));
-    p[0] = 0;
-    p[1] = 0;
-    p[2] = 255;
-
-    p[3] = 255;
-    p[4] = 0;
-    p[5] = 0;
-
-    scene.SetLayer(12, new ColorTextureSceneLayer(i));
-
-    std::unique_ptr<ColorTextureSceneLayer> l(new ColorTextureSceneLayer(i));
-    l->SetOrigin(-3, 2);
-    l->SetPixelSpacing(1.5, 1);
-    l->SetAngle(20.0 / 180.0 * 3.14);
-    scene.SetLayer(14, l.release());
-  }
-
-  // Texture of 1x1 size
-  {
-    Orthanc::Image i(Orthanc::PixelFormat_RGB24, 1, 1, false);
-
-    uint8_t *p = reinterpret_cast<uint8_t*>(i.GetRow(0));
-    p[0] = 255;
-    p[1] = 0;
-    p[2] = 0;
-
-    std::unique_ptr<ColorTextureSceneLayer> l(new ColorTextureSceneLayer(i));
-    l->SetOrigin(-2, 1);
-    l->SetAngle(20.0 / 180.0 * 3.14);
-    scene.SetLayer(13, l.release());
-  }
-
-  // Some lines
-  {
-    std::unique_ptr<PolylineSceneLayer> layer(new PolylineSceneLayer);
-
-    layer->SetThickness(1);
-
-    PolylineSceneLayer::Chain chain;
-    chain.push_back(ScenePoint2D(0 - 0.5, 0 - 0.5));
-    chain.push_back(ScenePoint2D(0 - 0.5, 2 - 0.5));
-    chain.push_back(ScenePoint2D(2 - 0.5, 2 - 0.5));
-    chain.push_back(ScenePoint2D(2 - 0.5, 0 - 0.5));
-    layer->AddChain(chain, true, 255, 0, 0);
-
-    chain.clear();
-    chain.push_back(ScenePoint2D(-5, -5));
-    chain.push_back(ScenePoint2D(5, -5));
-    chain.push_back(ScenePoint2D(5, 5));
-    chain.push_back(ScenePoint2D(-5, 5));
-    layer->AddChain(chain, true, 0, 255, 0);
-
-    double dy = 1.01;
-    chain.clear();
-    chain.push_back(ScenePoint2D(-4, -4));
-    chain.push_back(ScenePoint2D(4, -4 + dy));
-    chain.push_back(ScenePoint2D(-4, -4 + 2.0 * dy));
-    chain.push_back(ScenePoint2D(4, 2));
-    layer->AddChain(chain, false, 0, 0, 255);
-
-    //    layer->SetColor(0,255, 255);
-    scene.SetLayer(50, layer.release());
-  }
-
-  // Some text
-  {
-    std::unique_ptr<TextSceneLayer> layer(new TextSceneLayer);
-    layer->SetText("Hello");
-    scene.SetLayer(100, layer.release());
-  }
-}
-
-#if ORTHANC_SANDBOXED == 0
-void TakeScreenshot(const std::string& target,
-                    const OrthancStone::Scene2D& scene,
-                    unsigned int canvasWidth,
-                    unsigned int canvasHeight)
-{
-  using namespace OrthancStone;
-  // Take a screenshot, then save it as PNG file
-  CairoCompositor compositor(scene, canvasWidth, canvasHeight);
-  compositor.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, BASIC_SCENE_FONT_SIZE, Orthanc::Encoding_Latin1);
-  compositor.Refresh();
-
-  Orthanc::ImageAccessor canvas;
-  compositor.GetCanvas().GetReadOnlyAccessor(canvas);
-
-  Orthanc::Image png(Orthanc::PixelFormat_RGB24, canvas.GetWidth(), canvas.GetHeight(), false);
-  Orthanc::ImageProcessing::Convert(png, canvas);
-
-  Orthanc::PngWriter writer;
-  writer.WriteToFile(target, png);
-}
-#endif
-
-void ShowCursorInfo(Scene2D& scene, const PointerEvent& pointerEvent)
-{
-  ScenePoint2D p = pointerEvent.GetMainPosition().Apply(scene.GetCanvasToSceneTransform());
-
-  char buf[64];
-  sprintf(buf, "(%0.02f,%0.02f)", p.GetX(), p.GetY());
-
-  if (scene.HasLayer(BASIC_SCENE_LAYER_POSITION))
-  {
-    TextSceneLayer& layer =
-        dynamic_cast<TextSceneLayer&>(scene.GetLayer(BASIC_SCENE_LAYER_POSITION));
-    layer.SetText(buf);
-    layer.SetPosition(p.GetX(), p.GetY());
-  }
-  else
-  {
-    std::unique_ptr<TextSceneLayer>
-        layer(new TextSceneLayer);
-    layer->SetColor(0, 255, 0);
-    layer->SetText(buf);
-    layer->SetBorder(20);
-    layer->SetAnchor(BitmapAnchor_BottomCenter);
-    layer->SetPosition(p.GetX(), p.GetY());
-    scene.SetLayer(BASIC_SCENE_LAYER_POSITION, layer.release());
-  }
-}
-
-
-
-bool BasicScene2DInteractor::OnMouseEvent(const GuiAdapterMouseEvent& event, const PointerEvent& pointerEvent)
-{
-  if (currentTracker_.get() != NULL)
-  {
-    switch (event.type)
-    {
-    case GUIADAPTER_EVENT_MOUSEUP:
-    {
-      currentTracker_->PointerUp(pointerEvent);
-      if (!currentTracker_->IsAlive())
-      {
-        currentTracker_.reset();
-      }
-    };break;
-    case GUIADAPTER_EVENT_MOUSEMOVE:
-    {
-      currentTracker_->PointerMove(pointerEvent);
-    };break;
-    default:
-      return false;
-    }
-    return true;
-  }
-  else if (event.type == GUIADAPTER_EVENT_MOUSEDOWN)
-  {
-    if (event.button == GUIADAPTER_MOUSEBUTTON_LEFT)
-    {
-      currentTracker_.reset(new RotateSceneTracker(viewportController_, pointerEvent));
-    }
-    else if (event.button == GUIADAPTER_MOUSEBUTTON_MIDDLE)
-    {
-      currentTracker_.reset(new PanSceneTracker(viewportController_, pointerEvent));
-    }
-    else if (event.button == GUIADAPTER_MOUSEBUTTON_RIGHT)
-    {
-      currentTracker_.reset(new ZoomSceneTracker(viewportController_, pointerEvent, viewportController_->GetViewport().GetCanvasHeight()));
-    }
-  }
-  else if (event.type == GUIADAPTER_EVENT_MOUSEMOVE)
-  {
-    if (showCursorInfo_)
-    {
-      Scene2D& scene(viewportController_->GetScene());
-      ShowCursorInfo(scene, pointerEvent);
-    }
-    return true;
-  }
-  return false;
-}
-
-bool BasicScene2DInteractor::OnKeyboardEvent(const GuiAdapterKeyboardEvent& guiEvent)
-{
-  if (guiEvent.type == GUIADAPTER_EVENT_KEYDOWN)
-  {
-    switch (guiEvent.sym[0])
-    {
-    case 's':
-    {
-      //viewportController_->FitContent(viewportController_->GetViewport().GetCanvasWidth(), viewportController_->GetViewport().GetCanvasHeight());
-      viewportController_->FitContent();
-      return true;
-    };
-#if ORTHANC_SANDBOXED == 0
-    case 'c':
-    {
-      Scene2D& scene(viewportController_->GetScene());
-      TakeScreenshot("screenshot.png", scene, viewportController_->GetViewport().GetCanvasWidth(), viewportController_->GetViewport().GetCanvasHeight());
-      return true;
-    }
-#endif
-    case 'd':
-    {
-      showCursorInfo_ = !showCursorInfo_;
-      if (!showCursorInfo_)
-      {
-        Scene2D& scene(viewportController_->GetScene());
-        scene.DeleteLayer(BASIC_SCENE_LAYER_POSITION);
-      }
-
-      return true;
-    }
-    }
-  }
-  return false;
-}
-
-bool BasicScene2DInteractor::OnWheelEvent(const GuiAdapterWheelEvent& guiEvent)
-{
-  return false;
-}
--- a/Samples/MultiPlatform/BasicScene/BasicScene.h	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,55 +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 "Framework/Scene2DViewport/ViewportController.h"
-#include "Framework/Scene2D/Scene2D.h"
-
-extern const unsigned int BASIC_SCENE_FONT_SIZE;
-extern const int BASIC_SCENE_LAYER_POSITION;
-
-extern void PrepareScene(OrthancStone::Scene2D& scene);
-extern void TakeScreenshot(const std::string& target,
-                           const OrthancStone::Scene2D& scene,
-                           unsigned int canvasWidth,
-                           unsigned int canvasHeight);
-
-
-#include "Applications/Generic/Scene2DInteractor.h"
-#include "Framework/Scene2DViewport/IFlexiblePointerTracker.h"
-
-
-class BasicScene2DInteractor : public OrthancStone::Scene2DInteractor
-{
-  boost::shared_ptr<OrthancStone::IFlexiblePointerTracker>  currentTracker_;
-  bool                                                      showCursorInfo_;
-public:
-  BasicScene2DInteractor(boost::shared_ptr<OrthancStone::ViewportController> viewportController) :
-    Scene2DInteractor(viewportController),
-    showCursorInfo_(false)
-  {}
-
-  virtual bool OnMouseEvent(const OrthancStone::GuiAdapterMouseEvent& event, const OrthancStone::PointerEvent& pointerEvent) override;
-  virtual bool OnKeyboardEvent(const OrthancStone::GuiAdapterKeyboardEvent& guiEvent) override;
-  virtual bool OnWheelEvent(const OrthancStone::GuiAdapterWheelEvent& guiEvent) override;
-};
-
--- a/Samples/MultiPlatform/BasicScene/mainQt.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,103 +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/>.
- **/
-
-#define GLEW_STATIC 1
-// From Stone
-#include "../../Framework/OpenGL/OpenGLIncludes.h"
-#include "../../Applications/Sdl/SdlWindow.h"
-#include "../../Framework/Scene2D/CairoCompositor.h"
-#include "../../Framework/Scene2D/ColorTextureSceneLayer.h"
-#include "../../Framework/Scene2D/OpenGLCompositor.h"
-#include "../../Framework/Scene2D/PanSceneTracker.h"
-#include "../../Framework/Scene2D/RotateSceneTracker.h"
-#include "../../Framework/Scene2D/Scene2D.h"
-#include "../../Framework/Scene2D/ZoomSceneTracker.h"
-#include "../../Framework/Scene2DViewport/ViewportController.h"
-#include "../../Framework/Scene2DViewport/UndoStack.h"
-
-#include "../../Framework/StoneInitialization.h"
-#include "../../Framework/Messages/MessageBroker.h"
-
-// From Orthanc framework
-#include <Core/Logging.h>
-#include <Core/OrthancException.h>
-#include <Core/Images/Image.h>
-#include <Core/Images/ImageProcessing.h>
-#include <Core/Images/PngWriter.h>
-
-#include <boost/make_shared.hpp>
-#include <boost/ref.hpp>
-#include "EmbeddedResources.h"
-
-#include <stdio.h>
-#include <QDebug>
-#include <QWindow>
-
-#include "BasicScene.h"
-
-
-using namespace OrthancStone;
-
-
-
-static void GLAPIENTRY OpenGLMessageCallback(GLenum source,
-                                             GLenum type,
-                                             GLuint id,
-                                             GLenum severity,
-                                             GLsizei length,
-                                             const GLchar* message,
-                                             const void* userParam )
-{
-  if (severity != GL_DEBUG_SEVERITY_NOTIFICATION)
-  {
-    fprintf(stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n",
-            ( type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : "" ),
-            type, severity, message );
-  }
-}
-
-extern void InitGL();
-
-#include <QApplication>
-#include "BasicSceneWindow.h"
-
-int main(int argc, char* argv[])
-{
-  QApplication a(argc, argv);
-
-  OrthancStone::Samples::BasicSceneWindow window;
-  window.show();
-  window.GetOpenGlWidget().Init();
-
-  MessageBroker broker;
-  boost::shared_ptr<UndoStack> undoStack(new UndoStack);
-  boost::shared_ptr<ViewportController> controller = boost::make_shared<ViewportController>(undoStack, boost::ref(broker), window.GetOpenGlWidget());
-  PrepareScene(controller->GetScene());
-
-  window.GetOpenGlWidget().GetCompositor().SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT,
-                     BASIC_SCENE_FONT_SIZE, Orthanc::Encoding_Latin1);
-
-  boost::shared_ptr<OrthancStone::Scene2DInteractor> interactor(new BasicScene2DInteractor(controller));
-  window.GetOpenGlWidget().SetInteractor(interactor);
-
-  controller->FitContent();
-
-  return a.exec();
-}
--- a/Samples/MultiPlatform/BasicScene/mainSdl.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,199 +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/>.
- **/
-
-
-// From Stone
-#include "Framework/Viewport/SdlViewport.h"
-#include "Framework/Scene2D/OpenGLCompositor.h"
-#include "Framework/Scene2DViewport/UndoStack.h"
-#include "Framework/StoneInitialization.h"
-#include "Framework/Messages/MessageBroker.h"
-
-// From Orthanc framework
-#include <Core/Logging.h>
-#include <Core/OrthancException.h>
-
-#include <boost/make_shared.hpp>
-#include <boost/ref.hpp>
-
-#include <SDL.h>
-#include <stdio.h>
-
-
-#include "BasicScene.h"
-
-using namespace OrthancStone;
-
-boost::shared_ptr<BasicScene2DInteractor> interactor;
-
-void HandleApplicationEvent(boost::shared_ptr<OrthancStone::ViewportController> controller,
-                            const SDL_Event& event)
-{
-  using namespace OrthancStone;
-  Scene2D& scene(controller->GetScene());
-  if (event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP || event.type == SDL_MOUSEMOTION)
-  {
-    // TODO: this code is copy/pasted from GuiAdapter::Run() -> find the right place
-    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 guiEvent;
-    ConvertFromPlatform(guiEvent, ctrlPressed, shiftPressed, altPressed, event);
-    PointerEvent pointerEvent;
-    pointerEvent.AddPosition(controller->GetViewport().GetPixelCenterCoordinates(event.button.x, event.button.y));
-
-    interactor->OnMouseEvent(guiEvent, pointerEvent);
-    return;
-  }
-  else if ((event.type == SDL_KEYDOWN || event.type == SDL_KEYUP) && event.key.repeat == 0  /* Ignore key bounce */)
-  {
-    GuiAdapterKeyboardEvent guiEvent;
-    ConvertFromPlatform(guiEvent, event);
-
-    interactor->OnKeyboardEvent(guiEvent);
-  }
-
-}
-
-
-static void GLAPIENTRY
-OpenGLMessageCallback(GLenum source,
-                      GLenum type,
-                      GLuint id,
-                      GLenum severity,
-                      GLsizei length,
-                      const GLchar* message,
-                      const void* userParam )
-{
-  if (severity != GL_DEBUG_SEVERITY_NOTIFICATION)
-  {
-    fprintf(stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n",
-            ( type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : "" ),
-            type, severity, message );
-  }
-}
-
-
-void Run(boost::shared_ptr<OrthancStone::ViewportController> controller)
-{
-  SdlViewport& sdlViewport = dynamic_cast<SdlViewport&>(controller->GetViewport());
-
-  glEnable(GL_DEBUG_OUTPUT);
-  glDebugMessageCallback(OpenGLMessageCallback, 0);
-
-  controller->GetViewport().GetCompositor().SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT,
-                     BASIC_SCENE_FONT_SIZE, Orthanc::Encoding_Latin1);
-
-  controller->GetViewport().Refresh();
-  controller->FitContent();
-
-
-  bool stop = false;
-  while (!stop)
-  {
-    controller->GetViewport().Refresh();
-
-    SDL_Event event;
-    while (!stop &&
-           SDL_PollEvent(&event))
-    {
-      if (event.type == SDL_QUIT)
-      {
-        stop = true;
-        break;
-      }
-      else if (event.type == SDL_WINDOWEVENT &&
-               event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
-      {
-        sdlViewport.UpdateSize(event.window.data1, event.window.data2);
-      }
-      else if (event.type == SDL_KEYDOWN &&
-               event.key.repeat == 0 /* Ignore key bounce */)
-      {
-        switch (event.key.keysym.sym)
-        {
-          case SDLK_f:
-            sdlViewport.GetWindow().ToggleMaximize();
-            break;
-              
-          case SDLK_q:
-            stop = true;
-            break;
-
-          default:
-            break;
-        }
-      }
-      
-      HandleApplicationEvent(controller, event);
-    }
-
-    SDL_Delay(1);
-  }
-  interactor.reset();
-}
-
-
-
-
-/**
- * IMPORTANT: The full arguments to "main()" are needed for SDL on
- * Windows. Otherwise, one gets the linking error "undefined reference
- * to `SDL_main'". https://wiki.libsdl.org/FAQWindows
- **/
-int main(int argc, char* argv[])
-{
-  using namespace OrthancStone;
-  StoneInitialize();
-  Orthanc::Logging::EnableInfoLevel(true);
-
-  try
-  {
-    SdlOpenGLViewport viewport("Hello", 1024, 768);
-    MessageBroker broker;
-    boost::shared_ptr<UndoStack> undoStack(new UndoStack);
-    boost::shared_ptr<ViewportController> controller = boost::make_shared<ViewportController>(undoStack, boost::ref(broker), boost::ref(viewport));
-    interactor.reset(new BasicScene2DInteractor(controller));
-    PrepareScene(controller->GetScene());
-    Run(controller);
-  }
-  catch (Orthanc::OrthancException& e)
-  {
-    LOG(ERROR) << "EXCEPTION: " << e.What();
-  }
-
-  StoneFinalize();
-
-  return 0;
-}
--- a/Samples/Qt/BasicSceneWindow.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +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 "../../Framework/OpenGL/OpenGLIncludes.h"
-#include "BasicSceneWindow.h"
-
-/**
- * Don't use "ui_MainWindow.h" instead of <ui_MainWindow.h> below, as
- * this makes CMake unable to detect when the UI file changes.
- **/
-#include <ui_BasicSceneWindow.h>
-#include "../../Applications/Samples/SampleApplicationBase.h"
-
-namespace OrthancStone
-{
-  namespace Samples
-  {
-
-    BasicSceneWindow::BasicSceneWindow(
-      QWidget *parent) :
-      ui_(new Ui::BasicSceneWindow)
-    {
-      ui_->setupUi(this);
-    }
-
-    BasicSceneWindow::~BasicSceneWindow()
-    {
-      delete ui_;
-    }
-
-    QStoneOpenGlWidget& BasicSceneWindow::GetOpenGlWidget()
-    {
-      return *(ui_->centralWidget);
-    }
-
-  }
-}
--- a/Samples/Qt/BasicSceneWindow.h	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +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 <QMainWindow>
-#include <QStoneOpenGlWidget.h>
-// #include "../../Qt/QCairoWidget.h"
-// #include "../../Qt/QStoneMainWindow.h"
-
-namespace Ui 
-{
-  class BasicSceneWindow;
-}
-
-namespace OrthancStone
-{
-  namespace Samples
-  {
-
-    //class SampleSingleCanvasApplicationBase;
-
-    class BasicSceneWindow : public QMainWindow
-    {
-      Q_OBJECT
-
-    private:
-      Ui::BasicSceneWindow*   ui_;
-      //SampleSingleCanvasApplicationBase&  stoneSampleApplication_;
-
-    public:
-      explicit BasicSceneWindow(QWidget *parent = 0);
-      ~BasicSceneWindow();
-
-      QStoneOpenGlWidget& GetOpenGlWidget();
-    };
-  }
-}
--- a/Samples/Qt/BasicSceneWindow.ui	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,84 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>BasicSceneWindow</class>
- <widget class="QMainWindow" name="BasicSceneWindow">
-  <property name="geometry">
-   <rect>
-    <x>0</x>
-    <y>0</y>
-    <width>903</width>
-    <height>634</height>
-   </rect>
-  </property>
-  <property name="minimumSize">
-   <size>
-    <width>500</width>
-    <height>300</height>
-   </size>
-  </property>
-  <property name="baseSize">
-   <size>
-    <width>500</width>
-    <height>300</height>
-   </size>
-  </property>
-  <property name="windowTitle">
-   <string>Stone of Orthanc</string>
-  </property>
-  <property name="layoutDirection">
-   <enum>Qt::LeftToRight</enum>
-  </property>
-  <widget class="QWidget" name="mainWidget">
-   <property name="sizePolicy">
-    <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
-     <horstretch>0</horstretch>
-     <verstretch>0</verstretch>
-    </sizepolicy>
-   </property>
-   <property name="layoutDirection">
-    <enum>Qt::LeftToRight</enum>
-   </property>
-   <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0">
-    <property name="sizeConstraint">
-     <enum>QLayout::SetDefaultConstraint</enum>
-    </property>
-    <item>
-     <widget class="OrthancStone::QStoneOpenGlWidget" name="centralWidget" native="true">
-      <property name="minimumSize">
-       <size>
-        <width>0</width>
-        <height>500</height>
-       </size>
-      </property>
-     </widget>
-    </item>
-   </layout>
-  </widget>
-  <widget class="QMenuBar" name="menubar">
-   <property name="geometry">
-    <rect>
-     <x>0</x>
-     <y>0</y>
-     <width>903</width>
-     <height>21</height>
-    </rect>
-   </property>
-   <widget class="QMenu" name="menuTest">
-    <property name="title">
-     <string>Test</string>
-    </property>
-   </widget>
-   <addaction name="menuTest"/>
-  </widget>
-  <widget class="QStatusBar" name="statusbar"/>
- </widget>
- <customwidgets>
-  <customwidget>
-   <class>QStoneOpenGlWidget</class>
-   <extends>QWidget</extends>
-   <header location="global">QStoneOpenGlWidget.h</header>
-  </customwidget>
- </customwidgets>
- <resources/>
- <connections/>
-</ui>
--- a/Samples/Qt/CMakeLists.txt	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,83 +0,0 @@
-cmake_minimum_required(VERSION 2.8.3)
-
-#####################################################################
-## Configuration of the Orthanc framework
-#####################################################################
-
-# This CMake file defines the "ORTHANC_STONE_VERSION" macro, so it
-# must be the first inclusion
-include(${CMAKE_SOURCE_DIR}/../../Resources/CMake/Version.cmake)
-
-if (ORTHANC_STONE_VERSION STREQUAL "mainline")
-  set(ORTHANC_FRAMEWORK_VERSION "mainline")
-  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
-else()
-  set(ORTHANC_FRAMEWORK_VERSION "1.5.7")
-  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
-endif()
-
-set(ORTHANC_FRAMEWORK_SOURCE "${ORTHANC_FRAMEWORK_DEFAULT_SOURCE}" CACHE STRING "Source of the Orthanc source code (can be \"hg\", \"archive\", \"web\" or \"path\")")
-set(ORTHANC_FRAMEWORK_ARCHIVE "" CACHE STRING "Path to the Orthanc archive, if ORTHANC_FRAMEWORK_SOURCE is \"archive\"")
-set(ORTHANC_FRAMEWORK_ROOT "" CACHE STRING "Path to the Orthanc source directory, if ORTHANC_FRAMEWORK_SOURCE is \"path\"")
-
-
-#####################################################################
-## Configuration of the Stone framework
-#####################################################################
-
-include(${CMAKE_SOURCE_DIR}/../../Resources/CMake/OrthancStoneParameters.cmake)
-include(${ORTHANC_ROOT}/Resources/CMake/DownloadPackage.cmake)
-
-DownloadPackage(
-  "a24b8136b8f3bb93f166baf97d9328de"
-  "http://orthanc.osimis.io/ThirdPartyDownloads/ubuntu-font-family-0.83.zip"
-  "${CMAKE_BINARY_DIR}/ubuntu-font-family-0.83")
-
-set(ORTHANC_STONE_APPLICATION_RESOURCES
-  UBUNTU_FONT  ${CMAKE_BINARY_DIR}/ubuntu-font-family-0.83/Ubuntu-R.ttf
-  )
-
-SET(ENABLE_GOOGLE_TEST OFF)
-SET(ENABLE_LOCALE ON)
-SET(ENABLE_QT ON)
-SET(ENABLE_SDL OFF)
-SET(ENABLE_WEB_CLIENT ON)
-SET(ORTHANC_SANDBOXED OFF)
-LIST(APPEND ORTHANC_BOOST_COMPONENTS program_options)
-
-include(${CMAKE_SOURCE_DIR}/../../Resources/CMake/OrthancStoneConfiguration.cmake)
-
-add_definitions(
-  -DORTHANC_ENABLE_LOGGING_PLUGIN=0
-  )
-#####################################################################
-## Build the samples
-#####################################################################
-
-add_library(OrthancStone STATIC
-  ${ORTHANC_STONE_SOURCES}
-  )
-
-list(APPEND BASIC_SCENE_APPLICATIONS_SOURCES
-  BasicSceneWindow.cpp
-  )
-
-ORTHANC_QT_WRAP_UI(BASIC_SCENE_APPLICATIONS_SOURCES
-  BasicSceneWindow.ui
-  )
-
-ORTHANC_QT_WRAP_CPP(BASIC_SCENE_APPLICATIONS_SOURCES
-  BasicSceneWindow.h
-  QStoneOpenGlWidget.h
-  )
-
-add_executable(MpBasicScene
-  ${CMAKE_CURRENT_LIST_DIR}/../MultiPlatform/BasicScene/BasicScene.h
-  ${CMAKE_CURRENT_LIST_DIR}/../MultiPlatform/BasicScene/BasicScene.cpp
-  ${CMAKE_CURRENT_LIST_DIR}/../MultiPlatform/BasicScene/mainQt.cpp
-  QStoneOpenGlWidget.cpp
-  ${BASIC_SCENE_APPLICATIONS_SOURCES}
-  )
-
-target_include_directories(MpBasicScene PUBLIC ${CMAKE_SOURCE_DIR} ${ORTHANC_STONE_ROOT})
-target_link_libraries(MpBasicScene OrthancStone)
--- a/Samples/Qt/QStoneOpenGlWidget.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,171 +0,0 @@
-#include "../../Framework/OpenGL/OpenGLIncludes.h"
-#include "QStoneOpenGlWidget.h"
-
-#include <QMouseEvent>
-
-using namespace OrthancStone;
-
-void QStoneOpenGlWidget::initializeGL()
-{
-  glewInit();
-}
-
-void QStoneOpenGlWidget::MakeCurrent()
-{
-  this->makeCurrent();
-}
-
-void QStoneOpenGlWidget::resizeGL(int w, int h)
-{
-
-}
-
-void QStoneOpenGlWidget::paintGL()
-{
-  if (compositor_)
-  {
-    compositor_->Refresh();
-  }
-  doneCurrent();
-}
-
-void ConvertFromPlatform(
-    OrthancStone::GuiAdapterMouseEvent& guiEvent,
-    PointerEvent& pointerEvent,
-    const QMouseEvent& qtEvent,
-    const IViewport& viewport)
-{
-  guiEvent.targetX = qtEvent.x();
-  guiEvent.targetY = qtEvent.y();
-  pointerEvent.AddPosition(viewport.GetPixelCenterCoordinates(guiEvent.targetX, guiEvent.targetY));
-
-  switch (qtEvent.button())
-  {
-  case Qt::LeftButton: guiEvent.button = OrthancStone::GUIADAPTER_MOUSEBUTTON_LEFT; break;
-  case Qt::MiddleButton: guiEvent.button = OrthancStone::GUIADAPTER_MOUSEBUTTON_MIDDLE; break;
-  case Qt::RightButton: guiEvent.button = OrthancStone::GUIADAPTER_MOUSEBUTTON_RIGHT; break;
-  default:
-    guiEvent.button = OrthancStone::GUIADAPTER_MOUSEBUTTON_LEFT;
-  }
-
-  if (qtEvent.modifiers().testFlag(Qt::ShiftModifier))
-  {
-    guiEvent.shiftKey = true;
-  }
-  if (qtEvent.modifiers().testFlag(Qt::ControlModifier))
-  {
-    guiEvent.ctrlKey = true;
-  }
-  if (qtEvent.modifiers().testFlag(Qt::AltModifier))
-  {
-    guiEvent.altKey = true;
-  }
-}
-
-void QStoneOpenGlWidget::mouseEvent(QMouseEvent* qtEvent, OrthancStone::GuiAdapterHidEventType guiEventType)
-{
-  OrthancStone::GuiAdapterMouseEvent guiEvent;
-  PointerEvent pointerEvent;
-  ConvertFromPlatform(guiEvent, pointerEvent, *qtEvent, *this);
-  guiEvent.type = guiEventType;
-
-  if (sceneInteractor_.get() != NULL && compositor_.get() != NULL)
-  {
-    sceneInteractor_->OnMouseEvent(guiEvent, pointerEvent);
-  }
-
-  // force redraw of the OpenGL widget
-  update();
-}
-
-void QStoneOpenGlWidget::mousePressEvent(QMouseEvent* qtEvent)
-{
-  mouseEvent(qtEvent, GUIADAPTER_EVENT_MOUSEDOWN);
-}
-
-void QStoneOpenGlWidget::mouseMoveEvent(QMouseEvent* qtEvent)
-{
-  mouseEvent(qtEvent, GUIADAPTER_EVENT_MOUSEMOVE);
-}
-
-void QStoneOpenGlWidget::mouseReleaseEvent(QMouseEvent* qtEvent)
-{
-  mouseEvent(qtEvent, GUIADAPTER_EVENT_MOUSEUP);
-}
-
-void ConvertFromPlatform(
-    OrthancStone::GuiAdapterKeyboardEvent& guiEvent,
-    const QKeyEvent& qtEvent)
-{
-  if (qtEvent.text().length() > 0)
-  {
-    guiEvent.sym[0] = qtEvent.text()[0].cell();
-  }
-  else
-  {
-    guiEvent.sym[0] = 0;
-  }
-  guiEvent.sym[1] = 0;
-
-  if (qtEvent.modifiers().testFlag(Qt::ShiftModifier))
-  {
-    guiEvent.shiftKey = true;
-  }
-  if (qtEvent.modifiers().testFlag(Qt::ControlModifier))
-  {
-    guiEvent.ctrlKey = true;
-  }
-  if (qtEvent.modifiers().testFlag(Qt::AltModifier))
-  {
-    guiEvent.altKey = true;
-  }
-
-}
-
-
-bool QStoneOpenGlWidget::keyEvent(QKeyEvent* qtEvent, OrthancStone::GuiAdapterHidEventType guiEventType)
-{
-  bool handled = false;
-  OrthancStone::GuiAdapterKeyboardEvent guiEvent;
-  ConvertFromPlatform(guiEvent, *qtEvent);
-  guiEvent.type = guiEventType;
-
-  if (sceneInteractor_.get() != NULL && compositor_.get() != NULL)
-  {
-    handled = sceneInteractor_->OnKeyboardEvent(guiEvent);
-
-    if (handled)
-    {
-      // force redraw of the OpenGL widget
-      update();
-    }
-  }
-  return handled;
-}
-
-void QStoneOpenGlWidget::keyPressEvent(QKeyEvent *qtEvent)
-{
-  bool handled = keyEvent(qtEvent, GUIADAPTER_EVENT_KEYDOWN);
-  if (!handled)
-  {
-    QOpenGLWidget::keyPressEvent(qtEvent);
-  }
-}
-
-void QStoneOpenGlWidget::keyReleaseEvent(QKeyEvent *qtEvent)
-{
-  bool handled = keyEvent(qtEvent, GUIADAPTER_EVENT_KEYUP);
-  if (!handled)
-  {
-    QOpenGLWidget::keyPressEvent(qtEvent);
-  }
-}
-
-void QStoneOpenGlWidget::wheelEvent(QWheelEvent *qtEvent)
-{
-  OrthancStone::GuiAdapterWheelEvent guiEvent;
-  throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-
-  // force redraw of the OpenGL widget
-  update();
-}
--- a/Samples/Qt/QStoneOpenGlWidget.h	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,90 +0,0 @@
-#pragma once
-#include "../../Framework/OpenGL/OpenGLIncludes.h"
-#include <QOpenGLWidget>
-#include <QOpenGLFunctions>
-#include <QOpenGLContext>
-
-#include <boost/shared_ptr.hpp>
-#include "../../Framework/OpenGL/IOpenGLContext.h"
-#include "../../Framework/Scene2D/OpenGLCompositor.h"
-#include "../../Framework/Viewport/ViewportBase.h"
-#include "../../Applications/Generic/Scene2DInteractor.h"
-
-namespace OrthancStone
-{
-  class QStoneOpenGlWidget :
-      public QOpenGLWidget,
-      public OpenGL::IOpenGLContext,
-      public ViewportBase
-  {
-    std::unique_ptr<OrthancStone::OpenGLCompositor> compositor_;
-    boost::shared_ptr<Scene2DInteractor> sceneInteractor_;
-    QOpenGLContext                        openGlContext_;
-
-  public:
-    QStoneOpenGlWidget(QWidget *parent) :
-      QOpenGLWidget(parent),
-      ViewportBase("QtStoneOpenGlWidget")  // TODO: we shall be able to define a name but construction time is too early !
-    {
-      setFocusPolicy(Qt::StrongFocus);  // to enable keyPressEvent
-      setMouseTracking(true);           // to enable mouseMoveEvent event when no button is pressed
-    }
-
-    void Init()
-    {
-      QSurfaceFormat requestedFormat;
-      requestedFormat.setVersion( 2, 0 );
-      openGlContext_.setFormat( requestedFormat );
-      openGlContext_.create();
-      openGlContext_.makeCurrent(context()->surface());
-
-      compositor_.reset(new OpenGLCompositor(*this, GetScene()));
-    }
-
-  protected:
-
-    //**** QWidget overrides
-    void initializeGL() override;
-    void resizeGL(int w, int h) override;
-    void paintGL() override;
-
-    void mousePressEvent(QMouseEvent* event) override;
-    void mouseMoveEvent(QMouseEvent* event) override;
-    void mouseReleaseEvent(QMouseEvent* event) override;
-    void keyPressEvent(QKeyEvent* event) override;
-    void keyReleaseEvent(QKeyEvent *event) override;
-    void wheelEvent(QWheelEvent* event) override;
-
-    //**** IOpenGLContext overrides
-
-    virtual void MakeCurrent() override;
-    virtual void SwapBuffer() override {}
-
-    virtual unsigned int GetCanvasWidth() const override
-    {
-      return this->width();
-    }
-
-    virtual unsigned int GetCanvasHeight() const override
-    {
-      return this->height();
-    }
-
-  public:
-
-    void SetInteractor(boost::shared_ptr<Scene2DInteractor> sceneInteractor)
-    {
-      sceneInteractor_ = sceneInteractor;
-    }
-
-    virtual ICompositor& GetCompositor()
-    {
-      return *compositor_;
-    }
-
-  protected:
-    void mouseEvent(QMouseEvent* qtEvent, OrthancStone::GuiAdapterHidEventType guiEventType);
-    bool keyEvent(QKeyEvent* qtEvent, OrthancStone::GuiAdapterHidEventType guiEventType);
-
-  };
-}
--- a/Samples/Qt/Scene2DInteractor.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,72 +0,0 @@
-#include "Scene2DInteractor.h"
-
-#include "../../Framework/Scene2D/PanSceneTracker.h"
-#include "../../Framework/Scene2D/ZoomSceneTracker.h"
-#include "../../Framework/Scene2D/RotateSceneTracker.h"
-
-
-namespace OrthancStone
-{
-
-}
-
-using namespace OrthancStone;
-
-
-bool BasicScene2DInteractor::OnMouseEvent(const GuiAdapterMouseEvent& event, const PointerEvent& pointerEvent)
-{
-  if (currentTracker_.get() != NULL)
-  {
-    switch (event.type)
-    {
-    case GUIADAPTER_EVENT_MOUSEUP:
-    {
-      currentTracker_->PointerUp(pointerEvent);
-      if (!currentTracker_->IsAlive())
-      {
-        currentTracker_.reset();
-      }
-    };break;
-    case GUIADAPTER_EVENT_MOUSEMOVE:
-    {
-      currentTracker_->PointerMove(pointerEvent);
-    };break;
-    }
-    return true;
-  }
-  else
-  {
-    if (event.button == GUIADAPTER_MOUSEBUTTON_LEFT)
-    {
-      currentTracker_.reset(new RotateSceneTracker(viewportController_, pointerEvent));
-    }
-    else if (event.button == GUIADAPTER_MOUSEBUTTON_MIDDLE)
-    {
-      currentTracker_.reset(new PanSceneTracker(viewportController_, pointerEvent));
-    }
-    else if (event.button == GUIADAPTER_MOUSEBUTTON_RIGHT && compositor_.get() != NULL)
-    {
-      currentTracker_.reset(new ZoomSceneTracker(viewportController_, pointerEvent, compositor_->GetHeight()));
-    }
-    return true;
-  }
-  return false;
-}
-
-bool BasicScene2DInteractor::OnKeyboardEvent(const GuiAdapterKeyboardEvent& guiEvent)
-{
-  switch (guiEvent.sym[0])
-  {
-  case 's':
-  {
-    viewportController_->FitContent(compositor_->GetWidth(), compositor_->GetHeight());
-    return true;
-  };
-  }
-  return false;
-}
-
-bool BasicScene2DInteractor::OnWheelEvent(const GuiAdapterWheelEvent& guiEvent)
-{
-  return false;
-}
--- a/Samples/Qt/Scene2DInteractor.h	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,19 +0,0 @@
-#pragma once
-
-#include "../../Applications/Generic/Scene2DInteractor.h"
-#include "../../Framework/Scene2DViewport/IFlexiblePointerTracker.h"
-
-
-class BasicScene2DInteractor : public OrthancStone::Scene2DInteractor
-{
-  boost::shared_ptr<OrthancStone::IFlexiblePointerTracker>  currentTracker_;
-public:
-  BasicScene2DInteractor(boost::shared_ptr<OrthancStone::ViewportController> viewportController) :
-    Scene2DInteractor(viewportController)
-  {}
-
-  virtual bool OnMouseEvent(const OrthancStone::GuiAdapterMouseEvent& event, const OrthancStone::PointerEvent& pointerEvent) override;
-  virtual bool OnKeyboardEvent(const OrthancStone::GuiAdapterKeyboardEvent& guiEvent);
-  virtual bool OnWheelEvent(const OrthancStone::GuiAdapterWheelEvent& guiEvent);
-};
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/README.md	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,183 @@
+General
+=======
+These samples assume that a recent version of Orthanc is checked out in an
+`orthanc` folder next to the `orthanc-stone` folder. Let's call the top folder
+the `devroot` folder. This name does not matter and is not used anywhere.
+
+Here's the directory layout that we suggest:
+
+```
+devroot/
+ |
+ +- orthanc/
+ |
+ +- orthanc-stone/
+ |
+ ...
+```
+
+ Orthanc can be retrieved with:
+ ```
+ hg clone https://hg.orthanc-server.com/orthanc
+ ```
+
+Furthermore, the samples usually assume that an Orthanc is running locally,
+without authentication, on port 8042. The samples can easily be tweaked if 
+your setup is different.
+
+When Dicom resources are to be displayed, their IDs can be supplied in the 
+various ways suitable for the platform (command-line arguments, URL parameters
+or through the GUI)
+
+ 
+WebAssembly samples
+===================
+
+Building the WebAssembly samples require the Emscripten SDK 
+(https://emscripten.org/). This SDK goes far beyond the simple compilation to
+the wasm (Web Assembly) bytecode and provides a comprehensive library that 
+eases porting native C and C++ programs and libraries. The Emscripten SDK also
+makes it easy to generate the companion Javascript files requires to use a 
+wasm module in a web application.
+
+Although Emscripten runs on all major platforms, Stone of Orthanc is developed
+and tested with the Linux version of Emscripten.
+
+Emscripten runs perfectly fine under the Windows Subsystem for Linux (that is
+the environment used quite often by the Stone of Orthanc team)
+
+**Important note:** The following examples **and the build scripts** will 
+assume that you have installed the Emscripten SDK in `~/apps/emsdk`.
+
+The following packages should get you going (a Debian-like distribution such 
+as Debian or Ubuntu is assumed)
+
+```
+sudo apt-get update 
+sudo apt-get install -y build-essential curl wget git python cmake pkg-config
+sudo apt-get install -y mercurial unzip npm ninja-build p7zip-full gettext-base 
+```
+
+SingleFrameViewer
+-----------------
+
+This sample application displays a single frame of a Dicom instance that can
+be loaded from Orthanc, either by using the Orthanc REST API or through the 
+Dicomweb server functionality of Orthanc.
+
+This barebones sample uses plain Javascript and requires the 
+Emscripten toolchain and cmake, in addition to a few standard packages.
+
+Here's how you can build it: create the following script (for instance, 
+`build-wasm-SingleFrameViewer.sh`) one level above the orthanc-stone repository,
+thus, in the folder we have called `devroot`.
+
+If you feel confident, you can also simply read the following script and 
+enter the commands interactively in the terminal.
+
+```
+#!/bin/bash
+
+if [ ! -d "orthanc-stone" ]; then
+  echo "This script must be run from the folder one level above orthanc-stone"
+  exit 1
+fi
+
+if [[ ! $# -eq 1 ]]; then
+  echo "Usage:"
+  echo "  $0 [BUILD_TYPE]"
+  echo ""
+  echo "  with:"
+  echo "    BUILD_TYPE = Debug, RelWithDebInfo or Release"
+  echo ""
+  exit 1
+fi
+
+# define the variables that we'll use
+buildType=$1
+buildFolderName="`pwd`/out/build-stone-wasm-SingleFrameViewer-$buildType"
+installFolderName="`pwd`/out/install-stone-wasm-SingleFrameViewer-$buildType"
+
+# configure the environment to use Emscripten
+. ~/apps/emsdk/emsdk_env.sh
+
+
+mkdir -p $buildFolderName
+
+# change current folder to the build folder
+pushd $buildFolderName
+
+emcmake cmake -G "Ninja" \
+  -DCMAKE_BUILD_TYPE=$buildType \
+  -DCMAKE_INSTALL_PREFIX=$installFolderName \
+  -DSTATIC_BUILD=ON -DOPENSSL_NO_CAPIENG=ON -DALLOW_DOWNLOADS=ON \
+  ../orthanc-stone/Samples/WebAssembly/SingleFrameViewer
+
+# perform build + installation
+ninja install
+
+# restore the original working folder
+popd
+
+echo "If all went well, the output files can be found in $installFolderName:"
+
+ls $installFolderName```
+```
+
+Simply navigate to the dev root, and execute the script:
+
+```
+./build-wasm-SingleFrameViewer.sh RelWithDebInfo
+```
+
+I suggest that you do *not* use the `Debug` confirmation unless you really 
+need it, for the additional checks that are made will lead to a very long 
+build time and much slower execution (more severe than with a native non-wasm
+target)
+
+Native samples
+=================
+
+SdlSimpleViewer
+---------------
+
+### Windows build 
+
+Here's how to build the SdlSimpleViewer example using Visual Studio 2019
+(the shell is Powershell, but the legacy shell can also be used with some 
+tweaks). This example is meant to be launched from the folder above 
+orthanc-stone.
+
+```
+  # create the build folder and navigate to it
+  $buildDir = "build-stone-sdlviewer-msvc16-x64"
+
+  if (-not (Test-Path $buildDir)) {
+    mkdir -p $buildDir | Out-Null
+  }
+  
+  cd $buildDir
+  
+  # perform the configuration
+  cmake -G "Visual Studio 16 2019" -A x64 `
+    -DMSVC_MULTIPLE_PROCESSES=ON `
+    -DALLOW_DOWNLOADS=ON `
+    -DSTATIC_BUILD=ON `
+    -DOPENSSL_NO_CAPIENG=ON `
+    ../orthanc-stone/Samples/Sdl/SimpleViewer
+  
+  $solutionPath = ls -filter *.sln
+  Write-Host "Solution file(s) available at: $solutionPath"
+```
+
+The initial configuration step will be quite lengthy, for CMake needs to 
+setup its internal cache based on your environment and build tools.
+
+Subsequent runs will be several orders of magnitude faster!
+
+One the solution (.sln) file is ready, you can open it using the Visual Studio
+IDE and choose Build --> Build solution.
+
+An alternative is to execute `cmake --build .` in the build folder created by
+the script.
+
--- a/Samples/Sdl/BasicScene.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,418 +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/>.
- **/
-
-
-// From Stone
-#include "../../Framework/Viewport/SdlViewport.h"
-#include "../../Framework/Scene2D/CairoCompositor.h"
-#include "../../Framework/Scene2D/ColorTextureSceneLayer.h"
-#include "../../Framework/Scene2D/OpenGLCompositor.h"
-#include "../../Framework/Scene2D/PanSceneTracker.h"
-#include "../../Framework/Scene2D/RotateSceneTracker.h"
-#include "../../Framework/Scene2D/ZoomSceneTracker.h"
-#include "../../Framework/Scene2DViewport/ViewportController.h"
-#include "../../Framework/Scene2DViewport/UndoStack.h"
-
-#include "../../Framework/StoneInitialization.h"
-#include "../../Framework/Messages/MessageBroker.h"
-
-// From Orthanc framework
-#include <Core/Logging.h>
-#include <Core/OrthancException.h>
-#include <Core/Images/Image.h>
-#include <Core/Images/ImageProcessing.h>
-#include <Core/Images/PngWriter.h>
-
-#include <boost/make_shared.hpp>
-
-#include <SDL.h>
-#include <stdio.h>
-
-static const unsigned int FONT_SIZE = 32;
-static const int LAYER_POSITION = 150;
-
-#define OPENGL_ENABLED 0
-
-void PrepareScene(OrthancStone::Scene2D& scene)
-{
-  using namespace OrthancStone;
-
-  // Texture of 2x2 size
-  {
-    Orthanc::Image i(Orthanc::PixelFormat_RGB24, 2, 2, false);
-    
-    uint8_t *p = reinterpret_cast<uint8_t*>(i.GetRow(0));
-    p[0] = 255;
-    p[1] = 0;
-    p[2] = 0;
-
-    p[3] = 0;
-    p[4] = 255;
-    p[5] = 0;
-
-    p = reinterpret_cast<uint8_t*>(i.GetRow(1));
-    p[0] = 0;
-    p[1] = 0;
-    p[2] = 255;
-
-    p[3] = 255;
-    p[4] = 0;
-    p[5] = 0;
-
-    scene.SetLayer(12, new ColorTextureSceneLayer(i));
-
-    std::unique_ptr<ColorTextureSceneLayer> l(new ColorTextureSceneLayer(i));
-    l->SetOrigin(-3, 2);
-    l->SetPixelSpacing(1.5, 1);
-    l->SetAngle(20.0 / 180.0 * M_PI);
-    scene.SetLayer(14, l.release());
-  }
-
-  // Texture of 1x1 size
-  {
-    Orthanc::Image i(Orthanc::PixelFormat_RGB24, 1, 1, false);
-    
-    uint8_t *p = reinterpret_cast<uint8_t*>(i.GetRow(0));
-    p[0] = 255;
-    p[1] = 0;
-    p[2] = 0;
-
-    std::unique_ptr<ColorTextureSceneLayer> l(new ColorTextureSceneLayer(i));
-    l->SetOrigin(-2, 1);
-    l->SetAngle(20.0 / 180.0 * M_PI);
-    scene.SetLayer(13, l.release());
-  }
-
-  // Some lines
-  {
-    std::unique_ptr<PolylineSceneLayer> layer(new PolylineSceneLayer);
-
-    layer->SetThickness(10);
-
-    PolylineSceneLayer::Chain chain;
-    chain.push_back(ScenePoint2D(0 - 0.5, 0 - 0.5));
-    chain.push_back(ScenePoint2D(0 - 0.5, 2 - 0.5));
-    chain.push_back(ScenePoint2D(2 - 0.5, 2 - 0.5));
-    chain.push_back(ScenePoint2D(2 - 0.5, 0 - 0.5));
-    layer->AddChain(chain, true, 255, 0, 0);
-
-    chain.clear();
-    chain.push_back(ScenePoint2D(-5, -5));
-    chain.push_back(ScenePoint2D(5, -5));
-    chain.push_back(ScenePoint2D(5, 5));
-    chain.push_back(ScenePoint2D(-5, 5));
-    layer->AddChain(chain, true, 0, 255, 0);
-
-    double dy = 1.01;
-    chain.clear();
-    chain.push_back(ScenePoint2D(-4, -4));
-    chain.push_back(ScenePoint2D(4, -4 + dy));
-    chain.push_back(ScenePoint2D(-4, -4 + 2.0 * dy));
-    chain.push_back(ScenePoint2D(4, 2));
-    layer->AddChain(chain, false, 0, 0, 255);
-
-    scene.SetLayer(50, layer.release());
-  }
-
-  // Some text
-  {
-    std::unique_ptr<TextSceneLayer> layer(new TextSceneLayer);
-    layer->SetText("Hello");
-    scene.SetLayer(100, layer.release());
-  }
-}
-
-
-void TakeScreenshot(const std::string& target,
-                    const OrthancStone::Scene2D& scene,
-                    unsigned int canvasWidth,
-                    unsigned int canvasHeight)
-{
-  using namespace OrthancStone;
-  // Take a screenshot, then save it as PNG file
-  CairoCompositor compositor(scene, canvasWidth, canvasHeight);
-  compositor.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, FONT_SIZE, Orthanc::Encoding_Latin1);
-  compositor.Refresh();
-
-  Orthanc::ImageAccessor canvas;
-  compositor.GetCanvas().GetReadOnlyAccessor(canvas);
-
-  Orthanc::Image png(Orthanc::PixelFormat_RGB24, canvas.GetWidth(), canvas.GetHeight(), false);
-  Orthanc::ImageProcessing::Convert(png, canvas);
-        
-  Orthanc::PngWriter writer;
-  writer.WriteToFile(target, png);
-}
-
-
-void HandleApplicationEvent(const SDL_Event& event,
-                            boost::shared_ptr<OrthancStone::ViewportController>& controller,
-                            boost::shared_ptr<OrthancStone::IFlexiblePointerTracker>& activeTracker)
-{
-  using namespace OrthancStone;
-
-  Scene2D& scene = controller->GetScene();
-  IViewport& viewport = controller->GetViewport();
-
-  if (event.type == SDL_MOUSEMOTION)
-  {
-    int scancodeCount = 0;
-    const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount);
-
-    if (activeTracker.get() == NULL &&
-        SDL_SCANCODE_LCTRL < scancodeCount &&
-        keyboardState[SDL_SCANCODE_LCTRL])
-    {
-      // The "left-ctrl" key is down, while no tracker is present
-
-      PointerEvent e;
-      e.AddPosition(viewport.GetPixelCenterCoordinates(event.button.x, event.button.y));
-
-      ScenePoint2D p = e.GetMainPosition().Apply(scene.GetCanvasToSceneTransform());
-
-      char buf[64];
-      sprintf(buf, "(%0.02f,%0.02f)", p.GetX(), p.GetY());
-
-      if (scene.HasLayer(LAYER_POSITION))
-      {
-        TextSceneLayer& layer =
-          dynamic_cast<TextSceneLayer&>(scene.GetLayer(LAYER_POSITION));
-        layer.SetText(buf);
-        layer.SetPosition(p.GetX(), p.GetY());
-      }
-      else
-      {
-        std::unique_ptr<TextSceneLayer> 
-          layer(new TextSceneLayer);
-        layer->SetColor(0, 255, 0);
-        layer->SetText(buf);
-        layer->SetBorder(20);
-        layer->SetAnchor(BitmapAnchor_BottomCenter);
-        layer->SetPosition(p.GetX(), p.GetY());
-        scene.SetLayer(LAYER_POSITION, layer.release());
-      }
-    }
-    else
-    {
-      scene.DeleteLayer(LAYER_POSITION);
-    }
-  }
-  else if (event.type == SDL_MOUSEBUTTONDOWN)
-  {
-    PointerEvent e;
-    e.AddPosition(viewport.GetPixelCenterCoordinates(event.button.x, event.button.y));
-
-    switch (event.button.button)
-    {
-      case SDL_BUTTON_MIDDLE:
-        activeTracker = boost::make_shared<PanSceneTracker>(controller, e);
-        break;
-
-      case SDL_BUTTON_RIGHT:
-        activeTracker = boost::make_shared<ZoomSceneTracker>
-          (controller, e, viewport.GetCanvasHeight());
-        break;
-
-      case SDL_BUTTON_LEFT:
-        activeTracker = boost::make_shared<RotateSceneTracker>(controller, e);
-        break;
-
-      default:
-        break;
-    }
-  }
-  else if (event.type == SDL_KEYDOWN &&
-           event.key.repeat == 0 /* Ignore key bounce */)
-  {
-    switch (event.key.keysym.sym)
-    {
-      case SDLK_s:
-        controller->FitContent(viewport.GetCanvasWidth(), 
-                               viewport.GetCanvasHeight());
-        break;
-              
-      case SDLK_c:
-        TakeScreenshot("screenshot.png", scene, 
-                       viewport.GetCanvasWidth(), 
-                       viewport.GetCanvasHeight());
-        break;
-              
-      default:
-        break;
-    }
-  }
-}
-
-#if OPENGL_ENABLED==1
-static void GLAPIENTRY
-OpenGLMessageCallback(GLenum source,
-                      GLenum type,
-                      GLuint id,
-                      GLenum severity,
-                      GLsizei length,
-                      const GLchar* message,
-                      const void* userParam )
-{
-  if (severity != GL_DEBUG_SEVERITY_NOTIFICATION)
-  {
-    fprintf(stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n",
-            ( type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : "" ),
-            type, severity, message );
-  }
-}
-#endif
-
-void Run(OrthancStone::MessageBroker& broker,
-         OrthancStone::SdlViewport& viewport)
-{
-  using namespace OrthancStone;
-  
-  boost::shared_ptr<ViewportController> controller(
-    new ViewportController(boost::make_shared<UndoStack>(), broker, viewport));
-  
-#if OPENGL_ENABLED==1
-  glEnable(GL_DEBUG_OUTPUT);
-  glDebugMessageCallback(OpenGLMessageCallback, 0);
-#endif
-
-  boost::shared_ptr<IFlexiblePointerTracker> tracker;
-
-  bool firstShown = true;
-  bool stop = false;
-  while (!stop)
-  {
-    viewport.Refresh();
-
-    SDL_Event event;
-    while (!stop &&
-           SDL_PollEvent(&event))
-    {
-      if (event.type == SDL_QUIT)
-      {
-        stop = true;
-        break;
-      }
-      else if (event.type == SDL_MOUSEMOTION)
-      {
-        if (tracker)
-        {
-          PointerEvent e;
-          e.AddPosition(viewport.GetPixelCenterCoordinates(
-            event.button.x, event.button.y));
-          tracker->PointerMove(e);
-        }
-      }
-      else if (event.type == SDL_MOUSEBUTTONUP)
-      {
-        if (tracker)
-        {
-          PointerEvent e;
-          e.AddPosition(viewport.GetPixelCenterCoordinates(
-            event.button.x, event.button.y));
-          tracker->PointerUp(e);
-          if(!tracker->IsAlive())
-            tracker.reset();
-        }
-      }
-      else if (event.type == SDL_WINDOWEVENT)
-      {
-        switch (event.window.event)
-        {
-          case SDL_WINDOWEVENT_SIZE_CHANGED:
-            tracker.reset();
-            viewport.UpdateSize(event.window.data1, event.window.data2);
-            break;
-
-          case SDL_WINDOWEVENT_SHOWN:
-            if (firstShown)
-            {
-              // Once the window is first shown, fit the content to its size
-              controller->FitContent(viewport.GetCanvasWidth(), viewport.GetCanvasHeight());
-              firstShown = false;
-            }
-            
-            break;
-
-          default:
-            break;
-        }
-      }
-      else if (event.type == SDL_KEYDOWN &&
-               event.key.repeat == 0 /* Ignore key bounce */)
-      {
-        switch (event.key.keysym.sym)
-        {
-          case SDLK_f:
-            viewport.GetWindow().ToggleMaximize();
-            break;
-              
-          case SDLK_q:
-            stop = true;
-            break;
-
-          default:
-            break;
-        }
-      }
-      
-      HandleApplicationEvent(event, controller, tracker);
-    }
-
-    SDL_Delay(1);
-  }
-}
-
-
-
-
-/**
- * IMPORTANT: The full arguments to "main()" are needed for SDL on
- * Windows. Otherwise, one gets the linking error "undefined reference
- * to `SDL_main'". https://wiki.libsdl.org/FAQWindows
- **/
-int main(int argc, char* argv[])
-{
-  OrthancStone::StoneInitialize();
-  Orthanc::Logging::EnableInfoLevel(true);
-
-  try
-  {
-#if OPENGL_ENABLED==1
-    OrthancStone::SdlOpenGLViewport viewport("Hello", 1024, 768);
-#else
-    OrthancStone::SdlCairoViewport viewport("Hello", 1024, 768);
-#endif
-    PrepareScene(viewport.GetScene());
-
-    viewport.GetCompositor().SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, 
-                                     FONT_SIZE, Orthanc::Encoding_Latin1);
-    
-    OrthancStone::MessageBroker broker;
-    Run(broker, viewport);
-  }
-  catch (Orthanc::OrthancException& e)
-  {
-    LOG(ERROR) << "EXCEPTION: " << e.What();
-  }
-
-  OrthancStone::StoneFinalize();
-
-  return 0;
-}
--- a/Samples/Sdl/CMakeLists.txt	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,129 +0,0 @@
-cmake_minimum_required(VERSION 2.8.3)
-
-#####################################################################
-## Configuration of the Orthanc framework
-#####################################################################
-
-# This CMake file defines the "ORTHANC_STONE_VERSION" macro, so it
-# must be the first inclusion
-include(${CMAKE_SOURCE_DIR}/../../Resources/CMake/Version.cmake)
-
-if (ORTHANC_STONE_VERSION STREQUAL "mainline")
-  set(ORTHANC_FRAMEWORK_VERSION "mainline")
-  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
-else()
-  set(ORTHANC_FRAMEWORK_VERSION "1.5.7")
-  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
-endif()
-
-set(ORTHANC_FRAMEWORK_SOURCE "${ORTHANC_FRAMEWORK_DEFAULT_SOURCE}" CACHE STRING "Source of the Orthanc source code (can be \"hg\", \"archive\", \"web\" or \"path\")")
-set(ORTHANC_FRAMEWORK_ARCHIVE "" CACHE STRING "Path to the Orthanc archive, if ORTHANC_FRAMEWORK_SOURCE is \"archive\"")
-set(ORTHANC_FRAMEWORK_ROOT "" CACHE STRING "Path to the Orthanc source directory, if ORTHANC_FRAMEWORK_SOURCE is \"path\"")
-
-
-#####################################################################
-## Configuration of the Stone framework
-#####################################################################
-
-include(${CMAKE_SOURCE_DIR}/../../Resources/CMake/OrthancStoneParameters.cmake)
-include(${ORTHANC_ROOT}/Resources/CMake/DownloadPackage.cmake)
-
-DownloadPackage(
-  "a24b8136b8f3bb93f166baf97d9328de"
-  "http://orthanc.osimis.io/ThirdPartyDownloads/ubuntu-font-family-0.83.zip"
-  "${CMAKE_BINARY_DIR}/ubuntu-font-family-0.83")
-
-set(ORTHANC_STONE_APPLICATION_RESOURCES
-  UBUNTU_FONT  ${CMAKE_BINARY_DIR}/ubuntu-font-family-0.83/Ubuntu-R.ttf
-  )
-
-SET(ENABLE_SDL_CONSOLE OFF CACHE BOOL "Enable the use of the MIT-licensed SDL_Console")
-SET(ENABLE_GOOGLE_TEST OFF)
-SET(ENABLE_LOCALE ON)
-SET(ENABLE_SDL ON)
-SET(ENABLE_WEB_CLIENT ON)
-SET(ORTHANC_SANDBOXED OFF)
-LIST(APPEND ORTHANC_BOOST_COMPONENTS program_options)
-
-include(${CMAKE_SOURCE_DIR}/../../Resources/CMake/OrthancStoneConfiguration.cmake)
-
-add_definitions(
-  -DORTHANC_ENABLE_LOGGING_PLUGIN=0
-  )
-
-
-#####################################################################
-## Build the samples
-#####################################################################
-
-add_library(OrthancStone STATIC
-  ${ORTHANC_STONE_SOURCES}
-  )
-
-#
-# BasicScene
-# 
-
-add_executable(BasicScene
-  BasicScene.cpp
-  )
-
-target_link_libraries(BasicScene OrthancStone)
-
-#
-# TrackerSample
-# 
-
-LIST(APPEND TRACKERSAMPLE_SOURCE "TrackerSample.cpp")
-LIST(APPEND TRACKERSAMPLE_SOURCE "TrackerSampleApp.cpp")
-LIST(APPEND TRACKERSAMPLE_SOURCE "TrackerSampleApp.h")
-
-if (MSVC AND MSVC_VERSION GREATER 1700)
-  LIST(APPEND TRACKERSAMPLE_SOURCE "cpp.hint")
-endif()
-
-add_executable(TrackerSample
-  ${TRACKERSAMPLE_SOURCE}
-  )
-
-target_link_libraries(TrackerSample OrthancStone)
-
-#
-# Loader
-# 
-
-add_executable(Loader
-  Loader.cpp
-  )
-
-target_link_libraries(Loader OrthancStone)
-
-#
-# FusionMprSdl
-# 
-
-add_executable(FusionMprSdl
-  FusionMprSdl.cpp
-  FusionMprSdl.h
-)
-
-target_link_libraries(FusionMprSdl OrthancStone)
-
-#
-# Multiplatform Basic Scene
-#
-
-LIST(APPEND MP_BASIC_SCENE_SOURCE "../MultiPlatform/BasicScene/BasicScene.cpp")
-LIST(APPEND MP_BASIC_SCENE_SOURCE "../MultiPlatform/BasicScene/BasicScene.h")
-LIST(APPEND MP_BASIC_SCENE_SOURCE "../MultiPlatform/BasicScene/mainSdl.cpp")
-
-if (MSVC AND MSVC_VERSION GREATER 1700)
-  LIST(APPEND MP_BASIC_SCENE_SOURCE "cpp.hint")
-endif()
-
-add_executable(MpBasicScene
-  ${MP_BASIC_SCENE_SOURCE}
-  )
-
-target_include_directories(MpBasicScene PUBLIC ${ORTHANC_STONE_ROOT})
-target_link_libraries(MpBasicScene OrthancStone)
--- a/Samples/Sdl/FusionMprSdl.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,805 +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 "FusionMprSdl.h"
-
-#include "../../Framework/OpenGL/SdlOpenGLContext.h"
-
-#include "../../Framework/StoneInitialization.h"
-
-#include "../../Framework/Scene2D/CairoCompositor.h"
-#include "../../Framework/Scene2D/ColorTextureSceneLayer.h"
-#include "../../Framework/Scene2D/OpenGLCompositor.h"
-#include "../../Framework/Scene2D/PanSceneTracker.h"
-#include "../../Framework/Scene2D/ZoomSceneTracker.h"
-#include "../../Framework/Scene2D/RotateSceneTracker.h"
-
-#include "../../Framework/Scene2DViewport/UndoStack.h"
-#include "../../Framework/Scene2DViewport/CreateLineMeasureTracker.h"
-#include "../../Framework/Scene2DViewport/CreateAngleMeasureTracker.h"
-#include "../../Framework/Scene2DViewport/IFlexiblePointerTracker.h"
-#include "../../Framework/Scene2DViewport/MeasureTool.h"
-#include "../../Framework/Scene2DViewport/PredeclaredTypes.h"
-
-#include "../../Framework/Volumes/VolumeSceneLayerSource.h"
-
-#include <Core/Images/Image.h>
-#include <Core/Images/ImageProcessing.h>
-#include <Core/Images/PngWriter.h>
-#include <Core/Logging.h>
-#include <Core/OrthancException.h>
-
-#include <boost/shared_ptr.hpp>
-#include <boost/weak_ptr.hpp>
-#include <boost/make_shared.hpp>
-
-#include <stdio.h>
-#include "../../Framework/Oracle/GetOrthancWebViewerJpegCommand.h"
-#include "../../Framework/Oracle/ThreadedOracle.h"
-#include "../../Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h"
-#include "../../Framework/Loaders/OrthancMultiframeVolumeLoader.h"
-#include "../../Framework/Loaders/DicomStructureSetLoader.h"
-#include "../../Framework/Scene2D/GrayscaleStyleConfigurator.h"
-#include "../../Framework/Scene2D/LookupTableStyleConfigurator.h"
-#include "../../Framework/Volumes/DicomVolumeImageMPRSlicer.h"
-#include "Core/SystemToolbox.h"
-
-namespace OrthancStone
-{
-  const char* FusionMprMeasureToolToString(size_t i)
-  {
-    static const char* descs[] = {
-      "FusionMprGuiTool_Rotate",
-      "FusionMprGuiTool_Pan",
-      "FusionMprGuiTool_Zoom",
-      "FusionMprGuiTool_LineMeasure",
-      "FusionMprGuiTool_CircleMeasure",
-      "FusionMprGuiTool_AngleMeasure",
-      "FusionMprGuiTool_EllipseMeasure",
-      "FusionMprGuiTool_LAST"
-    };
-    if (i >= FusionMprGuiTool_LAST)
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "Wrong tool index");
-    }
-    return descs[i];
-  }
-
-  Scene2D& FusionMprSdlApp::GetScene()
-  {
-    return controller_->GetScene();
-  }
-
-  const Scene2D& FusionMprSdlApp::GetScene() const
-  {
-    return controller_->GetScene();
-  }
-
-  void FusionMprSdlApp::SelectNextTool()
-  {
-    currentTool_ = static_cast<FusionMprGuiTool>(currentTool_ + 1);
-    if (currentTool_ == FusionMprGuiTool_LAST)
-      currentTool_ = static_cast<FusionMprGuiTool>(0);;
-    printf("Current tool is now: %s\n", FusionMprMeasureToolToString(currentTool_));
-  }
-
-  void FusionMprSdlApp::DisplayInfoText()
-  {
-    // do not try to use stuff too early!
-    ICompositor* pCompositor = &(viewport_.GetCompositor());
-    if (pCompositor == NULL)
-      return;
-
-    std::stringstream msg;
-	
-	for (std::map<std::string, std::string>::const_iterator kv = infoTextMap_.begin();
-		kv != infoTextMap_.end(); ++kv)
-    {
-      msg << kv->first << " : " << kv->second << std::endl;
-    }
-	std::string msgS = msg.str();
-
-    TextSceneLayer* layerP = NULL;
-    if (GetScene().HasLayer(FIXED_INFOTEXT_LAYER_ZINDEX))
-    {
-      TextSceneLayer& layer = dynamic_cast<TextSceneLayer&>(
-        GetScene().GetLayer(FIXED_INFOTEXT_LAYER_ZINDEX));
-      layerP = &layer;
-    }
-    else
-    {
-      std::unique_ptr<TextSceneLayer> layer(new TextSceneLayer);
-      layerP = layer.get();
-      layer->SetColor(0, 255, 0);
-      layer->SetFontIndex(1);
-      layer->SetBorder(20);
-      layer->SetAnchor(BitmapAnchor_TopLeft);
-      //layer->SetPosition(0,0);
-      GetScene().SetLayer(FIXED_INFOTEXT_LAYER_ZINDEX, layer.release());
-    }
-    // position the fixed info text in the upper right corner
-    layerP->SetText(msgS.c_str());
-    double cX = viewport_.GetCompositor().GetCanvasWidth() * (-0.5);
-    double cY = viewport_.GetCompositor().GetCanvasHeight() * (-0.5);
-    GetScene().GetCanvasToSceneTransform().Apply(cX,cY);
-    layerP->SetPosition(cX, cY);
-  }
-
-  void FusionMprSdlApp::DisplayFloatingCtrlInfoText(const PointerEvent& e)
-  {
-    ScenePoint2D p = e.GetMainPosition().Apply(GetScene().GetCanvasToSceneTransform());
-
-    char buf[128];
-    sprintf(buf, "S:(%0.02f,%0.02f) C:(%0.02f,%0.02f)", 
-      p.GetX(), p.GetY(), 
-      e.GetMainPosition().GetX(), e.GetMainPosition().GetY());
-
-    if (GetScene().HasLayer(FLOATING_INFOTEXT_LAYER_ZINDEX))
-    {
-      TextSceneLayer& layer =
-        dynamic_cast<TextSceneLayer&>(GetScene().GetLayer(FLOATING_INFOTEXT_LAYER_ZINDEX));
-      layer.SetText(buf);
-      layer.SetPosition(p.GetX(), p.GetY());
-    }
-    else
-    {
-      std::unique_ptr<TextSceneLayer> layer(new TextSceneLayer);
-      layer->SetColor(0, 255, 0);
-      layer->SetText(buf);
-      layer->SetBorder(20);
-      layer->SetAnchor(BitmapAnchor_BottomCenter);
-      layer->SetPosition(p.GetX(), p.GetY());
-      GetScene().SetLayer(FLOATING_INFOTEXT_LAYER_ZINDEX, layer.release());
-    }
-  }
-
-  void FusionMprSdlApp::HideInfoText()
-  {
-    GetScene().DeleteLayer(FLOATING_INFOTEXT_LAYER_ZINDEX);
-  }
-
-  void FusionMprSdlApp::HandleApplicationEvent(
-    const SDL_Event & event)
-  {
-    DisplayInfoText();
-
-    if (event.type == SDL_MOUSEMOTION)
-    {
-      int scancodeCount = 0;
-      const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount);
-
-      if (activeTracker_.get() == NULL &&
-        SDL_SCANCODE_LALT < scancodeCount &&
-        keyboardState[SDL_SCANCODE_LALT])
-      {
-        // The "left-ctrl" key is down, while no tracker is present
-        // Let's display the info text
-        PointerEvent e;
-        e.AddPosition(controller_->GetViewport().GetPixelCenterCoordinates(
-          event.button.x, event.button.y));
-
-        DisplayFloatingCtrlInfoText(e);
-      }
-      else
-      {
-        HideInfoText();
-        //LOG(TRACE) << "(event.type == SDL_MOUSEMOTION)";
-        if (activeTracker_.get() != NULL)
-        {
-          //LOG(TRACE) << "(activeTracker_.get() != NULL)";
-          PointerEvent e;
-          e.AddPosition(controller_->GetViewport().GetPixelCenterCoordinates(
-            event.button.x, event.button.y));
-          
-          //LOG(TRACE) << "event.button.x = " << event.button.x << "     " <<
-          //  "event.button.y = " << event.button.y;
-          LOG(TRACE) << "activeTracker_->PointerMove(e); " <<
-            e.GetMainPosition().GetX() << " " << e.GetMainPosition().GetY();
-          
-          activeTracker_->PointerMove(e);
-          if (!activeTracker_->IsAlive())
-            activeTracker_.reset();
-        }
-      }
-    }
-    else if (event.type == SDL_MOUSEBUTTONUP)
-    {
-      if (activeTracker_)
-      {
-        PointerEvent e;
-        e.AddPosition(controller_->GetViewport().GetPixelCenterCoordinates(event.button.x, event.button.y));
-        activeTracker_->PointerUp(e);
-        if (!activeTracker_->IsAlive())
-          activeTracker_.reset();
-      }
-    }
-    else if (event.type == SDL_MOUSEBUTTONDOWN)
-    {
-      PointerEvent e;
-      e.AddPosition(controller_->GetViewport().GetPixelCenterCoordinates(
-        event.button.x, event.button.y));
-      if (activeTracker_)
-      {
-        activeTracker_->PointerDown(e);
-        if (!activeTracker_->IsAlive())
-          activeTracker_.reset();
-      }
-      else
-      {
-        // we ATTEMPT to create a tracker if need be
-        activeTracker_ = CreateSuitableTracker(event, e);
-      }
-    }
-    else if (event.type == SDL_KEYDOWN &&
-      event.key.repeat == 0 /* Ignore key bounce */)
-    {
-      switch (event.key.keysym.sym)
-      {
-      case SDLK_ESCAPE:
-        if (activeTracker_)
-        {
-          activeTracker_->Cancel();
-          if (!activeTracker_->IsAlive())
-            activeTracker_.reset();
-        }
-        break;
-
-      case SDLK_t:
-        if (!activeTracker_)
-          SelectNextTool();
-        else
-        {
-          LOG(WARNING) << "You cannot change the active tool when an interaction"
-            " is taking place";
-        }
-        break;
-      case SDLK_s:
-        controller_->FitContent(viewport_.GetCompositor().GetCanvasWidth(),
-          viewport_.GetCompositor().GetCanvasHeight());
-        break;
-
-      case SDLK_z:
-        LOG(TRACE) << "SDLK_z has been pressed. event.key.keysym.mod == " << event.key.keysym.mod;
-        if (event.key.keysym.mod & KMOD_CTRL)
-        {
-          if (controller_->CanUndo())
-          {
-            LOG(TRACE) << "Undoing...";
-            controller_->Undo();
-          }
-          else
-          {
-            LOG(WARNING) << "Nothing to undo!!!";
-          }
-        }
-        break;
-
-      case SDLK_y:
-        LOG(TRACE) << "SDLK_y has been pressed. event.key.keysym.mod == " << event.key.keysym.mod;
-        if (event.key.keysym.mod & KMOD_CTRL)
-        {
-          if (controller_->CanRedo())
-          {
-            LOG(TRACE) << "Redoing...";
-            controller_->Redo();
-          }
-          else
-          {
-            LOG(WARNING) << "Nothing to redo!!!";
-          }
-        }
-        break;
-
-      case SDLK_c:
-        TakeScreenshot(
-          "screenshot.png",
-          viewport_.GetCompositor().GetCanvasWidth(),
-          viewport_.GetCompositor().GetCanvasHeight());
-        break;
-
-      default:
-        break;
-      }
-    }
-  }
-
-
-  void FusionMprSdlApp::OnSceneTransformChanged(
-    const ViewportController::SceneTransformChanged& message)
-  {
-    DisplayInfoText();
-  }
-
-  boost::shared_ptr<IFlexiblePointerTracker> FusionMprSdlApp::CreateSuitableTracker(
-    const SDL_Event & event,
-    const PointerEvent & e)
-  {
-    using namespace Orthanc;
-
-    switch (event.button.button)
-    {
-    case SDL_BUTTON_MIDDLE:
-      return boost::shared_ptr<IFlexiblePointerTracker>(new PanSceneTracker
-        (controller_, e));
-
-    case SDL_BUTTON_RIGHT:
-      return boost::shared_ptr<IFlexiblePointerTracker>(new ZoomSceneTracker
-        (controller_, e, viewport_.GetCompositor().GetCanvasHeight()));
-
-    case SDL_BUTTON_LEFT:
-    {
-      //LOG(TRACE) << "CreateSuitableTracker: case SDL_BUTTON_LEFT:";
-      // TODO: we need to iterate on the set of measuring tool and perform
-      // a hit test to check if a tracker needs to be created for edition.
-      // Otherwise, depending upon the active tool, we might want to create
-      // a "measuring tool creation" tracker
-
-      // TODO: if there are conflicts, we should prefer a tracker that 
-      // pertains to the type of measuring tool currently selected (TBD?)
-      boost::shared_ptr<IFlexiblePointerTracker> hitTestTracker = TrackerHitTest(e);
-
-      if (hitTestTracker != NULL)
-      {
-        //LOG(TRACE) << "hitTestTracker != NULL";
-        return hitTestTracker;
-      }
-      else
-      {
-        switch (currentTool_)
-        {
-        case FusionMprGuiTool_Rotate:
-          //LOG(TRACE) << "Creating RotateSceneTracker";
-          return boost::shared_ptr<IFlexiblePointerTracker>(new RotateSceneTracker(
-            controller_, e));
-        case FusionMprGuiTool_Pan:
-          return boost::shared_ptr<IFlexiblePointerTracker>(new PanSceneTracker(
-            controller_, e));
-        case FusionMprGuiTool_Zoom:
-          return boost::shared_ptr<IFlexiblePointerTracker>(new ZoomSceneTracker(
-            controller_, e, viewport_.GetCompositor().GetCanvasHeight()));
-        //case GuiTool_AngleMeasure:
-        //  return new AngleMeasureTracker(GetScene(), e);
-        //case GuiTool_CircleMeasure:
-        //  return new CircleMeasureTracker(GetScene(), e);
-        //case GuiTool_EllipseMeasure:
-        //  return new EllipseMeasureTracker(GetScene(), e);
-        case FusionMprGuiTool_LineMeasure:
-          return boost::shared_ptr<IFlexiblePointerTracker>(new CreateLineMeasureTracker(
-            IObserver::GetBroker(), controller_, e));
-        case FusionMprGuiTool_AngleMeasure:
-          return boost::shared_ptr<IFlexiblePointerTracker>(new CreateAngleMeasureTracker(
-            IObserver::GetBroker(), controller_, e));
-        case FusionMprGuiTool_CircleMeasure:
-          LOG(ERROR) << "Not implemented yet!";
-          return boost::shared_ptr<IFlexiblePointerTracker>();
-        case FusionMprGuiTool_EllipseMeasure:
-          LOG(ERROR) << "Not implemented yet!";
-          return boost::shared_ptr<IFlexiblePointerTracker>();
-        default:
-          throw OrthancException(ErrorCode_InternalError, "Wrong tool!");
-        }
-      }
-    }
-    default:
-      return boost::shared_ptr<IFlexiblePointerTracker>();
-    }
-  }
-
-
-  FusionMprSdlApp::FusionMprSdlApp(MessageBroker& broker)
-    : IObserver(broker)
-    , broker_(broker)
-    , oracleObservable_(broker)
-    , oracle_(*this)
-    , currentTool_(FusionMprGuiTool_Rotate)
-    , undoStack_(new UndoStack)
-    , viewport_("Hello", 1024, 1024, false) // False means we do NOT let Windows treat this as a legacy application that needs to be scaled
-  {
-    //oracleObservable.RegisterObserverCallback
-    //(new Callable
-    //  <FusionMprSdlApp, SleepOracleCommand::TimeoutMessage>(*this, &FusionMprSdlApp::Handle));
-
-    //oracleObservable.RegisterObserverCallback
-    //(new Callable
-    //  <Toto, GetOrthancImageCommand::SuccessMessage>(*this, &FusionMprSdlApp::Handle));
-
-    //oracleObservable.RegisterObserverCallback
-    //(new Callable
-    //  <FusionMprSdlApp, GetOrthancWebViewerJpegCommand::SuccessMessage>(*this, &ToFusionMprSdlAppto::Handle));
-
-    oracleObservable_.RegisterObserverCallback
-    (new Callable
-      <FusionMprSdlApp, OracleCommandExceptionMessage>(*this, &FusionMprSdlApp::Handle));
-    
-    controller_ = boost::shared_ptr<ViewportController>(
-      new ViewportController(undoStack_, broker_, viewport_));
-
-    controller_->RegisterObserverCallback(
-      new Callable<FusionMprSdlApp, ViewportController::SceneTransformChanged>
-      (*this, &FusionMprSdlApp::OnSceneTransformChanged));
-
-    TEXTURE_2x2_1_ZINDEX = 1;
-    TEXTURE_1x1_ZINDEX = 2;
-    TEXTURE_2x2_2_ZINDEX = 3;
-    LINESET_1_ZINDEX = 4;
-    LINESET_2_ZINDEX = 5;
-    FLOATING_INFOTEXT_LAYER_ZINDEX = 6;
-    FIXED_INFOTEXT_LAYER_ZINDEX = 7;
-  }
-
-  void FusionMprSdlApp::PrepareScene()
-  {
-    // Texture of 2x2 size
-    {
-      Orthanc::Image i(Orthanc::PixelFormat_RGB24, 2, 2, false);
-
-      uint8_t* p = reinterpret_cast<uint8_t*>(i.GetRow(0));
-      p[0] = 255;
-      p[1] = 0;
-      p[2] = 0;
-
-      p[3] = 0;
-      p[4] = 255;
-      p[5] = 0;
-
-      p = reinterpret_cast<uint8_t*>(i.GetRow(1));
-      p[0] = 0;
-      p[1] = 0;
-      p[2] = 255;
-
-      p[3] = 255;
-      p[4] = 0;
-      p[5] = 0;
-
-      GetScene().SetLayer(TEXTURE_2x2_1_ZINDEX, new ColorTextureSceneLayer(i));
-    }
-  }
-
-  void FusionMprSdlApp::DisableTracker()
-  {
-    if (activeTracker_)
-    {
-      activeTracker_->Cancel();
-      activeTracker_.reset();
-    }
-  }
-
-  void FusionMprSdlApp::TakeScreenshot(const std::string& target,
-    unsigned int canvasWidth,
-    unsigned int canvasHeight)
-  {
-    CairoCompositor compositor(GetScene(), canvasWidth, canvasHeight);
-    compositor.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, FONT_SIZE_0, Orthanc::Encoding_Latin1);
-    compositor.Refresh();
-
-    Orthanc::ImageAccessor canvas;
-    compositor.GetCanvas().GetReadOnlyAccessor(canvas);
-
-    Orthanc::Image png(Orthanc::PixelFormat_RGB24, canvas.GetWidth(), canvas.GetHeight(), false);
-    Orthanc::ImageProcessing::Convert(png, canvas);
-
-    Orthanc::PngWriter writer;
-    writer.WriteToFile(target, png);
-  }
-
-
-  boost::shared_ptr<IFlexiblePointerTracker> FusionMprSdlApp::TrackerHitTest(const PointerEvent & e)
-  {
-    // std::vector<boost::shared_ptr<MeasureTool>> measureTools_;
-    return boost::shared_ptr<IFlexiblePointerTracker>();
-  }
-
-  static void GLAPIENTRY
-    OpenGLMessageCallback(GLenum source,
-      GLenum type,
-      GLuint id,
-      GLenum severity,
-      GLsizei length,
-      const GLchar* message,
-      const void* userParam)
-  {
-    if (severity != GL_DEBUG_SEVERITY_NOTIFICATION)
-    {
-      fprintf(stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n",
-        (type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : ""),
-        type, severity, message);
-    }
-  }
-
-  static bool g_stopApplication = false;
-  
-
-  void FusionMprSdlApp::Handle(const DicomVolumeImage::GeometryReadyMessage& message)
-  {
-    printf("Geometry ready\n");
-
-    //plane_ = message.GetOrigin().GetGeometry().GetSagittalGeometry();
-    //plane_ = message.GetOrigin().GetGeometry().GetAxialGeometry();
-    plane_ = message.GetOrigin().GetGeometry().GetCoronalGeometry();
-    plane_.SetOrigin(message.GetOrigin().GetGeometry().GetCoordinates(0.5f, 0.5f, 0.5f));
-
-    //Refresh();
-  }
-
-
-  void FusionMprSdlApp::Handle(const OracleCommandExceptionMessage& message)
-  {
-    printf("EXCEPTION: [%s] on command type %d\n", message.GetException().What(), message.GetCommand().GetType());
-
-    switch (message.GetCommand().GetType())
-    {
-    case IOracleCommand::Type_GetOrthancWebViewerJpeg:
-      printf("URI: [%s]\n", dynamic_cast<const GetOrthancWebViewerJpegCommand&>
-        (message.GetCommand()).GetUri().c_str());
-      break;
-
-    default:
-      break;
-    }
-  }
-
-  void FusionMprSdlApp::SetVolume1(int depth,
-    const boost::shared_ptr<OrthancStone::IVolumeSlicer>& volume,
-    OrthancStone::ILayerStyleConfigurator* style)
-  {
-    source1_.reset(new OrthancStone::VolumeSceneLayerSource(controller_->GetScene(), depth, volume));
-
-    if (style != NULL)
-    {
-      source1_->SetConfigurator(style);
-    }
-  }
-
-  void FusionMprSdlApp::SetVolume2(int depth,
-    const boost::shared_ptr<OrthancStone::IVolumeSlicer>& volume,
-    OrthancStone::ILayerStyleConfigurator* style)
-  {
-    source2_.reset(new OrthancStone::VolumeSceneLayerSource(controller_->GetScene(), depth, volume));
-
-    if (style != NULL)
-    {
-      source2_->SetConfigurator(style);
-    }
-  }
-
-  void FusionMprSdlApp::SetStructureSet(int depth,
-    const boost::shared_ptr<OrthancStone::DicomStructureSetLoader>& volume)
-  {
-    source3_.reset(new OrthancStone::VolumeSceneLayerSource(controller_->GetScene(), depth, volume));
-  }
-  
-  void FusionMprSdlApp::Run()
-  {
-    // False means we do NOT let Windows treat this as a legacy application
-    // that needs to be scaled
-    controller_->FitContent(viewport_.GetCanvasWidth(), viewport_.GetCanvasHeight());
-
-    glEnable(GL_DEBUG_OUTPUT);
-    glDebugMessageCallback(OpenGLMessageCallback, 0);
-
-    viewport_.GetCompositor().SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT,
-      FONT_SIZE_0, Orthanc::Encoding_Latin1);
-    viewport_.GetCompositor().SetFont(1, Orthanc::EmbeddedResources::UBUNTU_FONT,
-      FONT_SIZE_1, Orthanc::Encoding_Latin1);
-
-
-    //////// from loader
-    {
-      Orthanc::WebServiceParameters p;
-      //p.SetUrl("http://localhost:8043/");
-      p.SetCredentials("orthanc", "orthanc");
-      oracle_.SetOrthancParameters(p);
-    }
-
-    //////// from Run
-
-    boost::shared_ptr<DicomVolumeImage>  ct(new DicomVolumeImage);
-    boost::shared_ptr<DicomVolumeImage>  dose(new DicomVolumeImage);
-
-
-    boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> ctLoader;
-    boost::shared_ptr<OrthancMultiframeVolumeLoader> doseLoader;
-    boost::shared_ptr<DicomStructureSetLoader>  rtstructLoader;
-
-    {
-      ctLoader.reset(new OrthancSeriesVolumeProgressiveLoader(ct, oracle_, oracleObservable_));
-      doseLoader.reset(new OrthancMultiframeVolumeLoader(dose, oracle_, oracleObservable_));
-      rtstructLoader.reset(new DicomStructureSetLoader(oracle_, oracleObservable_));
-    }
-
-    //toto->SetReferenceLoader(*ctLoader);
-    //doseLoader->RegisterObserverCallback
-    //(new Callable
-    //  <FusionMprSdlApp, DicomVolumeImage::GeometryReadyMessage>(*this, &FusionMprSdlApp::Handle));
-    ctLoader->RegisterObserverCallback
-    (new Callable
-      <FusionMprSdlApp, DicomVolumeImage::GeometryReadyMessage>(*this, &FusionMprSdlApp::Handle));
-
-    this->SetVolume1(0, ctLoader, new GrayscaleStyleConfigurator);
-
-    {
-      std::unique_ptr<LookupTableStyleConfigurator> config(new LookupTableStyleConfigurator);
-      config->SetLookupTable(Orthanc::EmbeddedResources::COLORMAP_HOT);
-
-      boost::shared_ptr<DicomVolumeImageMPRSlicer> tmp(new DicomVolumeImageMPRSlicer(dose));
-      this->SetVolume2(1, tmp, config.release());
-    }
-
-    this->SetStructureSet(2, rtstructLoader);
-
-#if 1
-    /*
-    BGO data
-    http://localhost:8042/twiga-orthanc-viewer-demo/twiga-orthanc-viewer-demo.html?ct-series=a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa
-    &
-    dose-instance=830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb
-    &
-    struct-instance=54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9
-    */
-    ctLoader->LoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");  // CT
-    doseLoader->LoadInstance("830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb");  // RT-DOSE
-    rtstructLoader->LoadInstance("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9");  // RT-STRUCT
-#else
-    //ctLoader->LoadSeries("cb3ea4d1-d08f3856-ad7b6314-74d88d77-60b05618");  // CT
-    //doseLoader->LoadInstance("41029085-71718346-811efac4-420e2c15-d39f99b6");  // RT-DOSE
-    //rtstructLoader->LoadInstance("83d9c0c3-913a7fee-610097d7-cbf0522d-fd75bee6");  // RT-STRUCT
-
-    // 2017-05-16
-    ctLoader->LoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");  // CT
-    doseLoader->LoadInstance("eac822ef-a395f94e-e8121fe0-8411fef8-1f7bffad");  // RT-DOSE
-    rtstructLoader->LoadInstance("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9");  // RT-STRUCT
-#endif
-
-    oracle_.Start();
-
-//// END from loader
-
-    while (!g_stopApplication)
-    {
-      viewport_.GetCompositor().Refresh();
-
-//////// from loader
-      if (source1_.get() != NULL)
-      {
-        source1_->Update(plane_);
-      }
-
-      if (source2_.get() != NULL)
-      {
-        source2_->Update(plane_);
-      }
-
-      if (source3_.get() != NULL)
-      {
-        source3_->Update(plane_);
-      }
-//// END from loader
-
-      SDL_Event event;
-      while (!g_stopApplication && SDL_PollEvent(&event))
-      {
-        if (event.type == SDL_QUIT)
-        {
-          g_stopApplication = true;
-          break;
-        }
-        else if (event.type == SDL_WINDOWEVENT &&
-          event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
-        {
-          DisableTracker(); // was: tracker.reset(NULL);
-        }
-        else if (event.type == SDL_KEYDOWN &&
-          event.key.repeat == 0 /* Ignore key bounce */)
-        {
-          switch (event.key.keysym.sym)
-          {
-          case SDLK_f:
-            viewport_.GetWindow().ToggleMaximize();
-            break;
-
-          case SDLK_s:
-            controller_->FitContent(viewport_.GetCanvasWidth(), viewport_.GetCanvasHeight());
-            break;
-
-          case SDLK_q:
-            g_stopApplication = true;
-            break;
-          default:
-            break;
-          }
-        }
-        HandleApplicationEvent(event);
-      }
-      SDL_Delay(1);
-    }
-
-    //// from loader
-
-    //Orthanc::SystemToolbox::ServerBarrier();
-
-    /**
-     * WARNING => The oracle must be stopped BEFORE the objects using
-     * it are destroyed!!! This forces to wait for the completion of
-     * the running callback methods. Otherwise, the callbacks methods
-     * might still be running while their parent object is destroyed,
-     * resulting in crashes. This is very visible if adding a sleep(),
-     * as in (*).
-     **/
-
-    oracle_.Stop();
-    //// END from loader
-  }
-
-  void FusionMprSdlApp::SetInfoDisplayMessage(
-    std::string key, std::string value)
-  {
-    if (value == "")
-      infoTextMap_.erase(key);
-    else
-      infoTextMap_[key] = value;
-    DisplayInfoText();
-  }
-
-}
-
-
-boost::weak_ptr<OrthancStone::FusionMprSdlApp> g_app;
-
-void FusionMprSdl_SetInfoDisplayMessage(std::string key, std::string value)
-{
-  boost::shared_ptr<OrthancStone::FusionMprSdlApp> app = g_app.lock();
-  if (app)
-  {
-    app->SetInfoDisplayMessage(key, value);
-  }
-}
-
-/**
- * IMPORTANT: The full arguments to "main()" are needed for SDL on
- * Windows. Otherwise, one gets the linking error "undefined reference
- * to `SDL_main'". https://wiki.libsdl.org/FAQWindows
- **/
-int main(int argc, char* argv[])
-{
-  using namespace OrthancStone;
-
-  StoneInitialize();
-  Orthanc::Logging::EnableInfoLevel(true);
-//  Orthanc::Logging::EnableTraceLevel(true);
-
-  try
-  {
-    OrthancStone::MessageBroker broker;
-    boost::shared_ptr<FusionMprSdlApp> app(new FusionMprSdlApp(broker));
-    g_app = app;
-    app->PrepareScene();
-    app->Run();
-  }
-  catch (Orthanc::OrthancException& e)
-  {
-    LOG(ERROR) << "EXCEPTION: " << e.What();
-  }
-
-  StoneFinalize();
-
-  return 0;
-}
-
-
--- a/Samples/Sdl/FusionMprSdl.h	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,206 +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 "../../Framework/Viewport/SdlViewport.h"
-
-#include "../../Framework/Messages/IObserver.h"
-#include "../../Framework/Messages/IMessageEmitter.h"
-#include "../../Framework/Oracle/OracleCommandExceptionMessage.h"
-#include "../../Framework/Scene2DViewport/ViewportController.h"
-#include "../../Framework/Volumes/DicomVolumeImage.h"
-#include "../../Framework/Oracle/ThreadedOracle.h"
-
-#include <boost/enable_shared_from_this.hpp>
-#include <boost/thread.hpp>
-#include <boost/noncopyable.hpp>
-
-#include <SDL.h>
-
-namespace OrthancStone
-{
-  class OpenGLCompositor;
-  class IVolumeSlicer;
-  class ILayerStyleConfigurator;
-  class DicomStructureSetLoader;
-  class IOracle;
-  class ThreadedOracle;
-  class VolumeSceneLayerSource;
-  class NativeFusionMprApplicationContext;
-  class SdlOpenGLViewport;
-   
-  enum FusionMprGuiTool
-  {
-    FusionMprGuiTool_Rotate = 0,
-    FusionMprGuiTool_Pan,
-    FusionMprGuiTool_Zoom,
-    FusionMprGuiTool_LineMeasure,
-    FusionMprGuiTool_CircleMeasure,
-    FusionMprGuiTool_AngleMeasure,
-    FusionMprGuiTool_EllipseMeasure,
-    FusionMprGuiTool_LAST
-  };
-
-  const char* MeasureToolToString(size_t i);
-
-  static const unsigned int FONT_SIZE_0 = 32;
-  static const unsigned int FONT_SIZE_1 = 24;
-
-  class Scene2D;
-  class UndoStack;
-
-  /**
-  This application subclasses IMessageEmitter to use a mutex before forwarding Oracle messages (that
-  can be sent from multiple threads)
-  */
-  class FusionMprSdlApp : public IObserver
-    , public boost::enable_shared_from_this<FusionMprSdlApp>
-    , public IMessageEmitter
-  {
-  public:
-    // 12 because.
-    FusionMprSdlApp(MessageBroker& broker);
-
-    void PrepareScene();
-    void Run();
-    void SetInfoDisplayMessage(std::string key, std::string value);
-    void DisableTracker();
-
-    Scene2D&       GetScene();
-    const Scene2D& GetScene() const;
-
-    void HandleApplicationEvent(const SDL_Event& event);
-
-    /**
-    This method is called when the scene transform changes. It allows to
-    recompute the visual elements whose content depend upon the scene transform
-    */
-    void OnSceneTransformChanged(
-      const ViewportController::SceneTransformChanged& message);
-
-
-    virtual void EmitMessage(const IObserver& observer,
-      const IMessage& message) ORTHANC_OVERRIDE
-    {
-      try
-      {
-        boost::unique_lock<boost::shared_mutex>  lock(mutex_);
-        oracleObservable_.EmitMessage(observer, message);
-      }
-      catch (Orthanc::OrthancException& e)
-      {
-        LOG(ERROR) << "Exception while emitting a message: " << e.What();
-        throw;
-      }
-    }
-    
-  private:
-#if 1
-    // if threaded (not wasm)
-    MessageBroker& broker_;
-    IObservable oracleObservable_;
-    ThreadedOracle oracle_;
-    boost::shared_mutex mutex_; // to serialize messages from the ThreadedOracle
-#endif
-
-    void SelectNextTool();
-
-    /**
-    This returns a random point in the canvas part of the scene, but in
-    scene coordinates
-    */
-    ScenePoint2D GetRandomPointInScene() const;
-
-    boost::shared_ptr<IFlexiblePointerTracker> TrackerHitTest(const PointerEvent& e);
-
-    boost::shared_ptr<IFlexiblePointerTracker> CreateSuitableTracker(
-      const SDL_Event& event,
-      const PointerEvent& e);
-
-    void TakeScreenshot(
-      const std::string& target,
-      unsigned int canvasWidth,
-      unsigned int canvasHeight);
-
-    /**
-      This adds the command at the top of the undo stack
-    */
-    void Commit(boost::shared_ptr<TrackerCommand> cmd);
-    void Undo();
-    void Redo();
-
-
-    // TODO private
-    void Handle(const DicomVolumeImage::GeometryReadyMessage& message);
-    void Handle(const OracleCommandExceptionMessage& message);
-
-    void SetVolume1(
-      int depth,
-      const boost::shared_ptr<IVolumeSlicer>& volume,
-      ILayerStyleConfigurator* style);
-    
-    void SetVolume2(
-      int depth,
-      const boost::shared_ptr<IVolumeSlicer>& volume,
-      ILayerStyleConfigurator* style);
-
-    void SetStructureSet(
-      int depth, 
-      const boost::shared_ptr<DicomStructureSetLoader>& volume);
-
-
-
-  private:
-    void DisplayFloatingCtrlInfoText(const PointerEvent& e);
-    void DisplayInfoText();
-    void HideInfoText();
-
-  private:
-    CoordinateSystem3D  plane_;
-
-    boost::shared_ptr<VolumeSceneLayerSource>  source1_, source2_, source3_;
-
-    /**
-    WARNING: the measuring tools do store a reference to the scene, and it
-    paramount that the scene gets destroyed AFTER the measurement tools.
-    */
-    boost::shared_ptr<ViewportController> controller_;
-
-    std::map<std::string, std::string> infoTextMap_;
-    boost::shared_ptr<IFlexiblePointerTracker> activeTracker_;
-
-    //static const int LAYER_POSITION = 150;
-
-    int TEXTURE_2x2_1_ZINDEX;
-    int TEXTURE_1x1_ZINDEX;
-    int TEXTURE_2x2_2_ZINDEX;
-    int LINESET_1_ZINDEX;
-    int LINESET_2_ZINDEX;
-    int FLOATING_INFOTEXT_LAYER_ZINDEX;
-    int FIXED_INFOTEXT_LAYER_ZINDEX;
-
-    FusionMprGuiTool currentTool_;
-    boost::shared_ptr<UndoStack> undoStack_;
-    SdlOpenGLViewport viewport_;
-  };
-
-}
-
-
- 
\ No newline at end of file
--- a/Samples/Sdl/Loader.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,518 +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 "../../Framework/Loaders/DicomStructureSetLoader.h"
-#include "../../Framework/Loaders/OrthancMultiframeVolumeLoader.h"
-#include "../../Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h"
-#include "../../Framework/Oracle/SleepOracleCommand.h"
-#include "../../Framework/Oracle/ThreadedOracle.h"
-#include "../../Framework/Scene2D/CairoCompositor.h"
-#include "../../Framework/Scene2D/GrayscaleStyleConfigurator.h"
-#include "../../Framework/Scene2D/LookupTableStyleConfigurator.h"
-#include "../../Framework/StoneInitialization.h"
-#include "../../Framework/Volumes/VolumeSceneLayerSource.h"
-#include "../../Framework/Volumes/DicomVolumeImageMPRSlicer.h"
-#include "../../Framework/Volumes/DicomVolumeImageReslicer.h"
-
-// From Orthanc framework
-#include <Core/Images/ImageProcessing.h>
-#include <Core/Images/PngWriter.h>
-#include <Core/Logging.h>
-#include <Core/OrthancException.h>
-#include <Core/SystemToolbox.h>
-
-
-namespace OrthancStone
-{
-  class NativeApplicationContext : public IMessageEmitter
-  {
-  private:
-    boost::shared_mutex  mutex_;
-    MessageBroker        broker_;
-    IObservable          oracleObservable_;
-
-  public:
-    NativeApplicationContext() :
-      oracleObservable_(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:
-      NativeApplicationContext&                that_;
-      boost::shared_lock<boost::shared_mutex>  lock_;
-
-    public:
-      ReaderLock(NativeApplicationContext& that) : 
-        that_(that),
-        lock_(that.mutex_)
-      {
-      }
-    };
-
-
-    class WriterLock : public boost::noncopyable
-    {
-    private:
-      NativeApplicationContext&                that_;
-      boost::unique_lock<boost::shared_mutex>  lock_;
-
-    public:
-      WriterLock(NativeApplicationContext& that) : 
-        that_(that),
-        lock_(that.mutex_)
-      {
-      }
-
-      MessageBroker& GetBroker() 
-      {
-        return that_.broker_;
-      }
-
-      IObservable& GetOracleObservable()
-      {
-        return that_.oracleObservable_;
-      }
-    };
-  };
-}
-
-
-
-class Toto : public OrthancStone::IObserver
-{
-private:
-  OrthancStone::CoordinateSystem3D  plane_;
-  OrthancStone::IOracle&            oracle_;
-  OrthancStone::Scene2D             scene_;
-  std::unique_ptr<OrthancStone::VolumeSceneLayerSource>  source1_, source2_, source3_;
-
-
-  void Refresh()
-  {
-    if (source1_.get() != NULL)
-    {
-      source1_->Update(plane_);
-    }
-      
-    if (source2_.get() != NULL)
-    {
-      source2_->Update(plane_);
-    }
-
-    if (source3_.get() != NULL)
-    {
-      source3_->Update(plane_);
-    }
-
-    scene_.FitContent(1024, 768);
-      
-    {
-      OrthancStone::CairoCompositor compositor(scene_, 1024, 768);
-      compositor.Refresh();
-        
-      Orthanc::ImageAccessor accessor;
-      compositor.GetCanvas().GetReadOnlyAccessor(accessor);
-
-      Orthanc::Image tmp(Orthanc::PixelFormat_RGB24, accessor.GetWidth(), accessor.GetHeight(), false);
-      Orthanc::ImageProcessing::Convert(tmp, accessor);
-        
-      static unsigned int count = 0;
-      char buf[64];
-      sprintf(buf, "scene-%06d.png", count++);
-        
-      Orthanc::PngWriter writer;
-      writer.WriteToFile(buf, tmp);
-    }
-  }
-
-  
-  void Handle(const OrthancStone::DicomVolumeImage::GeometryReadyMessage& message)
-  {
-    printf("Geometry ready\n");
-    
-    plane_ = message.GetOrigin().GetGeometry().GetSagittalGeometry();
-    //plane_ = message.GetOrigin().GetGeometry().GetAxialGeometry();
-    //plane_ = message.GetOrigin().GetGeometry().GetCoronalGeometry();
-    plane_.SetOrigin(message.GetOrigin().GetGeometry().GetCoordinates(0.5f, 0.5f, 0.5f));
-
-    Refresh();
-  }
-  
-  
-  void Handle(const OrthancStone::SleepOracleCommand::TimeoutMessage& message)
-  {
-    if (message.GetOrigin().HasPayload())
-    {
-      printf("TIMEOUT! %d\n", dynamic_cast<const Orthanc::SingleValueObject<unsigned int>& >(message.GetOrigin().GetPayload()).GetValue());
-    }
-    else
-    {
-      printf("TIMEOUT\n");
-
-      Refresh();
-
-      /**
-       * The sleep() leads to a crash if the oracle is still running,
-       * while this object is destroyed. Always stop the oracle before
-       * destroying active objects.  (*)
-       **/
-      // boost::this_thread::sleep(boost::posix_time::seconds(2));
-
-      oracle_.Schedule(*this, new OrthancStone::SleepOracleCommand(message.GetOrigin().GetDelay()));
-    }
-  }
-
-  void Handle(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message)
-  {
-    Json::Value v;
-    message.ParseJsonBody(v);
-
-    printf("ICI [%s]\n", v.toStyledString().c_str());
-  }
-
-  void Handle(const OrthancStone::GetOrthancImageCommand::SuccessMessage& message)
-  {
-    printf("IMAGE %dx%d\n", message.GetImage().GetWidth(), message.GetImage().GetHeight());
-  }
-
-  void Handle(const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message)
-  {
-    printf("WebViewer %dx%d\n", message.GetImage().GetWidth(), message.GetImage().GetHeight());
-  }
-
-  void Handle(const OrthancStone::OracleCommandExceptionMessage& message)
-  {
-    printf("EXCEPTION: [%s] on command type %d\n", message.GetException().What(), message.GetCommand().GetType());
-
-    switch (message.GetCommand().GetType())
-    {
-      case OrthancStone::IOracleCommand::Type_GetOrthancWebViewerJpeg:
-        printf("URI: [%s]\n", dynamic_cast<const OrthancStone::GetOrthancWebViewerJpegCommand&>
-               (message.GetCommand()).GetUri().c_str());
-        break;
-      
-      default:
-        break;
-    }
-  }
-
-public:
-  Toto(OrthancStone::IOracle& oracle,
-       OrthancStone::IObservable& oracleObservable) :
-    IObserver(oracleObservable.GetBroker()),
-    oracle_(oracle)
-  {
-    oracleObservable.RegisterObserverCallback
-      (new OrthancStone::Callable
-       <Toto, OrthancStone::SleepOracleCommand::TimeoutMessage>(*this, &Toto::Handle));
-
-    oracleObservable.RegisterObserverCallback
-      (new OrthancStone::Callable
-       <Toto, OrthancStone::OrthancRestApiCommand::SuccessMessage>(*this, &Toto::Handle));
-
-    oracleObservable.RegisterObserverCallback
-      (new OrthancStone::Callable
-       <Toto, OrthancStone::GetOrthancImageCommand::SuccessMessage>(*this, &Toto::Handle));
-
-    oracleObservable.RegisterObserverCallback
-      (new OrthancStone::Callable
-       <Toto, OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage>(*this, &Toto::Handle));
-
-    oracleObservable.RegisterObserverCallback
-      (new OrthancStone::Callable
-       <Toto, OrthancStone::OracleCommandExceptionMessage>(*this, &Toto::Handle));
-  }
-
-  void SetReferenceLoader(OrthancStone::IObservable& loader)
-  {
-    loader.RegisterObserverCallback
-      (new OrthancStone::Callable
-       <Toto, OrthancStone::DicomVolumeImage::GeometryReadyMessage>(*this, &Toto::Handle));
-  }
-
-  void SetVolume1(int depth,
-                  const boost::shared_ptr<OrthancStone::IVolumeSlicer>& volume,
-                  OrthancStone::ILayerStyleConfigurator* style)
-  {
-    source1_.reset(new OrthancStone::VolumeSceneLayerSource(scene_, depth, volume));
-
-    if (style != NULL)
-    {
-      source1_->SetConfigurator(style);
-    }
-  }
-
-  void SetVolume2(int depth,
-                  const boost::shared_ptr<OrthancStone::IVolumeSlicer>& volume,
-                  OrthancStone::ILayerStyleConfigurator* style)
-  {
-    source2_.reset(new OrthancStone::VolumeSceneLayerSource(scene_, depth, volume));
-
-    if (style != NULL)
-    {
-      source2_->SetConfigurator(style);
-    }
-  }
-
-  void SetStructureSet(int depth,
-                       const boost::shared_ptr<OrthancStone::DicomStructureSetLoader>& volume)
-  {
-    source3_.reset(new OrthancStone::VolumeSceneLayerSource(scene_, depth, volume));
-  }
-                       
-};
-
-
-void Run(OrthancStone::NativeApplicationContext& context,
-         OrthancStone::ThreadedOracle& oracle)
-{
-  // the oracle has been supplied with the context (as an IEmitter) upon
-  // creation
-  boost::shared_ptr<OrthancStone::DicomVolumeImage>  ct(new OrthancStone::DicomVolumeImage);
-  boost::shared_ptr<OrthancStone::DicomVolumeImage>  dose(new OrthancStone::DicomVolumeImage);
-
-  
-  boost::shared_ptr<Toto> toto;
-  boost::shared_ptr<OrthancStone::OrthancSeriesVolumeProgressiveLoader> ctLoader;
-  boost::shared_ptr<OrthancStone::OrthancMultiframeVolumeLoader> doseLoader;
-  boost::shared_ptr<OrthancStone::DicomStructureSetLoader>  rtstructLoader;
-
-  {
-    OrthancStone::NativeApplicationContext::WriterLock lock(context);
-    toto.reset(new Toto(oracle, lock.GetOracleObservable()));
-
-    // the oracle is used to schedule commands
-    // the oracleObservable is used by the loaders to:
-    // - request the broker (lifetime mgmt)
-    // - register the loader callbacks (called indirectly by the oracle)
-    ctLoader.reset(new OrthancStone::OrthancSeriesVolumeProgressiveLoader(ct, oracle, lock.GetOracleObservable()));
-    doseLoader.reset(new OrthancStone::OrthancMultiframeVolumeLoader(dose, oracle, lock.GetOracleObservable()));
-    rtstructLoader.reset(new OrthancStone::DicomStructureSetLoader(oracle, lock.GetOracleObservable()));
-  }
-
-
-  //toto->SetReferenceLoader(*ctLoader);
-  toto->SetReferenceLoader(*doseLoader);
-
-
-#if 1
-  toto->SetVolume1(0, ctLoader, new OrthancStone::GrayscaleStyleConfigurator);
-#else
-  {
-    boost::shared_ptr<OrthancStone::IVolumeSlicer> reslicer(new OrthancStone::DicomVolumeImageReslicer(ct));
-    toto->SetVolume1(0, reslicer, new OrthancStone::GrayscaleStyleConfigurator);
-  }
-#endif  
-  
-
-  {
-    std::unique_ptr<OrthancStone::LookupTableStyleConfigurator> config(new OrthancStone::LookupTableStyleConfigurator);
-    config->SetLookupTable(Orthanc::EmbeddedResources::COLORMAP_HOT);
-
-    boost::shared_ptr<OrthancStone::DicomVolumeImageMPRSlicer> tmp(new OrthancStone::DicomVolumeImageMPRSlicer(dose));
-    toto->SetVolume2(1, tmp, config.release());
-  }
-
-  toto->SetStructureSet(2, rtstructLoader);
-  
-  oracle.Schedule(*toto, new OrthancStone::SleepOracleCommand(100));
-
-  if (0)
-  {
-    Json::Value v = Json::objectValue;
-    v["Level"] = "Series";
-    v["Query"] = Json::objectValue;
-
-    std::unique_ptr<OrthancStone::OrthancRestApiCommand>  command(new OrthancStone::OrthancRestApiCommand);
-    command->SetMethod(Orthanc::HttpMethod_Post);
-    command->SetUri("/tools/find");
-    command->SetBody(v);
-
-    oracle.Schedule(*toto, command.release());
-  }
-  
-  if(0)
-  {
-    if (0)
-    {
-      std::unique_ptr<OrthancStone::GetOrthancImageCommand>  command(new OrthancStone::GetOrthancImageCommand);
-      command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Jpeg)));
-      command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/preview");
-      oracle.Schedule(*toto, command.release());
-    }
-    
-    if (0)
-    {
-      std::unique_ptr<OrthancStone::GetOrthancImageCommand>  command(new OrthancStone::GetOrthancImageCommand);
-      command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Png)));
-      command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/preview");
-      oracle.Schedule(*toto, command.release());
-    }
-    
-    if (0)
-    {
-      std::unique_ptr<OrthancStone::GetOrthancImageCommand>  command(new OrthancStone::GetOrthancImageCommand);
-      command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Png)));
-      command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/image-uint16");
-      oracle.Schedule(*toto, command.release());
-    }
-    
-    if (0)
-    {
-      std::unique_ptr<OrthancStone::GetOrthancImageCommand>  command(new OrthancStone::GetOrthancImageCommand);
-      command->SetHttpHeader("Accept-Encoding", "gzip");
-      command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam)));
-      command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/image-uint16");
-      oracle.Schedule(*toto, command.release());
-    }
-    
-    if (0)
-    {
-      std::unique_ptr<OrthancStone::GetOrthancImageCommand>  command(new OrthancStone::GetOrthancImageCommand);
-      command->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam)));
-      command->SetUri("/instances/6687cc73-07cae193-52ff29c8-f646cb16-0753ed92/image-uint16");
-      oracle.Schedule(*toto, command.release());
-    }
-
-    if (0)
-    {
-      std::unique_ptr<OrthancStone::GetOrthancWebViewerJpegCommand>  command(new OrthancStone::GetOrthancWebViewerJpegCommand);
-      command->SetHttpHeader("Accept-Encoding", "gzip");
-      command->SetInstance("e6c7c20b-c9f65d7e-0d76f2e2-830186f2-3e3c600e");
-      command->SetQuality(90);
-      oracle.Schedule(*toto, command.release());
-    }
-
-
-    if (0)
-    {
-      for (unsigned int i = 0; i < 10; i++)
-      {
-        std::unique_ptr<OrthancStone::SleepOracleCommand> command(new OrthancStone::SleepOracleCommand(i * 1000));
-        command->SetPayload(new Orthanc::SingleValueObject<unsigned int>(42 * i));
-        oracle.Schedule(*toto, command.release());
-      }
-    }
-  }
-  
-  // 2017-11-17-Anonymized
-#if 0
-  // BGO data
-  ctLoader->LoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");  // CT
-  doseLoader->LoadInstance("830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb");  // RT-DOSE
-  //rtstructLoader->LoadInstance("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9");  // RT-STRUCT
-#else
-  //ctLoader->LoadSeries("cb3ea4d1-d08f3856-ad7b6314-74d88d77-60b05618");  // CT
-  //doseLoader->LoadInstance("41029085-71718346-811efac4-420e2c15-d39f99b6");  // RT-DOSE
-  //rtstructLoader->LoadInstance("83d9c0c3-913a7fee-610097d7-cbf0522d-fd75bee6");  // RT-STRUCT
-
-  // 2017-05-16
-  ctLoader->LoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");  // CT
-  doseLoader->LoadInstance("eac822ef-a395f94e-e8121fe0-8411fef8-1f7bffad");  // RT-DOSE
-  rtstructLoader->LoadInstance("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9");  // RT-STRUCT
-#endif
-  // 2015-01-28-Multiframe
-  //doseLoader->LoadInstance("88f71e2a-5fad1c61-96ed14d6-5b3d3cf7-a5825279");  // Multiframe CT
-  
-  // Delphine
-  //ctLoader->LoadSeries("5990e39c-51e5f201-fe87a54c-31a55943-e59ef80e");  // CT
-  //ctLoader->LoadSeries("67f1b334-02c16752-45026e40-a5b60b6b-030ecab5");  // Lung 1/10mm
-
-
-  {
-    LOG(WARNING) << "...Waiting for Ctrl-C...";
-
-    oracle.Start();
-
-    Orthanc::SystemToolbox::ServerBarrier();
-
-    /**
-     * WARNING => The oracle must be stopped BEFORE the objects using
-     * it are destroyed!!! This forces to wait for the completion of
-     * the running callback methods. Otherwise, the callbacks methods
-     * might still be running while their parent object is destroyed,
-     * resulting in crashes. This is very visible if adding a sleep(),
-     * as in (*).
-     **/
-
-    oracle.Stop();
-  }
-}
-
-
-
-/**
- * IMPORTANT: The full arguments to "main()" are needed for SDL on
- * Windows. Otherwise, one gets the linking error "undefined reference
- * to `SDL_main'". https://wiki.libsdl.org/FAQWindows
- **/
-int main(int argc, char* argv[])
-{
-  OrthancStone::StoneInitialize();
-  //Orthanc::Logging::EnableInfoLevel(true);
-
-  try
-  {
-    OrthancStone::NativeApplicationContext context;
-
-    OrthancStone::ThreadedOracle oracle(context);
-    //oracle.SetThreadsCount(1);
-
-    {
-      Orthanc::WebServiceParameters p;
-      //p.SetUrl("http://localhost:8043/");
-      p.SetCredentials("orthanc", "orthanc");
-      oracle.SetOrthancParameters(p);
-    }
-
-    //oracle.Start();
-
-    Run(context, oracle);
-    
-    //oracle.Stop();
-  }
-  catch (Orthanc::OrthancException& e)
-  {
-    LOG(ERROR) << "EXCEPTION: " << e.What();
-  }
-
-  OrthancStone::StoneFinalize();
-
-  return 0;
-}
--- a/Samples/Sdl/RadiographyEditor.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,267 +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 "../Shared/RadiographyEditorApp.h"
-
-// From Stone
-#include "../../Framework/Oracle/SleepOracleCommand.h"
-#include "../../Framework/Oracle/ThreadedOracle.h"
-#include "../../Applications/Sdl/SdlOpenGLWindow.h"
-#include "../../Framework/Scene2D/OpenGLCompositor.h"
-#include "../../Framework/Scene2D/CairoCompositor.h"
-#include "../../Framework/Scene2D/ColorTextureSceneLayer.h"
-#include "../../Framework/Scene2D/OpenGLCompositor.h"
-#include "../../Framework/StoneInitialization.h"
-
-#include <Core/Logging.h>
-#include <Core/OrthancException.h>
-
-
-#include <boost/shared_ptr.hpp>
-#include <boost/weak_ptr.hpp>
-
-#include <SDL.h>
-#include <stdio.h>
-
-using namespace OrthancStone;
-
-namespace OrthancStone
-{
-  class NativeApplicationContext : public IMessageEmitter
-  {
-  private:
-    boost::shared_mutex  mutex_;
-    MessageBroker        broker_;
-    IObservable          oracleObservable_;
-
-  public:
-    NativeApplicationContext() :
-      oracleObservable_(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:
-      NativeApplicationContext&                that_;
-      boost::shared_lock<boost::shared_mutex>  lock_;
-
-    public:
-      ReaderLock(NativeApplicationContext& that) :
-        that_(that),
-        lock_(that.mutex_)
-      {
-      }
-    };
-
-
-    class WriterLock : public boost::noncopyable
-    {
-    private:
-      NativeApplicationContext&                that_;
-      boost::unique_lock<boost::shared_mutex>  lock_;
-
-    public:
-      WriterLock(NativeApplicationContext& that) :
-        that_(that),
-        lock_(that.mutex_)
-      {
-      }
-
-      MessageBroker& GetBroker()
-      {
-        return that_.broker_;
-      }
-
-      IObservable& GetOracleObservable()
-      {
-        return that_.oracleObservable_;
-      }
-    };
-  };
-}
-
-class OpenGlSdlCompositorFactory : public ICompositorFactory
-{
-  OpenGL::IOpenGLContext& openGlContext_;
-
-public:
-  OpenGlSdlCompositorFactory(OpenGL::IOpenGLContext& openGlContext) :
-    openGlContext_(openGlContext)
-  {}
-
-  ICompositor* GetCompositor(const Scene2D& scene)
-  {
-
-    OpenGLCompositor* compositor = new OpenGLCompositor(openGlContext_, scene);
-    compositor->SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT,
-                         FONT_SIZE_0, Orthanc::Encoding_Latin1);
-    compositor->SetFont(1, Orthanc::EmbeddedResources::UBUNTU_FONT,
-                         FONT_SIZE_1, Orthanc::Encoding_Latin1);
-    return compositor;
-  }
-};
-
-static void GLAPIENTRY
-OpenGLMessageCallback(GLenum source,
-                      GLenum type,
-                      GLuint id,
-                      GLenum severity,
-                      GLsizei length,
-                      const GLchar* message,
-                      const void* userParam)
-{
-  if (severity != GL_DEBUG_SEVERITY_NOTIFICATION)
-  {
-    fprintf(stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n",
-            (type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : ""),
-            type, severity, message);
-  }
-}
-
-
-/**
- * IMPORTANT: The full arguments to "main()" are needed for SDL on
- * Windows. Otherwise, one gets the linking error "undefined reference
- * to `SDL_main'". https://wiki.libsdl.org/FAQWindows
- **/
-int main(int argc, char* argv[])
-{
-  using namespace OrthancStone;
-
-  StoneInitialize();
-  Orthanc::Logging::EnableInfoLevel(true);
-  //  Orthanc::Logging::EnableTraceLevel(true);
-
-  try
-  {
-    OrthancStone::NativeApplicationContext context;
-    OrthancStone::NativeApplicationContext::WriterLock lock(context);
-    OrthancStone::ThreadedOracle oracle(context);
-
-    // False means we do NOT let Windows treat this as a legacy application
-    // that needs to be scaled
-    SdlOpenGLWindow window("Hello", 1024, 1024, false);
-
-    glEnable(GL_DEBUG_OUTPUT);
-    glDebugMessageCallback(OpenGLMessageCallback, 0);
-
-    std::unique_ptr<OpenGlSdlCompositorFactory> compositorFactory(new OpenGlSdlCompositorFactory(window));
-    boost::shared_ptr<RadiographyEditorApp> app(new RadiographyEditorApp(oracle, lock.GetOracleObservable(), compositorFactory.release()));
-    app->PrepareScene();
-    app->FitContent(window.GetCanvasWidth(), window.GetCanvasHeight());
-
-    bool stopApplication = false;
-
-    while (!stopApplication)
-    {
-      app->Refresh();
-
-      SDL_Event event;
-      while (!stopApplication && SDL_PollEvent(&event))
-      {
-        OrthancStone::KeyboardModifiers modifiers = OrthancStone::KeyboardModifiers_None;
-        if (event.key.keysym.mod & KMOD_CTRL)
-          modifiers = static_cast<OrthancStone::KeyboardModifiers>(static_cast<int>(modifiers) | static_cast<int>(OrthancStone::KeyboardModifiers_Control));
-        if (event.key.keysym.mod & KMOD_ALT)
-          modifiers = static_cast<OrthancStone::KeyboardModifiers>(static_cast<int>(modifiers) | static_cast<int>(OrthancStone::KeyboardModifiers_Alt));
-        if (event.key.keysym.mod & KMOD_SHIFT)
-          modifiers = static_cast<OrthancStone::KeyboardModifiers>(static_cast<int>(modifiers) | static_cast<int>(OrthancStone::KeyboardModifiers_Shift));
-
-        OrthancStone::MouseButton button;
-        if (event.button.button == SDL_BUTTON_LEFT)
-          button = OrthancStone::MouseButton_Left;
-        else if (event.button.button == SDL_BUTTON_MIDDLE)
-          button = OrthancStone::MouseButton_Middle;
-        else if (event.button.button == SDL_BUTTON_RIGHT)
-          button = OrthancStone::MouseButton_Right;
-
-        if (event.type == SDL_QUIT)
-        {
-          stopApplication = true;
-          break;
-        }
-        else if (event.type == SDL_WINDOWEVENT &&
-                 event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
-        {
-          app->DisableTracker(); // was: tracker.reset(NULL);
-          app->UpdateSize();
-        }
-        else if (event.type == SDL_KEYDOWN &&
-                 event.key.repeat == 0 /* Ignore key bounce */)
-        {
-          switch (event.key.keysym.sym)
-          {
-          case SDLK_f:
-            window.GetWindow().ToggleMaximize();
-            break;
-
-          case SDLK_q:
-            stopApplication = true;
-            break;
-          default:
-          {
-            app->OnKeyPressed(event.key.keysym.sym, modifiers);
-           }
-          }
-        }
-        else if (event.type == SDL_MOUSEBUTTONDOWN)
-        {
-          app->OnMouseDown(event.button.x, event.button.y, modifiers, button);
-        }
-        else if (event.type == SDL_MOUSEMOTION)
-        {
-          app->OnMouseMove(event.button.x, event.button.y, modifiers);
-        }
-        else if (event.type == SDL_MOUSEBUTTONUP)
-        {
-          app->OnMouseUp(event.button.x, event.button.y, modifiers, button);
-        }
-      }
-      SDL_Delay(1);
-    }
-  }
-  catch (Orthanc::OrthancException& e)
-  {
-    LOG(ERROR) << "EXCEPTION: " << e.What();
-  }
-
-  StoneFinalize();
-
-  return 0;
-}
-
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Sdl/SimpleViewer/CMakeLists.txt	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,42 @@
+cmake_minimum_required(VERSION 2.8.10)
+
+project(SdlSimpleViewer)
+
+set(ORTHANC_FRAMEWORK_SOURCE "path")
+set(ORTHANC_FRAMEWORK_ROOT ${CMAKE_CURRENT_LIST_DIR}/../../../../orthanc)
+set(STONE_ROOT ${CMAKE_CURRENT_LIST_DIR}/../../../)
+
+include(${STONE_ROOT}/Resources/CMake/OrthancStoneParameters.cmake)
+
+SET(ENABLE_GOOGLE_TEST OFF)
+SET(ENABLE_LOCALE ON)  # Necessary for text rendering
+SET(ENABLE_QT OFF)
+SET(ENABLE_SDL ON)
+SET(ENABLE_DCMTK ON)  # <==
+SET(ENABLE_OPENGL ON)  #  <==
+SET(ENABLE_WEB_CLIENT ON)
+SET(ORTHANC_SANDBOXED OFF)
+
+include(${STONE_ROOT}/Resources/CMake/OrthancStoneConfiguration.cmake)
+include(${STONE_ROOT}/Resources/CMake/Utilities.cmake)
+
+include_directories(${STONE_ROOT})
+
+add_definitions(
+  -DORTHANC_ENABLE_LOGGING=1
+  -DORTHANC_ENABLE_LOGGING_PLUGIN=0
+  -DORTHANC_ENABLE_PUGIXML=0
+  -DORTHANC_DEFAULT_DICOM_ENCODING=Encoding_Latin1
+  )
+
+SortFilesInSourceGroups()
+
+add_executable(SdlSimpleViewer
+  SdlSimpleViewerApplication.h
+  SdlSimpleViewer.cpp
+  ${ORTHANC_STONE_SOURCES}
+  )
+
+
+target_link_libraries(SdlSimpleViewer ${DCMTK_LIBRARIES})
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Sdl/SimpleViewer/CMakeSettings.json	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,37 @@
+{
+  "configurations": [
+    {
+      "name": "x64-Debug",
+      "generator": "Ninja",
+      "configurationType": "Debug",
+      "inheritEnvironments": [ "msvc_x64_x64" ],
+      "buildRoot": "${projectDir}\\out\\build\\${name}",
+      "installRoot": "${projectDir}\\out\\install\\${name}",
+      "cmakeCommandArgs": "",
+      "buildCommandArgs": "-v",
+      "ctestCommandArgs": "",
+      "variables": [
+        {
+          "name": "MSVC_MULTIPLE_PROCESSES",
+          "value": "True",
+          "type": "BOOL"
+        },
+        {
+          "name": "ALLOW_DOWNLOADS",
+          "value": "True",
+          "type": "BOOL"
+        },
+        {
+          "name": "STATIC_BUILD",
+          "value": "True",
+          "type": "BOOL"
+        },
+        {
+          "name": "OPENSSL_NO_CAPIENG",
+          "value": "True",
+          "type": "BOOL"
+        },
+      ]
+    }
+  ]
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Sdl/SimpleViewer/SdlSimpleViewer.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,292 @@
+
+#include "SdlSimpleViewerApplication.h"
+
+#include <Core/OrthancException.h>
+
+#include <Framework/Loaders/GenericLoadersContext.h>
+#include <Framework/StoneException.h>
+#include <Framework/StoneEnumerations.h>
+#include <Framework/StoneInitialization.h>
+#include <Framework/Viewport/SdlViewport.h>
+
+#include <SDL.h>
+
+namespace OrthancStone
+{
+  static KeyboardModifiers GetKeyboardModifiers(const uint8_t* keyboardState,
+                                                const int scancodeCount)
+  {
+    int result = KeyboardModifiers_None;
+
+    if (keyboardState != NULL)
+    {
+      if (SDL_SCANCODE_LSHIFT < scancodeCount &&
+          keyboardState[SDL_SCANCODE_LSHIFT])
+      {
+        result |= KeyboardModifiers_Shift;
+      }
+
+      if (SDL_SCANCODE_RSHIFT < scancodeCount &&
+          keyboardState[SDL_SCANCODE_RSHIFT])
+      {
+        result |= KeyboardModifiers_Shift;
+      }
+
+      if (SDL_SCANCODE_LCTRL < scancodeCount &&
+          keyboardState[SDL_SCANCODE_LCTRL])
+      {
+        result |= KeyboardModifiers_Control;
+      }
+
+      if (SDL_SCANCODE_RCTRL < scancodeCount &&
+          keyboardState[SDL_SCANCODE_RCTRL])
+      {
+        result |= KeyboardModifiers_Control;
+      }
+
+      if (SDL_SCANCODE_LALT < scancodeCount &&
+          keyboardState[SDL_SCANCODE_LALT])
+      {
+        result |= KeyboardModifiers_Alt;
+      }
+
+      if (SDL_SCANCODE_RALT < scancodeCount &&
+          keyboardState[SDL_SCANCODE_RALT])
+      {
+        result |= KeyboardModifiers_Alt;
+      }
+    }
+
+    return static_cast<KeyboardModifiers>(result);
+  }
+
+
+  static void GetPointerEvent(PointerEvent& p,
+                              const ICompositor& compositor,
+                              SDL_Event event,
+                              const uint8_t* keyboardState,
+                              const int scancodeCount)
+  {
+    KeyboardModifiers modifiers = GetKeyboardModifiers(keyboardState, scancodeCount);
+
+    switch (event.button.button)
+    {
+    case SDL_BUTTON_LEFT:
+      p.SetMouseButton(OrthancStone::MouseButton_Left);
+      break;
+
+    case SDL_BUTTON_RIGHT:
+      p.SetMouseButton(OrthancStone::MouseButton_Right);
+      break;
+
+    case SDL_BUTTON_MIDDLE:
+      p.SetMouseButton(OrthancStone::MouseButton_Middle);
+      break;
+
+    default:
+      p.SetMouseButton(OrthancStone::MouseButton_None);
+      break;
+    }
+
+    p.AddPosition(compositor.GetPixelCenterCoordinates(event.button.x, event.button.y));
+    p.SetAltModifier(modifiers & KeyboardModifiers_Alt);
+    p.SetControlModifier(modifiers & KeyboardModifiers_Control);
+    p.SetShiftModifier(modifiers & KeyboardModifiers_Shift);
+  }
+
+}
+
+/**
+ * IMPORTANT: The full arguments to "main()" are needed for SDL on
+ * Windows. Otherwise, one gets the linking error "undefined reference
+ * to `SDL_main'". https://wiki.libsdl.org/FAQWindows
+ **/
+int main(int argc, char* argv[])
+{
+  try
+  {
+    OrthancStone::StoneInitialize();
+    Orthanc::Logging::EnableInfoLevel(true);
+    //Orthanc::Logging::EnableTraceLevel(true);
+
+    {
+
+#if 1
+      boost::shared_ptr<OrthancStone::SdlViewport> viewport = 
+        OrthancStone::SdlOpenGLViewport::Create("Stone of Orthanc", 800, 600);
+#else
+      boost::shared_ptr<OrthancStone::SdlViewport> viewport =
+        OrthancStone::SdlCairoViewport::Create("Stone of Orthanc", 800, 600);
+#endif
+
+      OrthancStone::GenericLoadersContext context(1, 4, 1);
+      
+      context.StartOracle();
+
+      {
+
+        boost::shared_ptr<SdlSimpleViewerApplication> application(
+          SdlSimpleViewerApplication::Create(context, viewport));
+
+        OrthancStone::DicomSource source;
+
+        // Default and command-line parameters
+        const char* instanceId = "285dece8-e1956b38-cdc7d084-6ce3371e-536a9ffc";
+        unsigned int frameIndex = 0;
+
+        if (argc == 1)
+        {
+          LOG(ERROR) << "No instanceId supplied. The default of " << instanceId << " will be used. "
+            << "Please supply the Orthanc instance ID of the frame you wish to display then, optionally, "
+            << "the zero-based index of the frame (for multi-frame instances)";
+          // TODO: frame number as second argument...
+        }
+
+        if (argc >= 2)
+          instanceId = argv[1];
+
+        if (argc >= 3)
+          frameIndex = atoi(argv[1]);
+
+        if (argc > 3)
+        {
+          LOG(ERROR) << "Extra arguments ignored!";
+        }
+         
+
+        application->LoadOrthancFrame(source, instanceId, frameIndex);
+
+        OrthancStone::DefaultViewportInteractor interactor;
+
+        {
+          int scancodeCount = 0;
+          const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount);
+
+          bool stop = false;
+          while (!stop)
+          {
+            bool paint = false;
+            SDL_Event event;
+            while (SDL_PollEvent(&event))
+            {
+              if (event.type == SDL_QUIT)
+              {
+                stop = true;
+                break;
+              }
+              else if (viewport->IsRefreshEvent(event))
+              {
+                paint = true;
+              }
+              else if (event.type == SDL_WINDOWEVENT &&
+                (event.window.event == SDL_WINDOWEVENT_RESIZED ||
+                 event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED))
+              {
+                viewport->UpdateSize(event.window.data1, event.window.data2);
+              }
+              else if (event.type == SDL_WINDOWEVENT &&
+                (event.window.event == SDL_WINDOWEVENT_SHOWN ||
+                 event.window.event == SDL_WINDOWEVENT_EXPOSED))
+              {
+                paint = true;
+              }
+              else if (event.type == SDL_KEYDOWN &&
+                       event.key.repeat == 0 /* Ignore key bounce */)
+              {
+                switch (event.key.keysym.sym)
+                {
+                case SDLK_f:
+                  viewport->ToggleMaximize();
+                  break;
+
+                case SDLK_s:
+                  application->FitContent();
+                  break;
+
+                case SDLK_q:
+                  stop = true;
+                  break;
+
+                default:
+                  break;
+                }
+              }
+              else if (event.type == SDL_MOUSEBUTTONDOWN ||
+                       event.type == SDL_MOUSEMOTION ||
+                       event.type == SDL_MOUSEBUTTONUP)
+              {
+                std::auto_ptr<OrthancStone::IViewport::ILock> lock(viewport->Lock());
+                if (lock->HasCompositor())
+                {
+                  OrthancStone::PointerEvent p;
+                  OrthancStone::GetPointerEvent(p, lock->GetCompositor(),
+                                                event, keyboardState, scancodeCount);
+
+                  switch (event.type)
+                  {
+                  case SDL_MOUSEBUTTONDOWN:
+                    lock->GetController().HandleMousePress(interactor, p,
+                                                           lock->GetCompositor().GetCanvasWidth(),
+                                                           lock->GetCompositor().GetCanvasHeight());
+                    lock->Invalidate();
+                    break;
+
+                  case SDL_MOUSEMOTION:
+                    if (lock->GetController().HandleMouseMove(p))
+                    {
+                      lock->Invalidate();
+                    }
+                    break;
+
+                  case SDL_MOUSEBUTTONUP:
+                    lock->GetController().HandleMouseRelease(p);
+                    lock->Invalidate();
+                    break;
+
+                  default:
+                    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+                  }
+                }
+              }
+            }
+
+            if (paint)
+            {
+              viewport->Paint();
+            }
+
+            // Small delay to avoid using 100% of CPU
+            SDL_Delay(1);
+          }
+        }
+
+        context.StopOracle();
+      }
+    }
+
+    OrthancStone::StoneFinalize();
+    return 0;
+  }
+  catch (Orthanc::OrthancException & e)
+  {
+    auto test = e.What();
+    fprintf(stdout, test);
+    LOG(ERROR) << "OrthancException: " << e.What();
+    return -1;
+  }
+  catch (OrthancStone::StoneException & e)
+  {
+    LOG(ERROR) << "StoneException: " << e.What();
+    return -1;
+  }
+  catch (std::runtime_error & e)
+  {
+    LOG(ERROR) << "Runtime error: " << e.what();
+    return -1;
+  }
+  catch (...)
+  {
+    LOG(ERROR) << "Native exception";
+    return -1;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Sdl/SimpleViewer/SdlSimpleViewerApplication.h	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,146 @@
+#pragma once
+
+#include <Framework/Viewport/IViewport.h>
+#include <Framework/Loaders/DicomResourcesLoader.h>
+#include <Framework/Loaders/ILoadersContext.h>
+#include <Framework/Loaders/SeriesFramesLoader.h>
+#include <Framework/Loaders/SeriesThumbnailsLoader.h>
+
+#include <boost/make_shared.hpp>
+
+
+using OrthancStone::ILoadersContext;
+using OrthancStone::ObserverBase;
+using OrthancStone::IViewport;
+using OrthancStone::DicomResourcesLoader;
+using OrthancStone::SeriesFramesLoader;
+using OrthancStone::TextureBaseSceneLayer;
+using OrthancStone::DicomSource;
+using OrthancStone::SeriesThumbnailsLoader;
+using OrthancStone::LoadedDicomResources;
+using OrthancStone::SeriesThumbnailType;
+using OrthancStone::OracleScheduler;
+using OrthancStone::OrthancRestApiCommand;
+using OrthancStone::OracleScheduler;
+using OrthancStone::OracleScheduler;
+using OrthancStone::OracleScheduler;
+
+
+class SdlSimpleViewerApplication : public ObserverBase<SdlSimpleViewerApplication>
+{
+
+public:
+  static boost::shared_ptr<SdlSimpleViewerApplication> Create(ILoadersContext& context, boost::shared_ptr<IViewport> viewport)
+  {
+    boost::shared_ptr<SdlSimpleViewerApplication> application(new SdlSimpleViewerApplication(context, viewport));
+
+    {
+      std::auto_ptr<ILoadersContext::ILock> lock(context.Lock());
+      DicomResourcesLoader::Factory f;
+      application->dicomLoader_ = boost::dynamic_pointer_cast<DicomResourcesLoader>(f.Create(*lock));
+    }
+
+    application->Register<DicomResourcesLoader::SuccessMessage>(*application->dicomLoader_, &SdlSimpleViewerApplication::Handle);
+
+    return application;
+  }
+
+  void LoadOrthancFrame(const DicomSource& source, const std::string& instanceId, unsigned int frame)
+  {
+    std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
+
+    dicomLoader_->ScheduleLoadOrthancResource(boost::make_shared<LoadedDicomResources>(Orthanc::DICOM_TAG_SOP_INSTANCE_UID),
+                                              0, source, Orthanc::ResourceType_Instance, instanceId,
+                                              new Orthanc::SingleValueObject<unsigned int>(frame));
+  }
+
+#if 0
+  void LoadDicomWebFrame(const DicomSource& source,
+                         const std::string& studyInstanceUid,
+                         const std::string& seriesInstanceUid,
+                         const std::string& sopInstanceUid,
+                         unsigned int frame)
+  {
+    std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
+
+    // We first must load the "/metadata" to know the number of frames
+    dicomLoader_->ScheduleGetDicomWeb(
+      boost::make_shared<LoadedDicomResources>(Orthanc::DICOM_TAG_SOP_INSTANCE_UID), 0, source,
+      "/studies/" + studyInstanceUid + "/series/" + seriesInstanceUid + "/instances/" + sopInstanceUid + "/metadata",
+      new Orthanc::SingleValueObject<unsigned int>(frame));
+  }
+#endif 
+
+  void FitContent()
+  {
+    std::auto_ptr<IViewport::ILock> lock(viewport_->Lock());
+    lock->GetCompositor().FitContent(lock->GetController().GetScene());
+    lock->Invalidate();
+  }
+
+private:
+  ILoadersContext& context_;
+  boost::shared_ptr<IViewport>             viewport_;
+  boost::shared_ptr<DicomResourcesLoader>  dicomLoader_;
+  boost::shared_ptr<SeriesFramesLoader>    framesLoader_;
+
+  SdlSimpleViewerApplication(ILoadersContext& context,
+                             boost::shared_ptr<IViewport> viewport) :
+    context_(context),
+    viewport_(viewport)
+  {
+  }
+
+  void Handle(const SeriesFramesLoader::FrameLoadedMessage& message)
+  {
+    LOG(INFO) << "Frame decoded! "
+      << message.GetImage().GetWidth() << "x" << message.GetImage().GetHeight()
+      << " " << Orthanc::EnumerationToString(message.GetImage().GetFormat());
+
+    std::auto_ptr<TextureBaseSceneLayer> layer(
+      message.GetInstanceParameters().CreateTexture(message.GetImage()));
+    layer->SetLinearInterpolation(true);
+
+    {
+      std::auto_ptr<IViewport::ILock> lock(viewport_->Lock());
+      lock->GetController().GetScene().SetLayer(0, layer.release());
+      lock->GetCompositor().FitContent(lock->GetController().GetScene());
+      lock->Invalidate();
+    }
+  }
+
+  void Handle(const DicomResourcesLoader::SuccessMessage& message)
+  {
+    if (message.GetResources()->GetSize() != 1)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    //message.GetResources()->GetResource(0).Print(stdout);
+
+    {
+      std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
+      SeriesFramesLoader::Factory f(*message.GetResources());
+
+      framesLoader_ = boost::dynamic_pointer_cast<SeriesFramesLoader>(
+        f.Create(*lock));
+      
+      Register<SeriesFramesLoader::FrameLoadedMessage>(
+        *framesLoader_, &SdlSimpleViewerApplication::Handle);
+
+      assert(message.HasUserPayload());
+
+      const Orthanc::SingleValueObject<unsigned int>& payload =
+        dynamic_cast<const Orthanc::SingleValueObject<unsigned int>&>(
+          message.GetUserPayload());
+
+      LOG(INFO) << "Loading pixel data of frame: " << payload.GetValue();
+      framesLoader_->ScheduleLoadFrame(
+        0, message.GetDicomSource(), payload.GetValue(),
+        message.GetDicomSource().GetQualityCount() - 1 /* download best quality available */,
+        NULL);
+    }
+  }
+
+};
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Sdl/SimpleViewer/SimpleViewer.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,292 @@
+
+#include "SdlSimpleViewerApplication.h"
+
+#include <Core/OrthancException.h>
+
+#include <Framework/Loaders/GenericLoadersContext.h>
+#include <Framework/StoneException.h>
+#include <Framework/StoneEnumerations.h>
+#include <Framework/StoneInitialization.h>
+#include <Framework/Viewport/SdlViewport.h>
+
+#include <SDL.h>
+
+namespace OrthancStone
+{
+  static KeyboardModifiers GetKeyboardModifiers(const uint8_t* keyboardState,
+                                                const int scancodeCount)
+  {
+    int result = KeyboardModifiers_None;
+
+    if (keyboardState != NULL)
+    {
+      if (SDL_SCANCODE_LSHIFT < scancodeCount &&
+          keyboardState[SDL_SCANCODE_LSHIFT])
+      {
+        result |= KeyboardModifiers_Shift;
+      }
+
+      if (SDL_SCANCODE_RSHIFT < scancodeCount &&
+          keyboardState[SDL_SCANCODE_RSHIFT])
+      {
+        result |= KeyboardModifiers_Shift;
+      }
+
+      if (SDL_SCANCODE_LCTRL < scancodeCount &&
+          keyboardState[SDL_SCANCODE_LCTRL])
+      {
+        result |= KeyboardModifiers_Control;
+      }
+
+      if (SDL_SCANCODE_RCTRL < scancodeCount &&
+          keyboardState[SDL_SCANCODE_RCTRL])
+      {
+        result |= KeyboardModifiers_Control;
+      }
+
+      if (SDL_SCANCODE_LALT < scancodeCount &&
+          keyboardState[SDL_SCANCODE_LALT])
+      {
+        result |= KeyboardModifiers_Alt;
+      }
+
+      if (SDL_SCANCODE_RALT < scancodeCount &&
+          keyboardState[SDL_SCANCODE_RALT])
+      {
+        result |= KeyboardModifiers_Alt;
+      }
+    }
+
+    return static_cast<KeyboardModifiers>(result);
+  }
+
+
+  static void GetPointerEvent(PointerEvent& p,
+                              const ICompositor& compositor,
+                              SDL_Event event,
+                              const uint8_t* keyboardState,
+                              const int scancodeCount)
+  {
+    KeyboardModifiers modifiers = GetKeyboardModifiers(keyboardState, scancodeCount);
+
+    switch (event.button.button)
+    {
+    case SDL_BUTTON_LEFT:
+      p.SetMouseButton(OrthancStone::MouseButton_Left);
+      break;
+
+    case SDL_BUTTON_RIGHT:
+      p.SetMouseButton(OrthancStone::MouseButton_Right);
+      break;
+
+    case SDL_BUTTON_MIDDLE:
+      p.SetMouseButton(OrthancStone::MouseButton_Middle);
+      break;
+
+    default:
+      p.SetMouseButton(OrthancStone::MouseButton_None);
+      break;
+    }
+
+    p.AddPosition(compositor.GetPixelCenterCoordinates(event.button.x, event.button.y));
+    p.SetAltModifier(modifiers & KeyboardModifiers_Alt);
+    p.SetControlModifier(modifiers & KeyboardModifiers_Control);
+    p.SetShiftModifier(modifiers & KeyboardModifiers_Shift);
+  }
+
+}
+
+/**
+ * IMPORTANT: The full arguments to "main()" are needed for SDL on
+ * Windows. Otherwise, one gets the linking error "undefined reference
+ * to `SDL_main'". https://wiki.libsdl.org/FAQWindows
+ **/
+int main(int argc, char* argv[])
+{
+  try
+  {
+    OrthancStone::StoneInitialize();
+    Orthanc::Logging::EnableInfoLevel(true);
+    //Orthanc::Logging::EnableTraceLevel(true);
+
+    {
+
+#if 1
+      boost::shared_ptr<OrthancStone::SdlViewport> viewport = 
+        OrthancStone::SdlOpenGLViewport::Create("Stone of Orthanc", 800, 600);
+#else
+      boost::shared_ptr<OrthancStone::SdlViewport> viewport =
+        OrthancStone::SdlCairoViewport::Create("Stone of Orthanc", 800, 600);
+#endif
+
+      OrthancStone::GenericLoadersContext context(1, 4, 1);
+      
+      context.StartOracle();
+
+      {
+
+        boost::shared_ptr<SdlSimpleViewerApplication> application(
+          SdlSimpleViewerApplication::Create(context, viewport));
+
+        OrthancStone::DicomSource source;
+
+        // Default and command-line parameters
+        const char* instanceId = "285dece8-e1956b38-cdc7d084-6ce3371e-536a9ffc";
+        unsigned int frameIndex = 0;
+
+        if (argc == 1)
+        {
+          LOG(ERROR) << "No instanceId supplied. The default of " << instanceId << " will be used. "
+            << "Please supply the Orthanc instance ID of the frame you wish to display then, optionally, "
+            << "the zero-based index of the frame (for multi-frame instances)";
+          // TODO: frame number as second argument...
+        }
+
+        if (argc >= 2)
+          instanceId = argv[1];
+
+        if (argc >= 3)
+          frameIndex = atoi(argv[1]);
+
+        if (argc > 3)
+        {
+          LOG(ERROR) << "Extra arguments ignored!";
+        }
+         
+
+        application->LoadOrthancFrame(source, instanceId, frameIndex);
+
+        OrthancStone::DefaultViewportInteractor interactor;
+
+        {
+          int scancodeCount = 0;
+          const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount);
+
+          bool stop = false;
+          while (!stop)
+          {
+            bool paint = false;
+            SDL_Event event;
+            while (SDL_PollEvent(&event))
+            {
+              if (event.type == SDL_QUIT)
+              {
+                stop = true;
+                break;
+              }
+              else if (viewport->IsRefreshEvent(event))
+              {
+                paint = true;
+              }
+              else if (event.type == SDL_WINDOWEVENT &&
+                (event.window.event == SDL_WINDOWEVENT_RESIZED ||
+                 event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED))
+              {
+                viewport->UpdateSize(event.window.data1, event.window.data2);
+              }
+              else if (event.type == SDL_WINDOWEVENT &&
+                (event.window.event == SDL_WINDOWEVENT_SHOWN ||
+                 event.window.event == SDL_WINDOWEVENT_EXPOSED))
+              {
+                paint = true;
+              }
+              else if (event.type == SDL_KEYDOWN &&
+                       event.key.repeat == 0 /* Ignore key bounce */)
+              {
+                switch (event.key.keysym.sym)
+                {
+                case SDLK_f:
+                  viewport->ToggleMaximize();
+                  break;
+
+                case SDLK_s:
+                  application->FitContent();
+                  break;
+
+                case SDLK_q:
+                  stop = true;
+                  break;
+
+                default:
+                  break;
+                }
+              }
+              else if (event.type == SDL_MOUSEBUTTONDOWN ||
+                       event.type == SDL_MOUSEMOTION ||
+                       event.type == SDL_MOUSEBUTTONUP)
+              {
+                std::auto_ptr<OrthancStone::IViewport::ILock> lock(viewport->Lock());
+                if (lock->HasCompositor())
+                {
+                  OrthancStone::PointerEvent p;
+                  OrthancStone::GetPointerEvent(p, lock->GetCompositor(),
+                                                event, keyboardState, scancodeCount);
+
+                  switch (event.type)
+                  {
+                  case SDL_MOUSEBUTTONDOWN:
+                    lock->GetController().HandleMousePress(interactor, p,
+                                                           lock->GetCompositor().GetCanvasWidth(),
+                                                           lock->GetCompositor().GetCanvasHeight());
+                    lock->Invalidate();
+                    break;
+
+                  case SDL_MOUSEMOTION:
+                    if (lock->GetController().HandleMouseMove(p))
+                    {
+                      lock->Invalidate();
+                    }
+                    break;
+
+                  case SDL_MOUSEBUTTONUP:
+                    lock->GetController().HandleMouseRelease(p);
+                    lock->Invalidate();
+                    break;
+
+                  default:
+                    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+                  }
+                }
+              }
+            }
+
+            if (paint)
+            {
+              viewport->Paint();
+            }
+
+            // Small delay to avoid using 100% of CPU
+            SDL_Delay(1);
+          }
+        }
+
+        context.StopOracle();
+      }
+    }
+
+    OrthancStone::StoneFinalize();
+    return 0;
+  }
+  catch (Orthanc::OrthancException & e)
+  {
+    auto test = e.What();
+    fprintf(stdout, test);
+    LOG(ERROR) << "OrthancException: " << e.What();
+    return -1;
+  }
+  catch (OrthancStone::StoneException & e)
+  {
+    LOG(ERROR) << "StoneException: " << e.What();
+    return -1;
+  }
+  catch (std::runtime_error & e)
+  {
+    LOG(ERROR) << "Runtime error: " << e.what();
+    return -1;
+  }
+  catch (...)
+  {
+    LOG(ERROR) << "Native exception";
+    return -1;
+  }
+}
--- a/Samples/Sdl/TrackerSample.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,93 +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 "TrackerSampleApp.h"
-
- // From Stone
-#include "../../Framework/OpenGL/SdlOpenGLContext.h"
-#include "../../Framework/Scene2D/CairoCompositor.h"
-#include "../../Framework/Scene2D/ColorTextureSceneLayer.h"
-#include "../../Framework/Scene2D/OpenGLCompositor.h"
-#include "../../Framework/StoneInitialization.h"
-
-#include <Core/Logging.h>
-#include <Core/OrthancException.h>
-
-
-#include <boost/shared_ptr.hpp>
-#include <boost/weak_ptr.hpp>
-
-#include <SDL.h>
-#include <stdio.h>
-
-/*
-TODO:
-
-- to decouple the trackers from the sample, we need to supply them with
-  the scene rather than the app
-
-- in order to do that, we need a GetNextFreeZIndex function (or something 
-  along those lines) in the scene object
-
-*/
-
-boost::weak_ptr<OrthancStone::TrackerSampleApp> g_app;
-
-void TrackerSample_SetInfoDisplayMessage(std::string key, std::string value)
-{
-  boost::shared_ptr<OrthancStone::TrackerSampleApp> app = g_app.lock();
-  if (app)
-  {
-    app->SetInfoDisplayMessage(key, value);
-  }
-}
-
-/**
- * IMPORTANT: The full arguments to "main()" are needed for SDL on
- * Windows. Otherwise, one gets the linking error "undefined reference
- * to `SDL_main'". https://wiki.libsdl.org/FAQWindows
- **/
-int main(int argc, char* argv[])
-{
-  using namespace OrthancStone;
-
-  StoneInitialize();
-  Orthanc::Logging::EnableInfoLevel(true);
-//  Orthanc::Logging::EnableTraceLevel(true);
-
-  try
-  {
-    MessageBroker broker;
-    boost::shared_ptr<TrackerSampleApp> app(new TrackerSampleApp(broker));
-    g_app = app;
-    app->PrepareScene();
-    app->Run();
-  }
-  catch (Orthanc::OrthancException& e)
-  {
-    LOG(ERROR) << "EXCEPTION: " << e.What();
-  }
-
-  StoneFinalize();
-
-  return 0;
-}
-
-
--- a/Samples/Sdl/TrackerSampleApp.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,733 +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 "TrackerSampleApp.h"
-
-#include "../../Framework/OpenGL/SdlOpenGLContext.h"
-#include "../../Framework/Scene2D/CairoCompositor.h"
-#include "../../Framework/Scene2D/ColorTextureSceneLayer.h"
-#include "../../Framework/Scene2D/OpenGLCompositor.h"
-#include "../../Framework/Scene2D/PanSceneTracker.h"
-#include "../../Framework/Scene2D/RotateSceneTracker.h"
-#include "../../Framework/Scene2D/Scene2D.h"
-#include "../../Framework/Scene2D/ZoomSceneTracker.h"
-#include "../../Framework/Scene2DViewport/UndoStack.h"
-#include "../../Framework/Scene2DViewport/CreateAngleMeasureTracker.h"
-#include "../../Framework/Scene2DViewport/CreateLineMeasureTracker.h"
-#include "../../Framework/StoneInitialization.h"
-
-// From Orthanc framework
-#include <Core/Logging.h>
-#include <Core/OrthancException.h>
-#include <Core/Images/Image.h>
-#include <Core/Images/ImageProcessing.h>
-#include <Core/Images/PngWriter.h>
-
-#include <boost/ref.hpp>
-#include <boost/make_shared.hpp>
-#include <SDL.h>
-
-#include <stdio.h>
-
-namespace OrthancStone
-{
-  const char* MeasureToolToString(size_t i)
-  {
-    static const char* descs[] = {
-      "GuiTool_Rotate",
-      "GuiTool_Pan",
-      "GuiTool_Zoom",
-      "GuiTool_LineMeasure",
-      "GuiTool_CircleMeasure",
-      "GuiTool_AngleMeasure",
-      "GuiTool_EllipseMeasure",
-      "GuiTool_LAST"
-    };
-    if (i >= GuiTool_LAST)
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "Wrong tool index");
-    }
-    return descs[i];
-  }
-
-  void TrackerSampleApp::SelectNextTool()
-  {
-    currentTool_ = static_cast<GuiTool>(currentTool_ + 1);
-    if (currentTool_ == GuiTool_LAST)
-      currentTool_ = static_cast<GuiTool>(0);;
-    printf("Current tool is now: %s\n", MeasureToolToString(currentTool_));
-  }
-
-  void TrackerSampleApp::DisplayInfoText()
-  {
-    // do not try to use stuff too early!
-    std::stringstream msg;
-	
-	for (std::map<std::string, std::string>::const_iterator kv = infoTextMap_.begin();
-		kv != infoTextMap_.end(); ++kv)
-    {
-      msg << kv->first << " : " << kv->second << std::endl;
-    }
-	std::string msgS = msg.str();
-
-    TextSceneLayer* layerP = NULL;
-    if (controller_->GetScene().HasLayer(FIXED_INFOTEXT_LAYER_ZINDEX))
-    {
-      TextSceneLayer& layer = dynamic_cast<TextSceneLayer&>(
-        controller_->GetScene().GetLayer(FIXED_INFOTEXT_LAYER_ZINDEX));
-      layerP = &layer;
-    }
-    else
-    {
-      std::unique_ptr<TextSceneLayer> layer(new TextSceneLayer);
-      layerP = layer.get();
-      layer->SetColor(0, 255, 0);
-      layer->SetFontIndex(1);
-      layer->SetBorder(20);
-      layer->SetAnchor(BitmapAnchor_TopLeft);
-      //layer->SetPosition(0,0);
-      controller_->GetScene().SetLayer(FIXED_INFOTEXT_LAYER_ZINDEX, layer.release());
-    }
-    // position the fixed info text in the upper right corner
-    layerP->SetText(msgS.c_str());
-    double cX = GetCompositor().GetCanvasWidth() * (-0.5);
-    double cY = GetCompositor().GetCanvasHeight() * (-0.5);
-    controller_->GetScene().GetCanvasToSceneTransform().Apply(cX,cY);
-    layerP->SetPosition(cX, cY);
-  }
-
-  void TrackerSampleApp::DisplayFloatingCtrlInfoText(const PointerEvent& e)
-  {
-    ScenePoint2D p = e.GetMainPosition().Apply(controller_->GetScene().GetCanvasToSceneTransform());
-
-    char buf[128];
-    sprintf(buf, "S:(%0.02f,%0.02f) C:(%0.02f,%0.02f)", 
-      p.GetX(), p.GetY(), 
-      e.GetMainPosition().GetX(), e.GetMainPosition().GetY());
-
-    if (controller_->GetScene().HasLayer(FLOATING_INFOTEXT_LAYER_ZINDEX))
-    {
-      TextSceneLayer& layer =
-        dynamic_cast<TextSceneLayer&>(controller_->GetScene().GetLayer(FLOATING_INFOTEXT_LAYER_ZINDEX));
-      layer.SetText(buf);
-      layer.SetPosition(p.GetX(), p.GetY());
-    }
-    else
-    {
-      std::unique_ptr<TextSceneLayer> layer(new TextSceneLayer);
-      layer->SetColor(0, 255, 0);
-      layer->SetText(buf);
-      layer->SetBorder(20);
-      layer->SetAnchor(BitmapAnchor_BottomCenter);
-      layer->SetPosition(p.GetX(), p.GetY());
-      controller_->GetScene().SetLayer(FLOATING_INFOTEXT_LAYER_ZINDEX, layer.release());
-    }
-  }
-
-  void TrackerSampleApp::HideInfoText()
-  {
-    controller_->GetScene().DeleteLayer(FLOATING_INFOTEXT_LAYER_ZINDEX);
-  }
-
-  ScenePoint2D TrackerSampleApp::GetRandomPointInScene() const
-  {
-    unsigned int w = GetCompositor().GetCanvasWidth();
-    LOG(TRACE) << "GetCompositor().GetCanvasWidth() = " << 
-      GetCompositor().GetCanvasWidth();
-    unsigned int h = GetCompositor().GetCanvasHeight();
-    LOG(TRACE) << "GetCompositor().GetCanvasHeight() = " << 
-      GetCompositor().GetCanvasHeight();
-
-    if ((w >= RAND_MAX) || (h >= RAND_MAX))
-      LOG(WARNING) << "Canvas is too big : tools will not be randomly placed";
-
-    int x = rand() % w;
-    int y = rand() % h;
-    LOG(TRACE) << "random x = " << x << "random y = " << y;
-
-    ScenePoint2D p = controller_->GetViewport().GetPixelCenterCoordinates(x, y);
-    LOG(TRACE) << "--> p.GetX() = " << p.GetX() << " p.GetY() = " << p.GetY();
-
-    ScenePoint2D r = p.Apply(controller_->GetScene().GetCanvasToSceneTransform());
-    LOG(TRACE) << "--> r.GetX() = " << r.GetX() << " r.GetY() = " << r.GetY();
-    return r;
-  }
-
-  void TrackerSampleApp::CreateRandomMeasureTool()
-  {
-    static bool srandCalled = false;
-    if (!srandCalled)
-    {
-      srand(42);
-      srandCalled = true;
-    }
-
-    int i = rand() % 2;
-    LOG(TRACE) << "random i = " << i;
-    switch (i)
-    {
-    case 0:
-      // line measure
-      {
-        boost::shared_ptr<CreateLineMeasureCommand> cmd = 
-          boost::make_shared<CreateLineMeasureCommand>(
-		  boost::ref(IObserver::GetBroker()),
-            controller_,
-            GetRandomPointInScene());
-        cmd->SetEnd(GetRandomPointInScene());
-        controller_->PushCommand(cmd);
-      }
-      break;
-    case 1:
-      // angle measure
-      {
-      boost::shared_ptr<CreateAngleMeasureCommand> cmd =
-        boost::make_shared<CreateAngleMeasureCommand>(
-          boost::ref(IObserver::GetBroker()),
-          controller_,
-          GetRandomPointInScene());
-        cmd->SetCenter(GetRandomPointInScene());
-        cmd->SetSide2End(GetRandomPointInScene());
-        controller_->PushCommand(cmd);
-      }
-      break;
-    }
-  }
-
-  void TrackerSampleApp::HandleApplicationEvent(
-    const SDL_Event & event)
-  {
-    DisplayInfoText();
-
-    if (event.type == SDL_MOUSEMOTION)
-    {
-      int scancodeCount = 0;
-      const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount);
-
-      if (activeTracker_.get() == NULL &&
-        SDL_SCANCODE_LALT < scancodeCount &&
-        keyboardState[SDL_SCANCODE_LALT])
-      {
-        // The "left-ctrl" key is down, while no tracker is present
-        // Let's display the info text
-        PointerEvent e;
-        e.AddPosition(controller_->GetViewport().GetPixelCenterCoordinates(
-          event.button.x, event.button.y));
-
-        DisplayFloatingCtrlInfoText(e);
-      }
-      else if (activeTracker_.get() != NULL)
-      {
-        HideInfoText();
-        //LOG(TRACE) << "(event.type == SDL_MOUSEMOTION)";
-        if (activeTracker_.get() != NULL)
-        {
-          //LOG(TRACE) << "(activeTracker_.get() != NULL)";
-          PointerEvent e;
-          e.AddPosition(controller_->GetViewport().GetPixelCenterCoordinates(
-            event.button.x, event.button.y));
-          
-          //LOG(TRACE) << "event.button.x = " << event.button.x << "     " <<
-          //  "event.button.y = " << event.button.y;
-          LOG(TRACE) << "activeTracker_->PointerMove(e); " <<
-            e.GetMainPosition().GetX() << " " << e.GetMainPosition().GetY();
-          
-          activeTracker_->PointerMove(e);
-          if (!activeTracker_->IsAlive())
-            activeTracker_.reset();
-        }
-      }
-      else
-      {
-        HideInfoText();
-
-        PointerEvent e;
-        e.AddPosition(controller_->GetViewport().GetPixelCenterCoordinates(event.button.x, event.button.y));
-
-        ScenePoint2D scenePos = e.GetMainPosition().Apply(
-          controller_->GetScene().GetCanvasToSceneTransform());
-        //auto measureTools = GetController()->HitTestMeasureTools(scenePos);
-        //LOG(TRACE) << "# of hit tests: " << measureTools.size();
-        
-        // this returns the collection of measuring tools where hit test is true
-        std::vector<boost::shared_ptr<MeasureTool> > measureTools = controller_->HitTestMeasureTools(scenePos);
-
-        // let's refresh the measuring tools highlighted state
-        // first let's tag them as "unhighlighted"
-        controller_->ResetMeasuringToolsHighlight();
-
-        // then immediately take the first one and ask it to highlight the 
-        // measuring tool UI part that is hot
-        if (measureTools.size() > 0)
-        {
-          measureTools[0]->Highlight(scenePos);
-        }
-      }
-    }
-    else if (event.type == SDL_MOUSEBUTTONUP)
-    {
-      if (activeTracker_)
-      {
-        PointerEvent e;
-        e.AddPosition(controller_->GetViewport().GetPixelCenterCoordinates(event.button.x, event.button.y));
-        activeTracker_->PointerUp(e);
-        if (!activeTracker_->IsAlive())
-          activeTracker_.reset();
-      }
-    }
-    else if (event.type == SDL_MOUSEBUTTONDOWN)
-    {
-      PointerEvent e;
-      e.AddPosition(controller_->GetViewport().GetPixelCenterCoordinates(
-        event.button.x, event.button.y));
-      if (activeTracker_)
-      {
-        activeTracker_->PointerDown(e);
-        if (!activeTracker_->IsAlive())
-          activeTracker_.reset();
-      }
-      else
-      {
-        // we ATTEMPT to create a tracker if need be
-        activeTracker_ = CreateSuitableTracker(event, e);
-      }
-    }
-    else if (event.type == SDL_KEYDOWN &&
-      event.key.repeat == 0 /* Ignore key bounce */)
-    {
-      switch (event.key.keysym.sym)
-      {
-      case SDLK_ESCAPE:
-        if (activeTracker_)
-        {
-          activeTracker_->Cancel();
-          if (!activeTracker_->IsAlive())
-            activeTracker_.reset();
-        }
-        break;
-
-      case SDLK_t:
-        if (!activeTracker_)
-          SelectNextTool();
-        else
-        {
-          LOG(WARNING) << "You cannot change the active tool when an interaction"
-            " is taking place";
-        }
-        break;
-
-      case SDLK_m:
-        CreateRandomMeasureTool();
-        break;
-      case SDLK_s:
-        controller_->FitContent(GetCompositor().GetCanvasWidth(),
-          GetCompositor().GetCanvasHeight());
-        break;
-
-      case SDLK_z:
-        LOG(TRACE) << "SDLK_z has been pressed. event.key.keysym.mod == " << event.key.keysym.mod;
-        if (event.key.keysym.mod & KMOD_CTRL)
-        {
-          if (controller_->CanUndo())
-          {
-            LOG(TRACE) << "Undoing...";
-            controller_->Undo();
-          }
-          else
-          {
-            LOG(WARNING) << "Nothing to undo!!!";
-          }
-        }
-        break;
-
-      case SDLK_y:
-        LOG(TRACE) << "SDLK_y has been pressed. event.key.keysym.mod == " << event.key.keysym.mod;
-        if (event.key.keysym.mod & KMOD_CTRL)
-        {
-          if (controller_->CanRedo())
-          {
-            LOG(TRACE) << "Redoing...";
-            controller_->Redo();
-          }
-          else
-          {
-            LOG(WARNING) << "Nothing to redo!!!";
-          }
-        }
-        break;
-
-      case SDLK_c:
-        TakeScreenshot(
-          "screenshot.png",
-          GetCompositor().GetCanvasWidth(),
-          GetCompositor().GetCanvasHeight());
-        break;
-
-      default:
-        break;
-      }
-    }
-  }
-
-
-  void TrackerSampleApp::OnSceneTransformChanged(
-    const ViewportController::SceneTransformChanged& message)
-  {
-    DisplayInfoText();
-  }
-
-  boost::shared_ptr<IFlexiblePointerTracker> TrackerSampleApp::CreateSuitableTracker(
-    const SDL_Event & event,
-    const PointerEvent & e)
-  {
-    using namespace Orthanc;
-
-    switch (event.button.button)
-    {
-    case SDL_BUTTON_MIDDLE:
-      return boost::shared_ptr<IFlexiblePointerTracker>(new PanSceneTracker
-        (controller_, e));
-
-    case SDL_BUTTON_RIGHT:
-      return boost::shared_ptr<IFlexiblePointerTracker>(new ZoomSceneTracker
-        (controller_, e, GetCompositor().GetCanvasHeight()));
-
-    case SDL_BUTTON_LEFT:
-    {
-      //LOG(TRACE) << "CreateSuitableTracker: case SDL_BUTTON_LEFT:";
-      // TODO: we need to iterate on the set of measuring tool and perform
-      // a hit test to check if a tracker needs to be created for edition.
-      // Otherwise, depending upon the active tool, we might want to create
-      // a "measuring tool creation" tracker
-
-      // TODO: if there are conflicts, we should prefer a tracker that 
-      // pertains to the type of measuring tool currently selected (TBD?)
-      boost::shared_ptr<IFlexiblePointerTracker> hitTestTracker = TrackerHitTest(e);
-
-      if (hitTestTracker != NULL)
-      {
-        //LOG(TRACE) << "hitTestTracker != NULL";
-        return hitTestTracker;
-      }
-      else
-      {
-        switch (currentTool_)
-        {
-        case GuiTool_Rotate:
-          //LOG(TRACE) << "Creating RotateSceneTracker";
-          return boost::shared_ptr<IFlexiblePointerTracker>(new RotateSceneTracker(
-            controller_, e));
-        case GuiTool_Pan:
-          return boost::shared_ptr<IFlexiblePointerTracker>(new PanSceneTracker(
-            controller_, e));
-        case GuiTool_Zoom:
-          return boost::shared_ptr<IFlexiblePointerTracker>(new ZoomSceneTracker(
-            controller_, e, GetCompositor().GetCanvasHeight()));
-        //case GuiTool_AngleMeasure:
-        //  return new AngleMeasureTracker(GetScene(), e);
-        //case GuiTool_CircleMeasure:
-        //  return new CircleMeasureTracker(GetScene(), e);
-        //case GuiTool_EllipseMeasure:
-        //  return new EllipseMeasureTracker(GetScene(), e);
-        case GuiTool_LineMeasure:
-          return boost::shared_ptr<IFlexiblePointerTracker>(new CreateLineMeasureTracker(
-            IObserver::GetBroker(), controller_, e));
-        case GuiTool_AngleMeasure:
-          return boost::shared_ptr<IFlexiblePointerTracker>(new CreateAngleMeasureTracker(
-            IObserver::GetBroker(), controller_, e));
-        case GuiTool_CircleMeasure:
-          LOG(ERROR) << "Not implemented yet!";
-          return boost::shared_ptr<IFlexiblePointerTracker>();
-        case GuiTool_EllipseMeasure:
-          LOG(ERROR) << "Not implemented yet!";
-          return boost::shared_ptr<IFlexiblePointerTracker>();
-        default:
-          throw OrthancException(ErrorCode_InternalError, "Wrong tool!");
-        }
-      }
-    }
-    default:
-      return boost::shared_ptr<IFlexiblePointerTracker>();
-    }
-  }
-
-
-  TrackerSampleApp::TrackerSampleApp(MessageBroker& broker) : IObserver(broker)
-    , currentTool_(GuiTool_Rotate)
-    , undoStack_(new UndoStack)
-    , viewport_("Hello", 1024, 1024, false) // False means we do NOT let Windows treat this as a legacy application that needs to be scaled
-  {
-    controller_ = boost::shared_ptr<ViewportController>(
-      new ViewportController(undoStack_, broker, viewport_));
-
-    controller_->RegisterObserverCallback(
-      new Callable<TrackerSampleApp, ViewportController::SceneTransformChanged>
-      (*this, &TrackerSampleApp::OnSceneTransformChanged));
-
-    TEXTURE_2x2_1_ZINDEX = 1;
-    TEXTURE_1x1_ZINDEX = 2;
-    TEXTURE_2x2_2_ZINDEX = 3;
-    LINESET_1_ZINDEX = 4;
-    LINESET_2_ZINDEX = 5;
-    FLOATING_INFOTEXT_LAYER_ZINDEX = 6;
-    FIXED_INFOTEXT_LAYER_ZINDEX = 7;
-  }
-
-  void TrackerSampleApp::PrepareScene()
-  {
-    // Texture of 2x2 size
-    {
-      Orthanc::Image i(Orthanc::PixelFormat_RGB24, 2, 2, false);
-
-      uint8_t* p = reinterpret_cast<uint8_t*>(i.GetRow(0));
-      p[0] = 255;
-      p[1] = 0;
-      p[2] = 0;
-
-      p[3] = 0;
-      p[4] = 255;
-      p[5] = 0;
-
-      p = reinterpret_cast<uint8_t*>(i.GetRow(1));
-      p[0] = 0;
-      p[1] = 0;
-      p[2] = 255;
-
-      p[3] = 255;
-      p[4] = 0;
-      p[5] = 0;
-
-      controller_->GetScene().SetLayer(TEXTURE_2x2_1_ZINDEX, new ColorTextureSceneLayer(i));
-
-      std::unique_ptr<ColorTextureSceneLayer> l(new ColorTextureSceneLayer(i));
-      l->SetOrigin(-3, 2);
-      l->SetPixelSpacing(1.5, 1);
-      l->SetAngle(20.0 / 180.0 * M_PI);
-      controller_->GetScene().SetLayer(TEXTURE_2x2_2_ZINDEX, l.release());
-    }
-
-    // Texture of 1x1 size
-    {
-      Orthanc::Image i(Orthanc::PixelFormat_RGB24, 1, 1, false);
-
-      uint8_t* p = reinterpret_cast<uint8_t*>(i.GetRow(0));
-      p[0] = 255;
-      p[1] = 0;
-      p[2] = 0;
-
-      std::unique_ptr<ColorTextureSceneLayer> l(new ColorTextureSceneLayer(i));
-      l->SetOrigin(-2, 1);
-      l->SetAngle(20.0 / 180.0 * M_PI);
-      controller_->GetScene().SetLayer(TEXTURE_1x1_ZINDEX, l.release());
-    }
-
-    // Some lines
-    {
-      std::unique_ptr<PolylineSceneLayer> layer(new PolylineSceneLayer);
-
-      layer->SetThickness(1);
-
-      PolylineSceneLayer::Chain chain;
-      chain.push_back(ScenePoint2D(0 - 0.5, 0 - 0.5));
-      chain.push_back(ScenePoint2D(0 - 0.5, 2 - 0.5));
-      chain.push_back(ScenePoint2D(2 - 0.5, 2 - 0.5));
-      chain.push_back(ScenePoint2D(2 - 0.5, 0 - 0.5));
-      layer->AddChain(chain, true, 255, 0, 0);
-
-      chain.clear();
-      chain.push_back(ScenePoint2D(-5, -5));
-      chain.push_back(ScenePoint2D(5, -5));
-      chain.push_back(ScenePoint2D(5, 5));
-      chain.push_back(ScenePoint2D(-5, 5));
-      layer->AddChain(chain, true, 0, 255, 0);
-
-      double dy = 1.01;
-      chain.clear();
-      chain.push_back(ScenePoint2D(-4, -4));
-      chain.push_back(ScenePoint2D(4, -4 + dy));
-      chain.push_back(ScenePoint2D(-4, -4 + 2.0 * dy));
-      chain.push_back(ScenePoint2D(4, 2));
-      layer->AddChain(chain, false, 0, 0, 255);
-
-      controller_->GetScene().SetLayer(LINESET_1_ZINDEX, layer.release());
-    }
-
-    // Some text
-    {
-      std::unique_ptr<TextSceneLayer> layer(new TextSceneLayer);
-      layer->SetText("Hello");
-      controller_->GetScene().SetLayer(LINESET_2_ZINDEX, layer.release());
-    }
-  }
-
-
-  void TrackerSampleApp::DisableTracker()
-  {
-    if (activeTracker_)
-    {
-      activeTracker_->Cancel();
-      activeTracker_.reset();
-    }
-  }
-
-  void TrackerSampleApp::TakeScreenshot(const std::string& target,
-    unsigned int canvasWidth,
-    unsigned int canvasHeight)
-  {
-    CairoCompositor compositor(controller_->GetScene(), canvasWidth, canvasHeight);
-    compositor.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, FONT_SIZE_0, Orthanc::Encoding_Latin1);
-    compositor.Refresh();
-
-    Orthanc::ImageAccessor canvas;
-    compositor.GetCanvas().GetReadOnlyAccessor(canvas);
-
-    Orthanc::Image png(Orthanc::PixelFormat_RGB24, canvas.GetWidth(), canvas.GetHeight(), false);
-    Orthanc::ImageProcessing::Convert(png, canvas);
-
-    Orthanc::PngWriter writer;
-    writer.WriteToFile(target, png);
-  }
-
-
-  boost::shared_ptr<IFlexiblePointerTracker> TrackerSampleApp::TrackerHitTest(const PointerEvent & e)
-  {
-    // std::vector<boost::shared_ptr<MeasureTool>> measureTools_;
-    ScenePoint2D scenePos = e.GetMainPosition().Apply(
-      controller_->GetScene().GetCanvasToSceneTransform());
-
-    std::vector<boost::shared_ptr<MeasureTool> > measureTools = controller_->HitTestMeasureTools(scenePos);
-
-    if (measureTools.size() > 0)
-    {
-      return measureTools[0]->CreateEditionTracker(e);
-    }
-    return boost::shared_ptr<IFlexiblePointerTracker>();
-  }
-
-  static void GLAPIENTRY
-    OpenGLMessageCallback(GLenum source,
-      GLenum type,
-      GLuint id,
-      GLenum severity,
-      GLsizei length,
-      const GLchar* message,
-      const void* userParam)
-  {
-    if (severity != GL_DEBUG_SEVERITY_NOTIFICATION)
-    {
-      fprintf(stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n",
-        (type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : ""),
-        type, severity, message);
-    }
-  }
-
-  static bool g_stopApplication = false;
-  
-  ICompositor& TrackerSampleApp::GetCompositor()
-  {
-    using namespace Orthanc;
-    try
-    {
-      SdlViewport& viewport = dynamic_cast<SdlViewport&>(viewport_);
-      return viewport.GetCompositor();
-    }
-    catch (std::bad_cast e)
-    {
-      throw OrthancException(ErrorCode_InternalError, "Wrong viewport type!");
-     }
-  }
-
-  const ICompositor& TrackerSampleApp::GetCompositor() const
-  {
-    using namespace Orthanc;
-    try
-    {
-      SdlViewport& viewport = const_cast<SdlViewport&>(dynamic_cast<const SdlViewport&>(viewport_));
-      return viewport.GetCompositor();
-    }
-    catch (std::bad_cast e)
-    {
-      throw OrthancException(ErrorCode_InternalError, "Wrong viewport type!");
-    }
-  }
-
-
-  void TrackerSampleApp::Run()
-  {
-    controller_->FitContent(viewport_.GetCanvasWidth(), viewport_.GetCanvasHeight());
-
-    glEnable(GL_DEBUG_OUTPUT);
-    glDebugMessageCallback(OpenGLMessageCallback, 0);
-
-    GetCompositor().SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT,
-      FONT_SIZE_0, Orthanc::Encoding_Latin1);
-    GetCompositor().SetFont(1, Orthanc::EmbeddedResources::UBUNTU_FONT,
-      FONT_SIZE_1, Orthanc::Encoding_Latin1);
-
-    while (!g_stopApplication)
-    {
-      GetCompositor().Refresh();
-
-      SDL_Event event;
-      while (!g_stopApplication && SDL_PollEvent(&event))
-      {
-        if (event.type == SDL_QUIT)
-        {
-          g_stopApplication = true;
-          break;
-        }
-        else if (event.type == SDL_WINDOWEVENT &&
-          event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
-        {
-          DisableTracker(); // was: tracker.reset(NULL);
-        }
-        else if (event.type == SDL_KEYDOWN &&
-          event.key.repeat == 0 /* Ignore key bounce */)
-        {
-          switch (event.key.keysym.sym)
-          {
-          case SDLK_f:
-            viewport_.GetWindow().ToggleMaximize();
-            break;
-
-          case SDLK_q:
-            g_stopApplication = true;
-            break;
-          default:
-            break;
-          }
-        }
-        HandleApplicationEvent(event);
-      }
-      SDL_Delay(1);
-    }
-  }
-
-  void TrackerSampleApp::SetInfoDisplayMessage(
-    std::string key, std::string value)
-  {
-    if (value == "")
-      infoTextMap_.erase(key);
-    else
-      infoTextMap_[key] = value;
-    DisplayInfoText();
-  }
-
-}
--- a/Samples/Sdl/TrackerSampleApp.h	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,148 +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 "../../Framework/Messages/IObserver.h"
-#include "../../Framework/Scene2D/OpenGLCompositor.h"
-#include "../../Framework/Scene2DViewport/IFlexiblePointerTracker.h"
-#include "../../Framework/Scene2DViewport/MeasureTool.h"
-#include "../../Framework/Scene2DViewport/PredeclaredTypes.h"
-#include "../../Framework/Scene2DViewport/ViewportController.h"
-#include "../../Framework/Viewport/SdlViewport.h"
-
-#include <SDL.h>
-
-#include <boost/make_shared.hpp>
-#include <boost/shared_ptr.hpp>
-#include <boost/enable_shared_from_this.hpp>
-
-namespace OrthancStone
-{
-  enum GuiTool
-  {
-    GuiTool_Rotate = 0,
-    GuiTool_Pan,
-    GuiTool_Zoom,
-    GuiTool_LineMeasure,
-    GuiTool_CircleMeasure,
-    GuiTool_AngleMeasure,
-    GuiTool_EllipseMeasure,
-    GuiTool_LAST
-  };
-
-  const char* MeasureToolToString(size_t i);
-
-  static const unsigned int FONT_SIZE_0 = 32;
-  static const unsigned int FONT_SIZE_1 = 24;
-
-  class Scene2D;
-  class UndoStack;
-
-  class TrackerSampleApp : public IObserver
-    , public boost::enable_shared_from_this<TrackerSampleApp>
-  {
-  public:
-    // 12 because.
-    TrackerSampleApp(MessageBroker& broker);
-    void PrepareScene();
-    void Run();
-    void SetInfoDisplayMessage(std::string key, std::string value);
-    void DisableTracker();
-
-    void HandleApplicationEvent(const SDL_Event& event);
-
-    /**
-    This method is called when the scene transform changes. It allows to
-    recompute the visual elements whose content depend upon the scene transform
-    */
-    void OnSceneTransformChanged(
-      const ViewportController::SceneTransformChanged& message);
-
-  private:
-    void SelectNextTool();
-    void CreateRandomMeasureTool();
-
-
-    /**
-    In the case of this app, the viewport is an SDL viewport and it has 
-    a OpenGLCompositor& GetCompositor() method
-    */
-    ICompositor& GetCompositor();
-
-    /**
-    See the other overload
-    */
-    const ICompositor& GetCompositor() const;
-
-    /**
-    This returns a random point in the canvas part of the scene, but in
-    scene coordinates
-    */
-    ScenePoint2D GetRandomPointInScene() const;
-
-    boost::shared_ptr<IFlexiblePointerTracker> TrackerHitTest(const PointerEvent& e);
-
-    boost::shared_ptr<IFlexiblePointerTracker> CreateSuitableTracker(
-      const SDL_Event& event,
-      const PointerEvent& e);
-
-    void TakeScreenshot(
-      const std::string& target,
-      unsigned int canvasWidth,
-      unsigned int canvasHeight);
-
-    /**
-      This adds the command at the top of the undo stack
-    */
-    void Commit(boost::shared_ptr<TrackerCommand> cmd);
-    void Undo();
-    void Redo();
-
-  private:
-    void DisplayFloatingCtrlInfoText(const PointerEvent& e);
-    void DisplayInfoText();
-    void HideInfoText();
-
-  private:
-    /**
-    WARNING: the measuring tools do store a reference to the scene, and it 
-    paramount that the scene gets destroyed AFTER the measurement tools.
-    */
-    boost::shared_ptr<ViewportController> controller_;
-
-    std::map<std::string, std::string> infoTextMap_;
-    boost::shared_ptr<IFlexiblePointerTracker> activeTracker_;
-
-    //static const int LAYER_POSITION = 150;
-
-    int TEXTURE_2x2_1_ZINDEX;
-    int TEXTURE_1x1_ZINDEX;
-    int TEXTURE_2x2_2_ZINDEX;
-    int LINESET_1_ZINDEX;
-    int LINESET_2_ZINDEX;
-    int FLOATING_INFOTEXT_LAYER_ZINDEX;
-    int FIXED_INFOTEXT_LAYER_ZINDEX;
-
-    GuiTool currentTool_;
-    boost::shared_ptr<UndoStack> undoStack_;
-    SdlOpenGLViewport viewport_;
-  };
-
-}
--- a/Samples/Sdl/cpp.hint	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2 +0,0 @@
-#define ORTHANC_OVERRIDE
-#define ORTHANC_STONE_DEFINE_EMPTY_MESSAGE(FILE, LINE, NAME) class NAME : public ::OrthancStone::IMessage {};
--- a/Samples/WebAssembly/BasicMPR.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,427 +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 "dev.h"
-
-#include <emscripten.h>
-
-#include "../../Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h"
-#include "../../Framework/Oracle/SleepOracleCommand.h"
-#include "../../Framework/Oracle/WebAssemblyOracle.h"
-#include "../../Framework/Scene2D/GrayscaleStyleConfigurator.h"
-#include "../../Framework/StoneInitialization.h"
-#include "../../Framework/Volumes/VolumeSceneLayerSource.h"
-
-
-namespace OrthancStone
-{
-  class VolumeSlicerWidget : public IObserver
-  {
-  private:
-    OrthancStone::WebAssemblyViewport      viewport_;
-    std::unique_ptr<VolumeSceneLayerSource>  source_;
-    VolumeProjection                       projection_;
-    std::vector<CoordinateSystem3D>        planes_;
-    size_t                                 currentPlane_;
-
-    void Handle(const DicomVolumeImage::GeometryReadyMessage& message)
-    {
-      LOG(INFO) << "Geometry is available";
-
-      const VolumeImageGeometry& geometry = message.GetOrigin().GetGeometry();
-
-      const unsigned int depth = geometry.GetProjectionDepth(projection_);
-      currentPlane_ = depth / 2;
-      
-      planes_.resize(depth);
-
-      for (unsigned int z = 0; z < depth; z++)
-      {
-        planes_[z] = geometry.GetProjectionSlice(projection_, z);
-      }
-
-      Refresh();
-
-      viewport_.FitContent();
-    }
-    
-  public:
-    VolumeSlicerWidget(MessageBroker& broker,
-                         const std::string& canvas,
-                         VolumeProjection projection) :
-      IObserver(broker),
-      viewport_(broker, canvas),
-      projection_(projection),
-      currentPlane_(0)
-    {
-    }
-
-    void UpdateSize()
-    {
-      viewport_.UpdateSize();
-    }
-
-    void SetSlicer(int layerDepth,
-                   const boost::shared_ptr<IVolumeSlicer>& slicer,
-                   IObservable& loader,
-                   ILayerStyleConfigurator* configurator)
-    {
-      if (source_.get() != NULL)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls,
-                                        "Only one slicer can be registered");
-      }
-      
-      loader.RegisterObserverCallback(
-        new Callable<VolumeSlicerWidget, DicomVolumeImage::GeometryReadyMessage>
-        (*this, &VolumeSlicerWidget::Handle));
-
-      source_.reset(new VolumeSceneLayerSource(viewport_.GetScene(), layerDepth, slicer));
-
-      if (configurator != NULL)
-      {
-        source_->SetConfigurator(configurator);
-      }
-    }    
-
-    void Refresh()
-    {
-      if (source_.get() != NULL &&
-          currentPlane_ < planes_.size())
-      {
-        source_->Update(planes_[currentPlane_]);
-        viewport_.Refresh();
-      }
-    }
-
-    size_t GetSlicesCount() const
-    {
-      return planes_.size();
-    }
-
-    void Scroll(int delta)
-    {
-      if (!planes_.empty())
-      {
-        int tmp = static_cast<int>(currentPlane_) + delta;
-        unsigned int next;
-
-        if (tmp < 0)
-        {
-          next = 0;
-        }
-        else if (tmp >= static_cast<int>(planes_.size()))
-        {
-          next = planes_.size() - 1;
-        }
-        else
-        {
-          next = static_cast<size_t>(tmp);
-        }
-
-        if (next != currentPlane_)
-        {
-          currentPlane_ = next;
-          Refresh();
-        }
-      }
-    }
-  };
-}
-
-
-
-
-boost::shared_ptr<OrthancStone::DicomVolumeImage>  ct_(new OrthancStone::DicomVolumeImage);
-
-boost::shared_ptr<OrthancStone::OrthancSeriesVolumeProgressiveLoader>  loader_;
-
-std::unique_ptr<OrthancStone::VolumeSlicerWidget>  widget1_;
-std::unique_ptr<OrthancStone::VolumeSlicerWidget>  widget2_;
-std::unique_ptr<OrthancStone::VolumeSlicerWidget>  widget3_;
-
-OrthancStone::MessageBroker  broker_;
-OrthancStone::WebAssemblyOracle  oracle_(broker_);
-
-
-EM_BOOL OnWindowResize(int eventType, const EmscriptenUiEvent *uiEvent, void *userData)
-{
-  try
-  {
-    if (widget1_.get() != NULL)
-    {
-      widget1_->UpdateSize();
-    }
-  
-    if (widget2_.get() != NULL)
-    {
-      widget2_->UpdateSize();
-    }
-  
-    if (widget3_.get() != NULL)
-    {
-      widget3_->UpdateSize();
-    }
-  }
-  catch (Orthanc::OrthancException& e)
-  {
-    LOG(ERROR) << "Exception while updating canvas size: " << e.What();
-  }
-  
-  return true;
-}
-
-
-
-
-EM_BOOL OnAnimationFrame(double time, void *userData)
-{
-  try
-  {
-    if (widget1_.get() != NULL)
-    {
-      widget1_->Refresh();
-    }
-  
-    if (widget2_.get() != NULL)
-    {
-      widget2_->Refresh();
-    }
-  
-    if (widget3_.get() != NULL)
-    {
-      widget3_->Refresh();
-    }
-
-    return true;
-  }
-  catch (Orthanc::OrthancException& e)
-  {
-    LOG(ERROR) << "Exception in the animation loop, stopping now: " << e.What();
-    return false;
-  }  
-}
-
-
-static bool ctrlDown_ = false;
-
-
-EM_BOOL OnMouseWheel(int eventType,
-                     const EmscriptenWheelEvent *wheelEvent,
-                     void *userData)
-{
-  try
-  {
-    if (userData != NULL)
-    {
-      int delta = 0;
-
-      if (wheelEvent->deltaY < 0)
-      {
-        delta = -1;
-      }
-           
-      if (wheelEvent->deltaY > 0)
-      {
-        delta = 1;
-      }
-
-      OrthancStone::VolumeSlicerWidget& widget =
-        *reinterpret_cast<OrthancStone::VolumeSlicerWidget*>(userData);
-      
-      if (ctrlDown_)
-      {
-        delta *= static_cast<int>(widget.GetSlicesCount() / 10);
-      }
-
-      widget.Scroll(delta);
-    }
-  }
-  catch (Orthanc::OrthancException& e)
-  {
-    LOG(ERROR) << "Exception in the wheel event: " << e.What();
-  }
-  
-  return true;
-}
-
-
-EM_BOOL OnKeyDown(int eventType,
-                  const EmscriptenKeyboardEvent *keyEvent,
-                  void *userData)
-{
-  ctrlDown_ = keyEvent->ctrlKey;
-  return false;
-}
-
-
-EM_BOOL OnKeyUp(int eventType,
-                const EmscriptenKeyboardEvent *keyEvent,
-                void *userData)
-{
-  ctrlDown_ = false;
-  return false;
-}
-
-
-
-
-namespace OrthancStone
-{
-  class TestSleep : public IObserver
-  {
-  private:
-    WebAssemblyOracle&  oracle_;
-
-    void Schedule()
-    {
-      oracle_.Schedule(*this, new OrthancStone::SleepOracleCommand(2000));
-    }
-    
-    void Handle(const SleepOracleCommand::TimeoutMessage& message)
-    {
-      LOG(INFO) << "TIMEOUT";
-      Schedule();
-    }
-    
-  public:
-    TestSleep(MessageBroker& broker,
-              WebAssemblyOracle& oracle) :
-      IObserver(broker),
-      oracle_(oracle)
-    {
-      oracle.RegisterObserverCallback(
-        new Callable<TestSleep, SleepOracleCommand::TimeoutMessage>
-        (*this, &TestSleep::Handle));
-
-      LOG(INFO) << "STARTING";
-      Schedule();
-    }
-  };
-
-  //static TestSleep testSleep(broker_, oracle_);
-}
-
-
-
-static std::map<std::string, std::string> arguments_;
-
-static bool GetArgument(std::string& value,
-                        const std::string& key)
-{
-  std::map<std::string, std::string>::const_iterator found = arguments_.find(key);
-
-  if (found == arguments_.end())
-  {
-    return false;
-  }
-  else
-  {
-    value = found->second;
-    return true;
-  }
-}
-
-
-extern "C"
-{
-  int main(int argc, char const *argv[]) 
-  {
-    OrthancStone::StoneInitialize();
-    Orthanc::Logging::EnableInfoLevel(true);
-    // Orthanc::Logging::EnableTraceLevel(true);
-    EM_ASM(window.dispatchEvent(new CustomEvent("WebAssemblyLoaded")););
-  }
-
-  EMSCRIPTEN_KEEPALIVE
-  void SetArgument(const char* key, const char* value)
-  {
-    // This is called for each GET argument (cf. "app.js")
-    LOG(INFO) << "Received GET argument: [" << key << "] = [" << value << "]";
-    arguments_[key] = value;
-  }
-
-  EMSCRIPTEN_KEEPALIVE
-  void Initialize()
-  {
-    try
-    {
-      oracle_.SetOrthancRoot("..");
-      
-      loader_.reset(new OrthancStone::OrthancSeriesVolumeProgressiveLoader(ct_, oracle_, oracle_));
-    
-      widget1_.reset(new OrthancStone::VolumeSlicerWidget(broker_, "mycanvas1", OrthancStone::VolumeProjection_Axial));
-      {
-        std::unique_ptr<OrthancStone::GrayscaleStyleConfigurator> style(new OrthancStone::GrayscaleStyleConfigurator);
-        style->SetLinearInterpolation(true);
-        style->SetWindowing(OrthancStone::ImageWindowing_Bone);
-        widget1_->SetSlicer(0, loader_, *loader_, style.release());
-      }
-      widget1_->UpdateSize();
-
-      widget2_.reset(new OrthancStone::VolumeSlicerWidget(broker_, "mycanvas2", OrthancStone::VolumeProjection_Coronal));
-      {
-        std::unique_ptr<OrthancStone::GrayscaleStyleConfigurator> style(new OrthancStone::GrayscaleStyleConfigurator);
-        style->SetLinearInterpolation(true);
-        style->SetWindowing(OrthancStone::ImageWindowing_Bone);
-        widget2_->SetSlicer(0, loader_, *loader_, style.release());
-      }
-      widget2_->UpdateSize();
-
-      widget3_.reset(new OrthancStone::VolumeSlicerWidget(broker_, "mycanvas3", OrthancStone::VolumeProjection_Sagittal));
-      {
-        std::unique_ptr<OrthancStone::GrayscaleStyleConfigurator> style(new OrthancStone::GrayscaleStyleConfigurator);
-        style->SetLinearInterpolation(true);
-        style->SetWindowing(OrthancStone::ImageWindowing_Bone);
-        widget3_->SetSlicer(0, loader_, *loader_, style.release());
-      }
-      widget3_->UpdateSize();
-
-      emscripten_set_resize_callback("#window", NULL, false, OnWindowResize);
-
-      emscripten_set_wheel_callback("mycanvas1", widget1_.get(), false, OnMouseWheel);
-      emscripten_set_wheel_callback("mycanvas2", widget2_.get(), false, OnMouseWheel);
-      emscripten_set_wheel_callback("mycanvas3", widget3_.get(), false, OnMouseWheel);
-
-      emscripten_set_keydown_callback("#window", NULL, false, OnKeyDown);
-      emscripten_set_keyup_callback("#window", NULL, false, OnKeyUp);
-    
-      emscripten_request_animation_frame_loop(OnAnimationFrame, NULL);
-
-
-      std::string ct;
-      if (GetArgument(ct, "ct"))
-      {
-        //loader_->LoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");
-        loader_->LoadSeries(ct);
-      }
-      else
-      {
-        LOG(ERROR) << "No Orthanc identifier for the CT series was provided";
-      }
-    }
-    catch (Orthanc::OrthancException& e)
-    {
-      LOG(ERROR) << "Exception during Initialize(): " << e.What();
-    }
-  }
-}
--- a/Samples/WebAssembly/BasicMPR.html	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,60 +0,0 @@
-<!doctype html>
-<html lang="en-us">
-  <head>
-    <meta charset="utf-8">
-    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-
-    <!-- Disable pinch zoom on mobile devices -->
-    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
-    <meta name="HandheldFriendly" content="true" />
-    
-    
-    <title>Stone of Orthanc</title>
-
-    <style>
-      html, body {
-      width: 100%;
-      height: 100%;
-      margin: 0px;
-      border: 0;
-      overflow: hidden; /*  Disable scrollbars */
-      display: block;  /* No floating content on sides */
-      }
-
-      #mycanvas1 {
-      position:absolute;
-      left:0%;
-      top:0%;
-      background-color: red;
-      width: 50%;
-      height: 100%;
-      }
-
-      #mycanvas2 {
-      position:absolute;
-      left:50%;
-      top:0%;
-      background-color: green;
-      width: 50%;
-      height: 50%;
-      }
-
-      #mycanvas3 {
-      position:absolute;
-      left:50%;
-      top:50%;
-      background-color: blue;
-      width: 50%;
-      height: 50%;
-      }
-    </style>
-  </head>
-  <body>
-    <canvas id="mycanvas1" oncontextmenu="return false;"></canvas>
-    <canvas id="mycanvas2" oncontextmenu="return false;"></canvas>
-    <canvas id="mycanvas3" oncontextmenu="return false;"></canvas>
-
-    <script type="text/javascript" src="app.js"></script>
-    <script type="text/javascript" async src="BasicMPR.js"></script>
-  </body>
-</html>
--- a/Samples/WebAssembly/BasicScene.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,210 +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 "dev.h"
-
-#include <emscripten.h>
-#include <emscripten/html5.h>
-
-// From Stone
-#include "../../Framework/Scene2D/ColorTextureSceneLayer.h"
-#include "../../Framework/StoneInitialization.h"
-
-// From Orthanc framework
-#include <Core/Images/Image.h>
-#include <Core/Logging.h>
-#include <Core/OrthancException.h>
-
-void PrepareScene(OrthancStone::Scene2D& scene)
-{
-  using namespace OrthancStone;
-
-  // Texture of 2x2 size
-  if (1)
-  {
-    Orthanc::Image i(Orthanc::PixelFormat_RGB24, 2, 2, false);
-    
-    uint8_t *p = reinterpret_cast<uint8_t*>(i.GetRow(0));
-    p[0] = 255;
-    p[1] = 0;
-    p[2] = 0;
-
-    p[3] = 0;
-    p[4] = 255;
-    p[5] = 0;
-
-    p = reinterpret_cast<uint8_t*>(i.GetRow(1));
-    p[0] = 0;
-    p[1] = 0;
-    p[2] = 255;
-
-    p[3] = 255;
-    p[4] = 0;
-    p[5] = 0;
-
-    scene.SetLayer(12, new ColorTextureSceneLayer(i));
-
-    std::unique_ptr<ColorTextureSceneLayer> l(new ColorTextureSceneLayer(i));
-    l->SetOrigin(-3, 2);
-    l->SetPixelSpacing(1.5, 1);
-    l->SetAngle(20.0 / 180.0 * M_PI);
-    scene.SetLayer(14, l.release());
-  }
-
-  // Texture of 1x1 size
-  if (1)
-  {
-    Orthanc::Image i(Orthanc::PixelFormat_RGB24, 1, 1, false);
-    
-    uint8_t *p = reinterpret_cast<uint8_t*>(i.GetRow(0));
-    p[0] = 255;
-    p[1] = 0;
-    p[2] = 0;
-
-    std::unique_ptr<ColorTextureSceneLayer> l(new ColorTextureSceneLayer(i));
-    l->SetOrigin(-2, 1);
-    l->SetAngle(20.0 / 180.0 * M_PI);
-    scene.SetLayer(13, l.release());
-  }
-
-  // Some lines
-  if (1)
-  {
-    std::unique_ptr<PolylineSceneLayer> layer(new PolylineSceneLayer);
-
-    layer->SetThickness(1);
-
-    PolylineSceneLayer::Chain chain;
-    chain.push_back(ScenePoint2D(0 - 0.5, 0 - 0.5));
-    chain.push_back(ScenePoint2D(0 - 0.5, 2 - 0.5));
-    chain.push_back(ScenePoint2D(2 - 0.5, 2 - 0.5));
-    chain.push_back(ScenePoint2D(2 - 0.5, 0 - 0.5));
-    layer->AddChain(chain, true, 255, 0, 0);
-
-    chain.clear();
-    chain.push_back(ScenePoint2D(-5, -5));
-    chain.push_back(ScenePoint2D(5, -5));
-    chain.push_back(ScenePoint2D(5, 5));
-    chain.push_back(ScenePoint2D(-5, 5));
-    layer->AddChain(chain, true, 0, 255, 0);
-
-    double dy = 1.01;
-    chain.clear();
-    chain.push_back(ScenePoint2D(-4, -4));
-    chain.push_back(ScenePoint2D(4, -4 + dy));
-    chain.push_back(ScenePoint2D(-4, -4 + 2.0 * dy));
-    chain.push_back(ScenePoint2D(4, 2));
-    layer->AddChain(chain, false, 0, 0, 255);
-
-    scene.SetLayer(50, layer.release());
-  }
-
-  // Some text
-  if (1)
-  {
-    std::unique_ptr<TextSceneLayer> layer(new TextSceneLayer);
-    layer->SetText("Hello");
-    scene.SetLayer(100, layer.release());
-  }
-}
-
-
-std::unique_ptr<OrthancStone::WebAssemblyViewport>  viewport1_;
-std::unique_ptr<OrthancStone::WebAssemblyViewport>  viewport2_;
-std::unique_ptr<OrthancStone::WebAssemblyViewport>  viewport3_;
-boost::shared_ptr<OrthancStone::ViewportController>   controller1_;
-boost::shared_ptr<OrthancStone::ViewportController>   controller2_;
-boost::shared_ptr<OrthancStone::ViewportController>   controller3_;
-OrthancStone::MessageBroker broker_;
-
-
-EM_BOOL OnWindowResize(
-  int eventType, const EmscriptenUiEvent *uiEvent, void *userData)
-{
-  if (viewport1_.get() != NULL)
-  {
-    viewport1_->UpdateSize();
-  }
-  
-  if (viewport2_.get() != NULL)
-  {
-    viewport2_->UpdateSize();
-  }
-  
-  if (viewport3_.get() != NULL)
-  {
-    viewport3_->UpdateSize();
-  }
-  
-  return true;
-}
-
-extern "C"
-{
-  int main(int argc, char const *argv[]) 
-  {
-    OrthancStone::StoneInitialize();
-    // Orthanc::Logging::EnableInfoLevel(true);
-    // Orthanc::Logging::EnableTraceLevel(true);
-    EM_ASM(window.dispatchEvent(new CustomEvent("WebAssemblyLoaded")););
-  }
-
-  EMSCRIPTEN_KEEPALIVE
-  void Initialize()
-  {
-    viewport1_.reset(new OrthancStone::WebAssemblyViewport("mycanvas1"));
-    PrepareScene(viewport1_->GetScene());
-    viewport1_->UpdateSize();
-
-    viewport2_.reset(new OrthancStone::WebAssemblyViewport("mycanvas2"));
-    PrepareScene(viewport2_->GetScene());
-    viewport2_->UpdateSize();
-
-    viewport3_.reset(new OrthancStone::WebAssemblyViewport("mycanvas3"));
-    PrepareScene(viewport3_->GetScene());
-    viewport3_->UpdateSize();
-
-    viewport1_->GetCompositor().SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, 
-                                        FONT_SIZE, Orthanc::Encoding_Latin1);
-    viewport2_->GetCompositor().SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, 
-                                        FONT_SIZE, Orthanc::Encoding_Latin1);
-    viewport3_->GetCompositor().SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, 
-                                        FONT_SIZE, Orthanc::Encoding_Latin1);
-
-    controller1_.reset(new OrthancStone::ViewportController(boost::make_shared<OrthancStone::UndoStack>(), broker_, *viewport1_));
-    controller2_.reset(new OrthancStone::ViewportController(boost::make_shared<OrthancStone::UndoStack>(), broker_, *viewport2_));
-    controller3_.reset(new OrthancStone::ViewportController(boost::make_shared<OrthancStone::UndoStack>(), broker_, *viewport3_));
-
-    controller1_->FitContent(viewport1_->GetCanvasWidth(), viewport1_->GetCanvasHeight());
-    controller2_->FitContent(viewport2_->GetCanvasWidth(), viewport2_->GetCanvasHeight());
-    controller3_->FitContent(viewport3_->GetCanvasWidth(), viewport3_->GetCanvasHeight());
-
-    viewport1_->Refresh();
-    viewport2_->Refresh();
-    viewport3_->Refresh();
-
-    SetupEvents("mycanvas1", controller1_);
-    SetupEvents("mycanvas2", controller2_);
-    SetupEvents("mycanvas3", controller3_);
-
-    emscripten_set_resize_callback("#window", NULL, false, OnWindowResize);
-  }
-}
--- a/Samples/WebAssembly/BasicScene.html	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,69 +0,0 @@
-<!doctype html>
-<html lang="en-us">
-  <head>
-    <meta charset="utf-8">
-    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-
-    <!-- Disable pinch zoom on mobile devices -->
-    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
-    <meta name="HandheldFriendly" content="true" />
-    
-    
-    <title>Stone of Orthanc</title>
-
-    <style>
-      html, body {
-      width: 100%;
-      height: 100%;
-      margin: 0px;
-      border: 0;
-      overflow: hidden; /*  Disable scrollbars */
-      display: block;  /* No floating content on sides */
-      }
-
-      #mycanvas1 {
-      position:absolute;
-      left:0%;
-      top:0%;
-      background-color: red;
-      width: 50%;
-      height: 100%;
-      }
-
-      #mycanvas2 {
-      position:absolute;
-      left:50%;
-      top:0%;
-      background-color: green;
-      width: 50%;
-      height: 50%;
-      }
-
-      #mycanvas3 {
-      position:absolute;
-      left:50%;
-      top:50%;
-      background-color: blue;
-      width: 50%;
-      height: 50%;
-      }
-    </style>
-  </head>
-  <body>
-    <canvas id="mycanvas1" oncontextmenu="return false;"></canvas>
-    <canvas id="mycanvas2" oncontextmenu="return false;"></canvas>
-    <canvas id="mycanvas3" oncontextmenu="return false;"></canvas>
-
-    <script type="text/javascript">
-      if (!('WebAssembly' in window)) {
-      alert('Sorry, your browser does not support WebAssembly :(');
-      } else {
-      window.addEventListener('WebAssemblyLoaded', function() {
-      Module.ccall('Initialize', null, null, null);
-      });
-      }
-    </script>
-
-    <script type="text/javascript" async src="BasicScene.js"></script>
-  </body>
-</html>
--- a/Samples/WebAssembly/CMakeLists.txt	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,119 +0,0 @@
-cmake_minimum_required(VERSION 2.8.3)
-
-
-#####################################################################
-## Configuration of the Emscripten compiler for WebAssembly target
-#####################################################################
-
-set(WASM_FLAGS "-s WASM=1 -s FETCH=1")
-
-set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WASM_FLAGS}")
-set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WASM_FLAGS}")
-set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\"]'")
-set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ERROR_ON_UNDEFINED_SYMBOLS=1")
-set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1")
-set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ASSERTIONS=1 -s DISABLE_EXCEPTION_CATCHING=0")
-#set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s EXIT_RUNTIME=1")
-
-#set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1")
-
-
-#####################################################################
-## Configuration of the Orthanc framework
-#####################################################################
-
-# This CMake file defines the "ORTHANC_STONE_VERSION" macro, so it
-# must be the first inclusion
-include(${CMAKE_SOURCE_DIR}/../../Resources/CMake/Version.cmake)
-
-if (ORTHANC_STONE_VERSION STREQUAL "mainline")
-  set(ORTHANC_FRAMEWORK_VERSION "mainline")
-  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
-else()
-  set(ORTHANC_FRAMEWORK_VERSION "1.5.7")
-  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
-endif()
-
-set(ORTHANC_FRAMEWORK_SOURCE "${ORTHANC_FRAMEWORK_DEFAULT_SOURCE}" CACHE STRING "Source of the Orthanc source code (can be \"hg\", \"archive\", \"web\" or \"path\")")
-set(ORTHANC_FRAMEWORK_ARCHIVE "" CACHE STRING "Path to the Orthanc archive, if ORTHANC_FRAMEWORK_SOURCE is \"archive\"")
-set(ORTHANC_FRAMEWORK_ROOT "" CACHE STRING "Path to the Orthanc source directory, if ORTHANC_FRAMEWORK_SOURCE is \"path\"")
-
-
-#####################################################################
-## Configuration of the Stone framework
-#####################################################################
-
-include(${CMAKE_SOURCE_DIR}/../../Resources/CMake/OrthancStoneParameters.cmake)
-include(${ORTHANC_ROOT}/Resources/CMake/DownloadPackage.cmake)
-
-DownloadPackage(
-  "a24b8136b8f3bb93f166baf97d9328de"
-  "http://orthanc.osimis.io/ThirdPartyDownloads/ubuntu-font-family-0.83.zip"
-  "${CMAKE_BINARY_DIR}/ubuntu-font-family-0.83")
-
-set(ORTHANC_STONE_APPLICATION_RESOURCES
-  UBUNTU_FONT  ${CMAKE_BINARY_DIR}/ubuntu-font-family-0.83/Ubuntu-R.ttf
-  )
-
-SET(ENABLE_GOOGLE_TEST OFF)
-SET(ENABLE_LOCALE ON)
-SET(ORTHANC_SANDBOXED ON)
-SET(ENABLE_WASM ON)
-
-include(${CMAKE_SOURCE_DIR}/../../Resources/CMake/OrthancStoneConfiguration.cmake)
-
-add_definitions(
-  -DORTHANC_ENABLE_LOGGING_PLUGIN=0
-  )
-
-
-#####################################################################
-## Build the samples
-#####################################################################
-
-add_library(OrthancStone STATIC
-  ${ORTHANC_STONE_SOURCES}
-  )
-
-
-if (ON)
-  add_executable(BasicScene
-    BasicScene.cpp
-    #${CMAKE_CURRENT_LIST_DIR}/../Shared/SharedBasicScene.h
-    ${CMAKE_CURRENT_LIST_DIR}/../Shared/SharedBasicScene.cpp
-    )
-
-  target_link_libraries(BasicScene OrthancStone)
-
-  install(
-    TARGETS BasicScene
-    RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}
-    )
-endif()
-
-
-if (ON)
-  add_executable(BasicMPR
-    BasicMPR.cpp
-    )
-
-  target_link_libraries(BasicMPR OrthancStone)
-
-  install(
-    TARGETS BasicMPR
-    RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}
-    )
-endif()
-  
-
-install(
-  FILES
-  ${CMAKE_CURRENT_BINARY_DIR}/BasicMPR.wasm
-  ${CMAKE_CURRENT_BINARY_DIR}/BasicScene.wasm
-  ${CMAKE_SOURCE_DIR}/BasicMPR.html
-  ${CMAKE_SOURCE_DIR}/BasicScene.html
-  ${CMAKE_SOURCE_DIR}/Configuration.json
-  ${CMAKE_SOURCE_DIR}/app.js
-  ${CMAKE_SOURCE_DIR}/index.html
-  DESTINATION ${CMAKE_INSTALL_PREFIX}
-  )
--- a/Samples/WebAssembly/Configuration.json	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,17 +0,0 @@
-{
-  "Plugins": [
-    "/usr/local/share/orthanc/plugins/libOrthancWebViewer.so",
-    "/usr/local/share/orthanc/plugins/libServeFolders.so"
-  ],
-  "StorageDirectory" : "/var/lib/orthanc/db",
-  "IndexDirectory" : "/var/lib/orthanc/db",
-  "RemoteAccessAllowed" : true,
-  "AuthenticationEnabled" : false,
-  "ServeFolders" : {
-    "AllowCache" : false,
-    "GenerateETag" : true,
-    "Folders" : {
-      "/stone" : "/root/stone"
-    }
-  }
-}
--- a/Samples/WebAssembly/ConfigurationLocalSJO.json	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-{
-  "Plugins": [
-    "/home/jodogne/Subversion/orthanc-webviewer/r/libOrthancWebViewer.so",
-    "/home/jodogne/Subversion/orthanc/r/libServeFolders.so"
-  ],
-  "StorageDirectory" : "/tmp/orthanc-db",
-  "IndexDirectory" : "/tmp/orthanc-db",
-  "RemoteAccessAllowed" : true,
-  "AuthenticationEnabled" : false,
-  "ServeFolders" : {
-    "AllowCache" : false,
-    "GenerateETag" : true,
-    "Folders" : {
-      "/stone" : "/tmp/stone"
-    }
-  },
-  "WebViewer" : {
-    "CachePath" : "/tmp/orthanc-db/WebViewerCache"
-  }
-}
--- a/Samples/WebAssembly/NOTES.txt	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,76 +0,0 @@
-Docker SJO
-==========
-
-$ source ~/Downloads/emsdk/emsdk_env.sh
-$ cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_BUILD_TYPE=Release -DALLOW_DOWNLOADS=ON .. -DCMAKE_INSTALL_PREFIX=/tmp/stone
-$ ninja install
-$ docker run -p 4242:4242 -p 8042:8042 --rm -v /tmp/stone:/root/stone:ro -v /tmp/stone-db/:/var/lib/orthanc/db/ jodogne/orthanc-plugins:latest /root/stone/Configuration.json --verbose
-
-WARNING: This won't work using "orthanc-plugins:1.5.6", as support for
-PAM is mandatatory in "/instances/.../image-uint16".
-
-
-Docker BGO
-==========
-
-On Ubuntu WSL
--------------
-. ~/apps/emsdk/emsdk_env.sh
-cd /mnt/c/osi/dev/
-mkdir -p build_stone_newsamples_wasm_wsl
-mkdir -p build_install_stone_newsamples_wasm_wsl
-cd build_stone_newsamples_wasm_wsl
-cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_BUILD_TYPE=Release -DALLOW_DOWNLOADS=ON /mnt/c/osi/dev/orthanc-stone/Samples/WebAssembly -DCMAKE_INSTALL_PREFIX=/mnt/c/osi/dev/build_install_stone_newsamples_wasm_wsl
-ninja install
-
-Then, on Windows
------------------
-docker run -p 4242:4242 -p 8042:8042 --rm -v "C:/osi/dev/build_install_stone_newsamples_wasm_wsl:/root/stone:ro" jodogne/orthanc-plugins:1.5.6 /root/stone/Configuration.json --verbose
-
-# WAIT A COUPLE OF SECS
-# if the archive has NOT already been unzipped, unzip it
-# upload dicom files to running orthanc
-
-cd C:\osi\dev\twiga-orthanc-viewer\demo\dicomfiles
-if (-not (test-path RTVIEWER-c8febcc6-eb9e22a4-130f208c-e0a6a4cd-4d432c57)) { unzip RTVIEWER-c8febcc6-eb9e22a4-130f208c-e0a6a4cd-4d432c57.zip}
-ImportDicomFiles.ps1 127.0.0.1 8042 .\RTVIEWER-c8febcc6-eb9e22a4-130f208c-e0a6a4cd-4d432c57\
-
---> localhost:8042 --> Plugins --> serve-folders --> stone --> ...
-
-Local BGO
-==========
-
-. ~/apps/emsdk/emsdk_env.sh
-cd /mnt/c/osi/dev/
-mkdir -p build_stone_newsamples_wasm_wsl
-mkdir -p build_install_stone_newsamples_wasm_wsl
-cd build_stone_newsamples_wasm_wsl
-cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_BUILD_TYPE=Release -DALLOW_DOWNLOADS=ON /mnt/c/osi/dev/orthanc-stone/Samples/WebAssembly -DCMAKE_INSTALL_PREFIX=/mnt/c/osi/dev/build_install_stone_newsamples_wasm_wsl
-
-
-
-TODO: Orthanc.exe 
-
-
-Local SJO
-==========
-
-$ source ~/Downloads/emsdk/emsdk_env.sh
-$ cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_BUILD_TYPE=Release -DALLOW_DOWNLOADS=ON .. -DCMAKE_INSTALL_PREFIX=/tmp/stone
-$ ninja install
-
-$ make -C ~/Subversion/orthanc/r -j4
-$ make -C ~/Subversion/orthanc-webviewer/r -j4
-$ ~/Subversion/orthanc/r/Orthanc ../ConfigurationLocalSJO.json
-
-
-Local AM
-========
-
-. ~/apps/emsdk/emsdk_env.sh
-cd /mnt/c/o/
-mkdir -p build_stone_newsamples_wasm_wsl
-mkdir -p build_install_stone_newsamples_wasm_wsl
-cd build_stone_newsamples_wasm_wsl
-cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=${EMSDK}/fastcomp/emscripten/cmake/Modules/Platform/Emscripten.cmake -DORTHANC_FRAMEWORK_SOURCE=path -DORTHANC_FRAMEWORK_ROOT=/mnt/c/o/orthanc/ -DCMAKE_BUILD_TYPE=Release -DALLOW_DOWNLOADS=ON /mnt/c/o/orthanc-stone/Samples/WebAssembly -DCMAKE_INSTALL_PREFIX=/mnt/c/o/build_install_stone_newsamples_wasm_wsl
-ninja
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/WebAssembly/SingleFrameViewer/CMakeLists.txt	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,73 @@
+cmake_minimum_required(VERSION 2.8.3)
+
+# Configuration of the Emscripten compiler for WebAssembly target
+# ---------------------------------------------------------------
+set(USE_WASM ON CACHE BOOL "")
+set(ORTHANC_FRAMEWORK_ROOT ${CMAKE_CURRENT_LIST_DIR}/../../../../orthanc CACHE STRING "")
+set(STONE_ROOT ${CMAKE_CURRENT_LIST_DIR}/../../../)
+
+set(EMSCRIPTEN_SET_LLVM_WASM_BACKEND ON CACHE BOOL "")
+
+set(WASM_FLAGS "-s WASM=1 -s FETCH=1")
+if (CMAKE_BUILD_TYPE STREQUAL "Debug")
+  set(WASM_FLAGS "${WASM_FLAGS} -s SAFE_HEAP=1")
+endif()
+
+set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WASM_FLAGS}")
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WASM_FLAGS}")
+
+set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\"]'")
+set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ERROR_ON_UNDEFINED_SYMBOLS=1")
+set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ASSERTIONS=1 -s DISABLE_EXCEPTION_CATCHING=0")
+set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1 -s TOTAL_MEMORY=268435456")  # 256MB + resize
+set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1")
+add_definitions(
+  -DDISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1
+)
+
+# Stone of Orthanc configuration
+# ---------------------------------------------------------------
+set(ALLOW_DOWNLOADS ON)
+set(ORTHANC_FRAMEWORK_SOURCE "path")
+
+include(${STONE_ROOT}/Resources/CMake/OrthancStoneParameters.cmake)
+
+SET(ENABLE_DCMTK ON)
+SET(ENABLE_GOOGLE_TEST OFF)
+SET(ENABLE_LOCALE ON)  # Necessary for text rendering
+SET(ENABLE_WASM ON)
+SET(ORTHANC_SANDBOXED ON)
+
+# this will set up the build system for Stone of Orthanc and will
+# populate the ORTHANC_STONE_SOURCES CMake variable
+include(${STONE_ROOT}/Resources/CMake/OrthancStoneConfiguration.cmake)
+
+include_directories(${STONE_ROOT})
+
+# Define the WASM module
+# ---------------------------------------------------------------
+add_executable(SingleFrameViewerWasm
+  SingleFrameViewer.cpp
+  ${ORTHANC_STONE_SOURCES}
+  )
+
+# Declare installation files for the module
+# ---------------------------------------------------------------
+install(
+  TARGETS SingleFrameViewerWasm
+  RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}
+  )
+
+# Declare installation files for the companion files (web scaffolding)
+# please note that ${CMAKE_CURRENT_BINARY_DIR}/SingleFrameViewerWasm.js 
+# (the generated JS loader for the WASM module) is handled by the `install1`
+# section above
+# ---------------------------------------------------------------
+install(
+  FILES
+  ${CMAKE_SOURCE_DIR}/SingleFrameViewerApp.js
+  ${CMAKE_SOURCE_DIR}/index.html
+  ${CMAKE_CURRENT_BINARY_DIR}/SingleFrameViewerWasm.wasm
+  ${CMAKE_SOURCE_DIR}/WasmWrapper.js
+  DESTINATION ${CMAKE_INSTALL_PREFIX}
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/WebAssembly/SingleFrameViewer/CMakeSettings.json	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,39 @@
+{
+  "configurations": [
+    {
+      "name": "wasm32-RelWithDebInfo",
+      "generator": "Ninja",
+      "configurationType": "RelWithDebInfo",
+      //"inheritEnvironments": [ "msvc_x64_x64" ],
+      "buildRoot": "${projectDir}\\out\\build\\${name}",
+      "installRoot": "${projectDir}\\out\\install\\${name}",
+      "cmakeCommandArgs": "",
+      "buildCommandArgs": "-v",
+      "ctestCommandArgs": "",
+      "cmakeToolchain": "C:/osi/emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake",
+      "intelliSenseMode": "windows-clang-x64",
+      "variables": [
+        {
+          "name": "CMAKE_BUILD_TYPE",
+          "value": "RelWithDebInfo",
+          "type": "STRING"
+        },
+        {
+          "name": "ALLOW_DOWNLOADS",
+          "value": "True",
+          "type": "BOOL"
+        },
+        {
+          "name": "STATIC_BUILD",
+          "value": "True",
+          "type": "BOOL"
+        },
+        {
+          "name": "OPENSSL_NO_CAPIENG",
+          "value": "True",
+          "type": "BOOL"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/WebAssembly/SingleFrameViewer/SingleFrameViewer.cpp	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,209 @@
+#include "SingleFrameViewerApplication.h"
+
+#include <Framework/Loaders/WebAssemblyLoadersContext.h>
+
+#include <Framework/StoneException.h>
+#include <Framework/StoneInitialization.h>
+
+#include <Core/Toolbox.h>
+
+#include <emscripten.h>
+#include <emscripten/html5.h>
+
+
+#define DISPATCH_JAVASCRIPT_EVENT(name)                         \
+  EM_ASM(                                                       \
+    const customEvent = document.createEvent("CustomEvent");    \
+    customEvent.initCustomEvent(name, false, false, undefined); \
+    window.dispatchEvent(customEvent);                          \
+    );
+
+#define EXTERN_CATCH_EXCEPTIONS                         \
+  catch (Orthanc::OrthancException& e)                  \
+  {                                                     \
+    LOG(ERROR) << "OrthancException: " << e.What();     \
+    DISPATCH_JAVASCRIPT_EVENT("StoneException");        \
+  }                                                     \
+  catch (OrthancStone::StoneException& e)               \
+  {                                                     \
+    LOG(ERROR) << "StoneException: " << e.What();       \
+    DISPATCH_JAVASCRIPT_EVENT("StoneException");        \
+  }                                                     \
+  catch (std::exception& e)                             \
+  {                                                     \
+    LOG(ERROR) << "Runtime error: " << e.what();        \
+    DISPATCH_JAVASCRIPT_EVENT("StoneException");        \
+  }                                                     \
+  catch (...)                                           \
+  {                                                     \
+    LOG(ERROR) << "Native exception";                   \
+    DISPATCH_JAVASCRIPT_EVENT("StoneException");        \
+  }
+
+
+
+namespace OrthancStone
+{
+  class Observer : public IWebViewerLoadersObserver
+  {
+  public:
+    virtual void SignalSeriesUpdated(LoadedDicomResources& series)
+    {
+      LOG(INFO) << "=====================================";
+      LOG(INFO) << series.GetSize() << " series";
+
+      /*for (size_t i = 0; i < series.GetSize(); i++)
+        {
+        series.GetResource(i).Print(stdout);
+        printf("\n");
+        }*/
+    }
+    
+    virtual void SignalThumbnailLoaded(const std::string& studyInstanceUid,
+                                       const std::string& seriesInstanceUid,
+                                       SeriesThumbnailType type)
+    {
+      LOG(INFO) << "*** Thumbnail loaded: " << studyInstanceUid << " / "
+                << seriesInstanceUid << " (type " << type << ")";
+    }
+  };
+}
+
+
+
+static std::auto_ptr<OrthancStone::WebAssemblyLoadersContext>  context_;
+static boost::shared_ptr<OrthancStone::Application>  application_;
+
+
+extern "C"
+{
+  int main(int argc, char const *argv[]) 
+  {
+    try
+    {
+      Orthanc::Logging::Initialize();
+      Orthanc::Logging::EnableInfoLevel(true);
+      //Orthanc::Logging::EnableTraceLevel(true);
+      LOG(WARNING) << "Initializing native Stone";
+
+      LOG(WARNING) << "Compiled with Emscripten " << __EMSCRIPTEN_major__
+                   << "." << __EMSCRIPTEN_minor__
+                   << "." << __EMSCRIPTEN_tiny__;
+
+      LOG(INFO) << "Endianness: " << Orthanc::EnumerationToString(Orthanc::Toolbox::DetectEndianness());
+      context_.reset(new OrthancStone::WebAssemblyLoadersContext(1, 4, 1));
+      context_->SetLocalOrthanc("..");
+      context_->SetDicomCacheSize(128 * 1024 * 1024);  // 128MB
+  
+      DISPATCH_JAVASCRIPT_EVENT("StoneInitialized");
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+
+    return 0;
+  }
+
+  
+  EMSCRIPTEN_KEEPALIVE
+  void InitializeViewport(const char* canvasId)
+  {
+    try
+    {
+      if (context_.get() == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls,
+                                        "The loaders context is not available yet");
+      }
+      
+      if (application_.get() != NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls,
+                                        "Only one single viewport is available for this application");
+      }
+
+      {
+        std::auto_ptr<OrthancStone::Observer> observer(new OrthancStone::Observer);
+      
+#if 1
+        OrthancStone::DicomSource source1;
+        //source1.SetDicomWebSource("http://localhost:8042/dicom-web");  source1.SetDicomWebRendered(true);
+        source1.SetDicomWebThroughOrthancSource("self");  source1.SetDicomWebRendered(true);
+        //source1.SetDicomWebThroughOrthancSource("my-google");  source1.SetDicomWebRendered(false);
+        boost::shared_ptr<OrthancStone::WebViewerLoaders> app2(
+          OrthancStone::WebViewerLoaders::Create(*context_, source1, true, observer.release()));
+#else
+        OrthancStone::DicomSource source1;
+        source1.SetOrthancSource();
+        boost::shared_ptr<OrthancStone::WebViewerLoaders> app2(
+          OrthancStone::WebViewerLoaders::Create(*context_, source1, true, observer.release()));
+        //app2->AddOrthancStudy("27f7126f-4f66fb14-03f4081b-f9341db2-53925988");
+        //app2->AddOrthancSeries("1e2c125c-411b8e86-3f4fe68e-a7584dd3-c6da78f0");
+#endif
+
+        // BRAINIX
+        //app2->AddDicomAllSeries();
+        //app2->AddDicomStudy("2.16.840.1.113669.632.20.1211.10000357775");
+        app2->AddDicomSeries("2.16.840.1.113669.632.20.1211.10000357775", "1.3.46.670589.11.0.0.11.4.2.0.8743.5.3800.2006120117110979000"); // Standard image: type 5
+
+        app2->AddDicomStudy("1.3.51.0.7.633920140505.6339234439.633987.633918098");  // "Video" type 4: video720p.dcm
+        app2->AddDicomStudy("1.2.276.0.7230010.3.1.2.2344313775.14992.1458058404.7528");  // "PDF" type 3: pdf.dcm  
+        app2->AddDicomSeries("1.2.276.0.7230010.3.1.2.296485376.1.1568899779.944131", "1.2.276.0.7230010.3.1.3.296485376.1.1568899781.944588"); // RTSTRUCT, "Unsupported" type 2: DICOM/WebViewer2/TFE/IMAGES/IM452
+
+      
+        //app2->AddDicomStudy("1.2.276.0.7230010.3.1.2.380371456.1.1544616291.954997");  // CSPO
+      }
+
+      boost::shared_ptr<OrthancStone::WebGLViewport> viewport(OrthancStone::GetWebGLViewportsRegistry().Add(canvasId));
+      application_ = OrthancStone::Application::Create(*context_, viewport);
+
+      // Paint the viewport to black
+      
+      {
+        OrthancStone::WebGLViewportsRegistry::Accessor accessor(
+          OrthancStone::GetWebGLViewportsRegistry(), canvasId);
+
+        if (accessor.IsValid())
+        {
+          accessor.GetViewport().Invalidate();
+        }
+      }
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+  }
+
+  
+  EMSCRIPTEN_KEEPALIVE
+  void LoadOrthanc(const char* instance,
+                   int frame)
+  {
+    try
+    {
+      if (application_.get() != NULL)
+      {
+        OrthancStone::DicomSource source;
+        application_->LoadOrthancFrame(source, instance, frame);
+      }
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+  }
+
+  
+  EMSCRIPTEN_KEEPALIVE
+  void LoadDicomWeb(const char* server,
+                    const char* studyInstanceUid,
+                    const char* seriesInstanceUid,
+                    const char* sopInstanceUid,
+                    int frame)
+  {
+    try
+    {
+      if (application_.get() != NULL)
+      {
+        OrthancStone::DicomSource source;
+        source.SetDicomWebThroughOrthancSource(server);
+        application_->LoadDicomWebFrame(source, studyInstanceUid, seriesInstanceUid,
+                                        sopInstanceUid, frame);
+      }
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/WebAssembly/SingleFrameViewer/SingleFrameViewerApp.js	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,89 @@
+
+// This object wraps the functions exposed by the wasm module
+
+const WasmModuleWrapper = function() {
+  this._InitializeViewport = undefined;
+  this._LoadOrthanc = undefined;
+  this._LoadDicomWeb = undefined;
+};
+
+WasmModuleWrapper.prototype.Setup = function(Module) {
+  this._InitializeViewport = Module.cwrap('InitializeViewport', null, [ 'string' ]);
+  this._LoadOrthanc = Module.cwrap('LoadOrthanc', null, [ 'string', 'int' ]);
+  this._LoadDicomWeb = Module.cwrap('LoadDicomWeb', null, [ 'string', 'string', 'string', 'string', 'int' ]);
+};
+
+WasmModuleWrapper.prototype.InitializeViewport = function(canvasId) {
+  this._InitializeViewport(canvasId);
+};
+
+WasmModuleWrapper.prototype.LoadOrthanc = function(instance, frame) {
+  this._LoadOrthanc(instance, frame);
+};
+
+WasmModuleWrapper.prototype.LoadDicomWeb = function(server, studyInstanceUid, seriesInstanceUid, sopInstanceUid, frame) {
+  this._LoadDicomWeb(server, studyInstanceUid, seriesInstanceUid, sopInstanceUid, frame);
+};
+
+var moduleWrapper = new WasmModuleWrapper();
+
+$(document).ready(function() {
+
+  window.addEventListener('StoneInitialized', function() {
+    stone.Setup(Module);
+    console.warn('Native Stone properly intialized');
+
+    stone.InitializeViewport('viewport');
+  });
+
+  window.addEventListener('StoneException', function() {
+    alert('Exception caught in Stone');
+  });    
+
+  var scriptSource;
+
+  if ('WebAssembly' in window) {
+    console.warn('Loading WebAssembly');
+    scriptSource = 'SingleFrameViewerWasm.js';
+  } else {
+    console.error('Your browser does not support WebAssembly!');
+  }
+
+  // Option 1: Loading script using plain HTML
+  
+  /*
+    var script = document.createElement('script');
+    script.src = scriptSource;
+    script.type = 'text/javascript';
+    document.body.appendChild(script);
+  */
+
+  // Option 2: Loading script using AJAX (gives the opportunity to
+  // report explicit errors)
+  
+  axios.get(scriptSource)
+    .then(function (response) {
+      var script = document.createElement('script');
+      script.innerHTML = response.data;
+      script.type = 'text/javascript';
+      document.body.appendChild(script);
+    })
+    .catch(function (error) {
+      alert('Cannot load the WebAssembly framework');
+    });
+});
+
+
+$('#orthancLoad').click(function() {
+  stone.LoadOrthanc($('#orthancInstance').val(),
+                    $('#orthancFrame').val());
+});
+
+
+$('#dicomWebLoad').click(function() {
+  stone.LoadDicomWeb($('#dicomWebServer').val(),
+                     $('#dicomWebStudy').val(),
+                     $('#dicomWebSeries').val(),
+                     $('#dicomWebInstance').val(),
+                     $('#dicomWebFrame').val());
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/WebAssembly/SingleFrameViewer/SingleFrameViewerApplication.h	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,483 @@
+#pragma once
+
+#include <Framework/Viewport/IViewport.h>
+#include <Framework/Loaders/DicomResourcesLoader.h>
+#include <Framework/Loaders/ILoadersContext.h>
+#include <Framework/Loaders/SeriesFramesLoader.h>
+#include <Framework/Loaders/SeriesThumbnailsLoader.h>
+
+#include <boost/make_shared.hpp>
+
+
+namespace OrthancStone
+{
+  class Application : public ObserverBase<Application>
+  {
+  private:
+    ILoadersContext&                         context_;
+    boost::shared_ptr<IViewport>             viewport_;
+    boost::shared_ptr<DicomResourcesLoader>  dicomLoader_;
+    boost::shared_ptr<SeriesFramesLoader>    framesLoader_;
+
+    Application(ILoadersContext& context,
+                boost::shared_ptr<IViewport> viewport) : 
+      context_(context),
+      viewport_(viewport)
+    {
+    }
+
+    void Handle(const SeriesFramesLoader::FrameLoadedMessage& message)
+    {
+      LOG(INFO) << "Frame decoded! "
+                << message.GetImage().GetWidth() << "x" << message.GetImage().GetHeight()
+                << " " << Orthanc::EnumerationToString(message.GetImage().GetFormat());
+
+      std::auto_ptr<TextureBaseSceneLayer> layer(
+        message.GetInstanceParameters().CreateTexture(message.GetImage()));
+      layer->SetLinearInterpolation(true);
+
+      {
+        std::auto_ptr<IViewport::ILock> lock(viewport_->Lock());
+        lock->GetController().GetScene().SetLayer(0, layer.release());
+        lock->GetCompositor().FitContent(lock->GetController().GetScene());
+        lock->Invalidate();
+      }
+    }
+
+    void Handle(const DicomResourcesLoader::SuccessMessage& message)
+    {
+      if (message.GetResources()->GetSize() != 1)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+   
+      //message.GetResources()->GetResource(0).Print(stdout);
+
+      {
+        std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
+        SeriesFramesLoader::Factory f(*message.GetResources());
+
+        framesLoader_ = boost::dynamic_pointer_cast<SeriesFramesLoader>(f.Create(*lock));
+        Register<SeriesFramesLoader::FrameLoadedMessage>(*framesLoader_, &Application::Handle);
+
+        assert(message.HasUserPayload());
+        const Orthanc::SingleValueObject<unsigned int>& payload =
+          dynamic_cast<const Orthanc::SingleValueObject<unsigned int>&>(message.GetUserPayload());
+
+        LOG(INFO) << "Loading pixel data of frame: " << payload.GetValue();
+        framesLoader_->ScheduleLoadFrame(
+          0, message.GetDicomSource(), payload.GetValue(),
+          message.GetDicomSource().GetQualityCount() - 1 /* download best quality available */,
+          NULL);
+      }
+    }
+
+  public:
+    static boost::shared_ptr<Application> Create(ILoadersContext& context,
+                                                 boost::shared_ptr<IViewport> viewport)
+    {
+      boost::shared_ptr<Application> application(new Application(context, viewport));
+
+      {
+        std::auto_ptr<ILoadersContext::ILock> lock(context.Lock());
+        DicomResourcesLoader::Factory f;
+        application->dicomLoader_ = boost::dynamic_pointer_cast<DicomResourcesLoader>(f.Create(*lock));
+      }
+
+      application->Register<DicomResourcesLoader::SuccessMessage>(*application->dicomLoader_, &Application::Handle);
+
+      return application;
+    }
+
+    void LoadOrthancFrame(const DicomSource& source,
+                          const std::string& instanceId,
+                          unsigned int frame)
+    {
+      std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
+
+      dicomLoader_->ScheduleLoadOrthancResource(
+        boost::make_shared<LoadedDicomResources>(Orthanc::DICOM_TAG_SOP_INSTANCE_UID), 
+        0, source, Orthanc::ResourceType_Instance, instanceId,
+        new Orthanc::SingleValueObject<unsigned int>(frame));
+    }
+
+    void LoadDicomWebFrame(const DicomSource& source,
+                           const std::string& studyInstanceUid,
+                           const std::string& seriesInstanceUid,
+                           const std::string& sopInstanceUid,
+                           unsigned int frame)
+    {
+      std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
+
+      // We first must load the "/metadata" to know the number of frames
+      dicomLoader_->ScheduleGetDicomWeb(
+        boost::make_shared<LoadedDicomResources>(Orthanc::DICOM_TAG_SOP_INSTANCE_UID), 0, source,
+        "/studies/" + studyInstanceUid + "/series/" + seriesInstanceUid + "/instances/" + sopInstanceUid + "/metadata",
+        new Orthanc::SingleValueObject<unsigned int>(frame));
+    }
+
+    void FitContent()
+    {
+      std::auto_ptr<IViewport::ILock> lock(viewport_->Lock());
+      lock->GetCompositor().FitContent(lock->GetController().GetScene());
+      lock->Invalidate();
+    }
+  };
+
+
+
+  class IWebViewerLoadersObserver : public boost::noncopyable
+  {
+  public:
+    virtual ~IWebViewerLoadersObserver()
+    {
+    }
+
+    virtual void SignalSeriesUpdated(LoadedDicomResources& series) = 0;
+
+    virtual void SignalThumbnailLoaded(const std::string& studyInstanceUid,
+                                       const std::string& seriesInstanceUid,
+                                       SeriesThumbnailType type) = 0;
+  };
+  
+
+  class WebViewerLoaders : public ObserverBase<WebViewerLoaders>
+  {
+  private:
+    static const int PRIORITY_ADD_RESOURCES = 0;
+    static const int PRIORITY_THUMBNAILS = OracleScheduler::PRIORITY_LOW + 100;
+
+    enum Type
+    {
+      Type_Orthanc = 1,
+      Type_DicomWeb = 2
+    };
+
+    ILoadersContext&                           context_;
+    std::auto_ptr<IWebViewerLoadersObserver>   observer_;
+    bool                                       loadThumbnails_;
+    DicomSource                                source_;
+    std::set<std::string>                      scheduledSeries_;
+    std::set<std::string>                      scheduledThumbnails_;
+    std::set<std::string>                      scheduledStudies_;
+    boost::shared_ptr<LoadedDicomResources>    loadedSeries_;
+    boost::shared_ptr<LoadedDicomResources>    loadedStudies_;
+    boost::shared_ptr<DicomResourcesLoader>    resourcesLoader_;
+    boost::shared_ptr<SeriesThumbnailsLoader>  thumbnailsLoader_;
+
+    WebViewerLoaders(ILoadersContext& context,
+                     IWebViewerLoadersObserver* observer) :
+      context_(context),
+      observer_(observer),
+      loadThumbnails_(false)
+    {
+      loadedSeries_ = boost::make_shared<LoadedDicomResources>(Orthanc::DICOM_TAG_SERIES_INSTANCE_UID);
+      loadedStudies_ = boost::make_shared<LoadedDicomResources>(Orthanc::DICOM_TAG_STUDY_INSTANCE_UID);
+    }
+
+    static Orthanc::IDynamicObject* CreatePayload(Type type)
+    {
+      return new Orthanc::SingleValueObject<Type>(type);
+    }
+    
+    void HandleThumbnail(const SeriesThumbnailsLoader::ThumbnailLoadedMessage& message)
+    {
+      if (observer_.get() != NULL)
+      {
+        observer_->SignalThumbnailLoaded(message.GetStudyInstanceUid(),
+                                         message.GetSeriesInstanceUid(),
+                                         message.GetType());
+      }
+    }
+    
+    void HandleLoadedResources(const DicomResourcesLoader::SuccessMessage& message)
+    {
+      LoadedDicomResources series(Orthanc::DICOM_TAG_SERIES_INSTANCE_UID);
+
+      switch (dynamic_cast<const Orthanc::SingleValueObject<Type>&>(message.GetUserPayload()).GetValue())
+      {
+        case Type_DicomWeb:
+        {          
+          for (size_t i = 0; i < loadedSeries_->GetSize(); i++)
+          {
+            std::string study;
+            if (loadedSeries_->GetResource(i).LookupStringValue(
+                  study, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) &&
+                loadedStudies_->HasResource(study))
+            {
+              Orthanc::DicomMap m;
+              m.Assign(loadedSeries_->GetResource(i));
+              loadedStudies_->MergeResource(m, study);
+              series.AddResource(m);
+            }
+          }
+
+          break;
+        }
+
+        case Type_Orthanc:
+        {          
+          for (size_t i = 0; i < message.GetResources()->GetSize(); i++)
+          {
+            series.AddResource(message.GetResources()->GetResource(i));
+          }
+
+          break;
+        }
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+
+      if (loadThumbnails_ &&
+          (!source_.IsDicomWeb() ||
+           source_.HasDicomWebRendered()))
+      {
+        for (size_t i = 0; i < series.GetSize(); i++)
+        {
+          std::string patientId, studyInstanceUid, seriesInstanceUid;
+          if (series.GetResource(i).LookupStringValue(patientId, Orthanc::DICOM_TAG_PATIENT_ID, false) &&
+              series.GetResource(i).LookupStringValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) &&
+              series.GetResource(i).LookupStringValue(seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false) &&
+              scheduledThumbnails_.find(seriesInstanceUid) == scheduledThumbnails_.end())
+          {
+            scheduledThumbnails_.insert(seriesInstanceUid);
+            thumbnailsLoader_->ScheduleLoadThumbnail(source_, patientId, studyInstanceUid, seriesInstanceUid);
+          }
+        }
+      }
+
+      if (observer_.get() != NULL &&
+          series.GetSize() > 0)
+      {
+        observer_->SignalSeriesUpdated(series);
+      }
+    }
+
+    void HandleOrthancRestApi(const OrthancRestApiCommand::SuccessMessage& message)
+    {
+      Json::Value body;
+      message.ParseJsonBody(body);
+
+      if (body.type() != Json::arrayValue)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+      }
+      else
+      {
+        for (Json::Value::ArrayIndex i = 0; i < body.size(); i++)
+        {
+          if (body[i].type() == Json::stringValue)
+          {
+            AddOrthancSeries(body[i].asString());
+          }
+          else
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+          }
+        }
+      }
+    }
+
+  public:
+    static boost::shared_ptr<WebViewerLoaders> Create(ILoadersContext& context,
+                                                      const DicomSource& source,
+                                                      bool loadThumbnails,
+                                                      IWebViewerLoadersObserver* observer)
+    {
+      boost::shared_ptr<WebViewerLoaders> application(new WebViewerLoaders(context, observer));
+      application->source_ = source;
+      application->loadThumbnails_ = loadThumbnails;
+
+      {
+        std::auto_ptr<ILoadersContext::ILock> lock(context.Lock());
+
+        {
+          DicomResourcesLoader::Factory f;
+          application->resourcesLoader_ = boost::dynamic_pointer_cast<DicomResourcesLoader>(f.Create(*lock));
+        }
+
+        {
+          SeriesThumbnailsLoader::Factory f;
+          f.SetPriority(PRIORITY_THUMBNAILS);
+          application->thumbnailsLoader_ = boost::dynamic_pointer_cast<SeriesThumbnailsLoader>(f.Create(*lock));
+        }
+
+        application->Register<OrthancRestApiCommand::SuccessMessage>(
+          lock->GetOracleObservable(), &WebViewerLoaders::HandleOrthancRestApi);
+
+        application->Register<DicomResourcesLoader::SuccessMessage>(
+          *application->resourcesLoader_, &WebViewerLoaders::HandleLoadedResources);
+
+        application->Register<SeriesThumbnailsLoader::ThumbnailLoadedMessage>(
+          *application->thumbnailsLoader_, &WebViewerLoaders::HandleThumbnail);
+
+        lock->AddLoader(application);
+      }
+
+      return application;
+    }
+    
+    void AddDicomAllSeries()
+    {
+      std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
+
+      if (source_.IsDicomWeb())
+      {
+        resourcesLoader_->ScheduleGetDicomWeb(loadedSeries_, PRIORITY_ADD_RESOURCES, source_,
+                                              "/series", CreatePayload(Type_DicomWeb));
+        resourcesLoader_->ScheduleGetDicomWeb(loadedStudies_, PRIORITY_ADD_RESOURCES, source_,
+                                              "/studies", CreatePayload(Type_DicomWeb));
+      }
+      else if (source_.IsOrthanc())
+      {
+        std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+        command->SetMethod(Orthanc::HttpMethod_Get);
+        command->SetUri("/series");
+        lock->Schedule(GetSharedObserver(), PRIORITY_ADD_RESOURCES, command.release());
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+    }
+    
+    void AddDicomStudy(const std::string& studyInstanceUid)
+    {
+      // Avoid adding twice the same study
+      if (scheduledStudies_.find(studyInstanceUid) == scheduledStudies_.end())
+      {
+        scheduledStudies_.insert(studyInstanceUid);
+
+        if (source_.IsDicomWeb())
+        {
+          Orthanc::DicomMap filter;
+          filter.SetValue(Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, studyInstanceUid, false);
+          
+          std::set<Orthanc::DicomTag> tags;
+          
+          {
+            std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());      
+            
+            resourcesLoader_->ScheduleQido(loadedStudies_, PRIORITY_ADD_RESOURCES, source_,
+                                           Orthanc::ResourceType_Study, filter, tags, CreatePayload(Type_DicomWeb));
+            
+            resourcesLoader_->ScheduleQido(loadedSeries_, PRIORITY_ADD_RESOURCES, source_,
+                                           Orthanc::ResourceType_Series, filter, tags, CreatePayload(Type_DicomWeb));
+          }
+        }
+        else if (source_.IsOrthanc())
+        {
+          std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+          command->SetMethod(Orthanc::HttpMethod_Post);
+          command->SetUri("/tools/find");
+
+          Json::Value body;
+          body["Level"] = "Series";
+          body["Query"] = Json::objectValue;
+          body["Query"]["StudyInstanceUID"] = studyInstanceUid;
+          command->SetBody(body);
+
+          {
+            std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());      
+            lock->Schedule(GetSharedObserver(), PRIORITY_ADD_RESOURCES, command.release());
+          }
+        }
+        else
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+        }        
+      }
+    }
+    
+    void AddDicomSeries(const std::string& studyInstanceUid,
+                        const std::string& seriesInstanceUid)
+    {
+      std::set<Orthanc::DicomTag> tags;
+
+      std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());      
+
+      if (scheduledStudies_.find(studyInstanceUid) == scheduledStudies_.end())
+      {
+        scheduledStudies_.insert(studyInstanceUid);
+          
+        if (source_.IsDicomWeb())
+        {
+          Orthanc::DicomMap filter;
+          filter.SetValue(Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, studyInstanceUid, false);
+          
+          resourcesLoader_->ScheduleQido(loadedStudies_, PRIORITY_ADD_RESOURCES, source_,
+                                         Orthanc::ResourceType_Study, filter, tags, CreatePayload(Type_DicomWeb));
+        }
+      }
+
+      if (scheduledSeries_.find(seriesInstanceUid) == scheduledSeries_.end())
+      {
+        scheduledSeries_.insert(seriesInstanceUid);
+
+        if (source_.IsDicomWeb())
+        {
+          Orthanc::DicomMap filter;
+          filter.SetValue(Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, studyInstanceUid, false);
+          filter.SetValue(Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, seriesInstanceUid, false);
+          
+          resourcesLoader_->ScheduleQido(loadedSeries_, PRIORITY_ADD_RESOURCES, source_,
+                                         Orthanc::ResourceType_Series, filter, tags, CreatePayload(Type_DicomWeb));
+        }
+        else if (source_.IsOrthanc())
+        {
+          std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
+          command->SetMethod(Orthanc::HttpMethod_Post);
+          command->SetUri("/tools/find");
+
+          Json::Value body;
+          body["Level"] = "Series";
+          body["Query"] = Json::objectValue;
+          body["Query"]["StudyInstanceUID"] = studyInstanceUid;
+          body["Query"]["SeriesInstanceUID"] = seriesInstanceUid;
+          command->SetBody(body);
+
+          lock->Schedule(GetSharedObserver(), PRIORITY_ADD_RESOURCES, command.release());
+        }
+        else
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+        }
+      }
+    }
+
+    void AddOrthancStudy(const std::string& orthancId)
+    {
+      if (source_.IsOrthanc())
+      {
+        std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());      
+        resourcesLoader_->ScheduleLoadOrthancResources(
+          loadedSeries_, PRIORITY_ADD_RESOURCES, source_,
+          Orthanc::ResourceType_Study, orthancId, Orthanc::ResourceType_Series,
+          CreatePayload(Type_Orthanc));
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType,
+                                        "Only applicable to Orthanc DICOM sources");
+      }
+    }
+
+    void AddOrthancSeries(const std::string& orthancId)
+    {
+      if (source_.IsOrthanc())
+      {
+        std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());      
+        resourcesLoader_->ScheduleLoadOrthancResource(
+          loadedSeries_, PRIORITY_ADD_RESOURCES,
+          source_, Orthanc::ResourceType_Series, orthancId,
+          CreatePayload(Type_Orthanc));
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType,
+                                        "Only applicable to Orthanc DICOM sources");
+      }
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/WebAssembly/SingleFrameViewer/WasmWrapper.js	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,27 @@
+
+const Stone = function() {
+  this._InitializeViewport = undefined;
+  this._LoadOrthanc = undefined;
+  this._LoadDicomWeb = undefined;
+};
+
+Stone.prototype.Setup = function(Module) {
+  this._InitializeViewport = Module.cwrap('InitializeViewport', null, [ 'string' ]);
+  this._LoadOrthanc = Module.cwrap('LoadOrthanc', null, [ 'string', 'int' ]);
+  this._LoadDicomWeb = Module.cwrap('LoadDicomWeb', null, [ 'string', 'string', 'string', 'string', 'int' ]);
+};
+
+Stone.prototype.InitializeViewport = function(canvasId) {
+  this._InitializeViewport(canvasId);
+};
+
+Stone.prototype.LoadOrthanc = function(instance, frame) {
+  this._LoadOrthanc(instance, frame);
+};
+
+Stone.prototype.LoadDicomWeb = function(server, studyInstanceUid, seriesInstanceUid, sopInstanceUid, frame) {
+  this._LoadDicomWeb(server, studyInstanceUid, seriesInstanceUid, sopInstanceUid, frame);
+};
+
+var stone = new Stone();
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/WebAssembly/SingleFrameViewer/index.html	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,71 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <title>Osimis' Web Viewer</title>
+    <meta charset="utf-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1" />
+    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
+    <meta name="apple-mobile-web-app-capable" content="yes" />
+    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
+    <link rel="icon" href="data:;base64,iVBORw0KGgo=">
+
+    <style>
+      canvas {
+      background-color: green;
+      width : 100%;
+      height : 512px;
+      }
+    </style>
+  </head>
+  <body>
+    <h1>Viewport</h1>
+
+    <canvas id="viewport" >
+    </canvas>
+
+    <h1>Load from Orthanc</h1>
+    <p>
+      Orthanc instance: <input type="text" id="orthancInstance" size="80"
+                               value="5eb2dd5f-3fca21a8-fa7565fd-63e112ae-344830a4">
+      <!-- Orthanc instance: <input type="text" id="orthancInstance" size="80"
+                               value="61f3143e-96f34791-ad6bbb8d-62559e75-45943e1b"> -->
+      <!-- Orthanc instance: <input type="text" id="orthancInstance" size="80"
+                               value="8dddbd75-f4095768-6e66f851-22305751-18782bdd"> -->
+    </p>
+    <p>
+      Frame number: <input type="text" id="orthancFrame" value="0">
+    </p>
+    <p>
+      <button id="orthancLoad">Load</button>
+    </p>
+
+    <h1>Load from DICOMweb</h1>
+    <p>
+      Server name in Orthanc: <input type="text" id="dicomWebServer" value="self">
+    </p>
+    <p>
+      Study instance UID: <input type="text" id="dicomWebStudy" size="80"
+                                 value="1.2.840.113543.6.6.4.7.64067529866380271256212683512383713111129">
+    </p>
+    <p>
+      Series instance UID: <input type="text" id="dicomWebSeries" size="80"
+                                  value="1.2.840.113543.6.6.4.7.63556916880112768082712975118701689357177">
+    </p>
+    <p>
+      SOP instance UID: <input type="text" id="dicomWebInstance" size="80"
+                               value="1.2.840.113543.6.6.4.7.64234348190163144631511103849051737563212">
+    </p>
+    <p>
+      Frame number: <input type="text" id="dicomWebFrame" value="0">
+    </p>
+    <p>
+      <button id="dicomWebLoad">Load</button>
+    </p>
+
+    <script src="https://code.jquery.com/jquery-3.4.1.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.0/axios.js"></script>
+    
+    <script src="WasmWrapper.js"></script>
+    <script src="SingleFrameViewerApp.js"></script>
+  </body>
+</html>
--- a/Samples/WebAssembly/app.js	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,33 +0,0 @@
-/**
- * This is a generic bootstrap code that is shared by all the Stone
- * sample applications.
- **/
-
-// Check support for WebAssembly
-if (!('WebAssembly' in window)) {
-  alert('Sorry, your browser does not support WebAssembly :(');
-} else {
-
-  // Wait for the module to be loaded (the event "WebAssemblyLoaded"
-  // must be emitted by the "main" function)
-  window.addEventListener('WebAssemblyLoaded', function() {
-
-    // Loop over the GET arguments
-    var parameters = window.location.search.substr(1);
-    if (parameters != null && parameters != '') {
-      var tokens = parameters.split('&');
-      for (var i = 0; i < tokens.length; i++) {
-        var arg = tokens[i].split('=');
-        if (arg.length == 2) {
-
-          // Send each GET argument to WebAssembly
-          Module.ccall('SetArgument', null, [ 'string', 'string' ],
-                       [ arg[0], decodeURIComponent(arg[1]) ]);
-        }
-      }
-    }
-
-    // Inform the WebAssembly module that it can start
-    Module.ccall('Initialize', null, null, null);
-  });
-}
--- a/Samples/WebAssembly/dev.h	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,202 +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 "../../Framework/Viewport/WebAssemblyViewport.h"
-#include "../../Framework/Scene2D/OpenGLCompositor.h"
-#include "../../Framework/Scene2D/PanSceneTracker.h"
-#include "../../Framework/Scene2D/RotateSceneTracker.h"
-#include "../../Framework/Scene2D/ZoomSceneTracker.h"
-#include "../../Framework/Scene2DViewport/UndoStack.h"
-#include "../../Framework/Scene2DViewport/ViewportController.h"
-
-#include <Core/OrthancException.h>
-
-#include <emscripten/html5.h>
-#include <boost/make_shared.hpp>
-
-static const unsigned int FONT_SIZE = 32;
-
-namespace OrthancStone
-{
-  class ActiveTracker : public boost::noncopyable
-  {
-  private:
-    boost::shared_ptr<IFlexiblePointerTracker> tracker_;
-    std::string                             canvasIdentifier_;
-    bool                                    insideCanvas_;
-    
-  public:
-    ActiveTracker(const boost::shared_ptr<IFlexiblePointerTracker>& tracker,
-                  const std::string& canvasId) :
-      tracker_(tracker),
-      canvasIdentifier_(canvasId),
-      insideCanvas_(true)
-    {
-      if (tracker_.get() == NULL)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-      }
-    }
-
-    bool IsAlive() const
-    {
-      return tracker_->IsAlive();
-    }
-
-    void PointerMove(const PointerEvent& event)
-    {
-      tracker_->PointerMove(event);
-    }
-
-    void PointerUp(const PointerEvent& event)
-    {
-      tracker_->PointerUp(event);
-    }
-  };
-}
-
-static OrthancStone::PointerEvent* ConvertMouseEvent(
-  const EmscriptenMouseEvent&        source,
-  OrthancStone::IViewport& viewport)
-{
-  std::unique_ptr<OrthancStone::PointerEvent> target(
-    new OrthancStone::PointerEvent);
-
-  target->AddPosition(viewport.GetPixelCenterCoordinates(
-                        source.targetX, source.targetY));
-  target->SetAltModifier(source.altKey);
-  target->SetControlModifier(source.ctrlKey);
-  target->SetShiftModifier(source.shiftKey);
-
-  return target.release();
-}
-
-std::unique_ptr<OrthancStone::ActiveTracker> tracker_;
-
-EM_BOOL OnMouseEvent(int eventType, 
-                     const EmscriptenMouseEvent *mouseEvent, 
-                     void *userData)
-{
-  if (mouseEvent != NULL &&
-      userData != NULL)
-  {
-    boost::shared_ptr<OrthancStone::ViewportController>& controller = 
-      *reinterpret_cast<boost::shared_ptr<OrthancStone::ViewportController>*>(userData);
-
-    switch (eventType)
-    {
-      case EMSCRIPTEN_EVENT_CLICK:
-      {
-        static unsigned int count = 0;
-        char buf[64];
-        sprintf(buf, "click %d", count++);
-
-        std::unique_ptr<OrthancStone::TextSceneLayer> layer(new OrthancStone::TextSceneLayer);
-        layer->SetText(buf);
-        controller->GetViewport().GetScene().SetLayer(100, layer.release());
-        controller->GetViewport().Refresh();
-        break;
-      }
-
-      case EMSCRIPTEN_EVENT_MOUSEDOWN:
-      {
-        boost::shared_ptr<OrthancStone::IFlexiblePointerTracker> t;
-
-        {
-          std::unique_ptr<OrthancStone::PointerEvent> event(
-            ConvertMouseEvent(*mouseEvent, controller->GetViewport()));
-
-          switch (mouseEvent->button)
-          {
-            case 0:  // Left button
-              emscripten_console_log("Creating RotateSceneTracker");
-              t.reset(new OrthancStone::RotateSceneTracker(
-                        controller, *event));
-              break;
-
-            case 1:  // Middle button
-              emscripten_console_log("Creating PanSceneTracker");
-              LOG(INFO) << "Creating PanSceneTracker" ;
-              t.reset(new OrthancStone::PanSceneTracker(
-                        controller, *event));
-              break;
-
-            case 2:  // Right button
-              emscripten_console_log("Creating ZoomSceneTracker");
-              t.reset(new OrthancStone::ZoomSceneTracker(
-                        controller, *event, controller->GetViewport().GetCanvasWidth()));
-              break;
-
-            default:
-              break;
-          }
-        }
-
-        if (t.get() != NULL)
-        {
-          tracker_.reset(
-            new OrthancStone::ActiveTracker(t, controller->GetViewport().GetCanvasIdentifier()));
-          controller->GetViewport().Refresh();
-        }
-
-        break;
-      }
-
-      case EMSCRIPTEN_EVENT_MOUSEMOVE:
-        if (tracker_.get() != NULL)
-        {
-          std::unique_ptr<OrthancStone::PointerEvent> event(
-            ConvertMouseEvent(*mouseEvent, controller->GetViewport()));
-          tracker_->PointerMove(*event);
-          controller->GetViewport().Refresh();
-        }
-        break;
-
-      case EMSCRIPTEN_EVENT_MOUSEUP:
-        if (tracker_.get() != NULL)
-        {
-          std::unique_ptr<OrthancStone::PointerEvent> event(
-            ConvertMouseEvent(*mouseEvent, controller->GetViewport()));
-          tracker_->PointerUp(*event);
-          controller->GetViewport().Refresh();
-          if (!tracker_->IsAlive())
-            tracker_.reset();
-        }
-        break;
-
-      default:
-        break;
-    }
-  }
-
-  return true;
-}
-
-
-void SetupEvents(const std::string& canvas,
-                 boost::shared_ptr<OrthancStone::ViewportController>& controller)
-{
-  emscripten_set_mousedown_callback(canvas.c_str(), &controller, false, OnMouseEvent);
-  emscripten_set_mousemove_callback(canvas.c_str(), &controller, false, OnMouseEvent);
-  emscripten_set_mouseup_callback(canvas.c_str(), &controller, false, OnMouseEvent);
-}
--- a/Samples/WebAssembly/index.html	Mon Apr 20 18:26:32 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,16 +0,0 @@
-<!doctype html>
-<html lang="en-us">
-  <head>
-    <meta charset="utf-8">
-    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-
-    <title>Stone of Orthanc</title>
-  </head>
-  <body>
-    <h1>Available samples</h1>
-    <ul>
-      <li><a href="BasicScene.html">Basic scene</a></li>
-      <li><a href="BasicMPR.html">Basic MPR display</a></li>
-    </ul>
-  </body>
-</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/UnitTestsSources/CMakeLists.txt	Wed Apr 22 14:05:47 2020 +0200
@@ -0,0 +1,122 @@
+cmake_minimum_required(VERSION 2.8.3)
+project(OrthancStone)
+
+include(../Resources/CMake/OrthancStoneParameters.cmake)
+
+set(ENABLE_STONE_DEPRECATED ON)  # Need deprecated classes for these samples
+
+if (OPENSSL_NO_CAPIENG)
+add_definitions(-DOPENSSL_NO_CAPIENG=1)
+endif()
+
+set(ENABLE_SDL OFF CACHE BOOL "Target SDL Native application")
+set(ENABLE_QT OFF CACHE BOOL "Target Qt Native application")
+set(ENABLE_WASM OFF CACHE BOOL "Target WASM application")
+set(ENABLE_UNITTESTS ON 
+
+
+if (ENABLE_WASM)
+  set(ORTHANC_SANDBOXED ON)
+  if (ENABLE_SDL)
+    message("ENABLE_SDL is only supported in native (incompatible with ENABLE_WASM)")
+  endif()
+  if (ENABLE_QT)
+    message("ENABLE_QT is only supported in native (incompatible with ENABLE_WASM)")
+  endif()
+  set(ENABLE_NATIVE OFF)
+  set(ORTHANC_SANDBOXED OFF)
+  set(ENABLE_CRYPTO_OPTIONS ON)
+  set(ENABLE_GOOGLE_TEST ON)
+  set(ENABLE_WEB_CLIENT ON)
+elseif (ENABLE_QT OR ENABLE_SDL)
+  set(ENABLE_NATIVE ON)
+  set(ORTHANC_SANDBOXED OFF)
+  set(ENABLE_CRYPTO_OPTIONS ON)
+  set(ENABLE_GOOGLE_TEST ON)
+  set(ENABLE_WEB_CLIENT ON)
+else()
+  set(ENABLE_NATIVE ON)
+  set(ENABLE_OPENGL OFF)
+endif()
+
+#####################################################################
+## Configuration for Orthanc
+#####################################################################
+
+if (ORTHANC_STONE_VERSION STREQUAL "mainline")
+  set(ORTHANC_FRAMEWORK_VERSION "mainline")
+  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
+else()
+  set(ORTHANC_FRAMEWORK_VERSION "1.4.1")
+  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
+endif()
+
+set(ORTHANC_FRAMEWORK_SOURCE "${ORTHANC_FRAMEWORK_DEFAULT_SOURCE}" CACHE STRING "Source of the Orthanc source code (can be \"hg\", \"archive\", \"web\" or \"path\")")
+set(ORTHANC_FRAMEWORK_ARCHIVE "" CACHE STRING "Path to the Orthanc archive, if ORTHANC_FRAMEWORK_SOURCE is \"archive\"")
+set(ORTHANC_FRAMEWORK_ROOT "" CACHE STRING "Path to the Orthanc source directory, if ORTHANC_FRAMEWORK_SOURCE is \"path\"")
+
+add_definitions(
+  -DORTHANC_ENABLE_LOGGING_PLUGIN=0
+  )
+
+#####################################################################
+## Build a static library containing the Orthanc Stone framework
+#####################################################################
+
+LIST(APPEND ORTHANC_BOOST_COMPONENTS program_options)
+
+include(../Resources/CMake/OrthancStoneConfiguration.cmake)
+
+add_library(OrthancStone STATIC
+  ${ORTHANC_STONE_SOURCES}
+  )
+
+include_directories(${ORTHANC_STONE_ROOT})
+  
+#####################################################################
+## Build the unit tests
+#####################################################################
+
+if (ENABLE_UNITTESTS)
+  add_executable(UnitTests
+    ${GOOGLE_TEST_SOURCES}
+    ${ORTHANC_STONE_ROOT}/UnitTestsSources/GenericToolboxTests.cpp
+    ${ORTHANC_STONE_ROOT}/UnitTestsSources/ImageToolboxTests.cpp
+    ${ORTHANC_STONE_ROOT}/UnitTestsSources/PixelTestPatternsTests.cpp
+    ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestCommands.cpp
+    ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestMessageBroker.cpp
+    ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestStrategy.cpp
+    ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestStructureSet.cpp
+    ${ORTHANC_STONE_ROOT}/UnitTestsSources/UnitTestsMain.cpp
+    )
+
+  target_link_libraries(UnitTests OrthancStone)
+
+  add_custom_command(
+    TARGET UnitTests
+    POST_BUILD
+    COMMAND ${CMAKE_COMMAND} -E copy
+      "${ORTHANC_STONE_ROOT}/UnitTestsSources/72c773ac-5059f2c4-2e6a9120-4fd4bca1-45701661.json" 
+      "$<TARGET_FILE_DIR:UnitTests>/72c773ac-5059f2c4-2e6a9120-4fd4bca1-45701661.json"
+    )
+
+endif()
+
+#####################################################################
+## Generate the documentation if Doxygen is present
+#####################################################################
+
+find_package(Doxygen)
+if (DOXYGEN_FOUND)
+  configure_file(
+    ${ORTHANC_STONE_ROOT}/Resources/OrthancStone.doxygen
+    ${CMAKE_CURRENT_BINARY_DIR}/OrthancStone.doxygen
+    @ONLY)
+
+  add_custom_target(doc
+    ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/OrthancStone.doxygen
+    COMMENT "Generating documentation with Doxygen" VERBATIM
+    )
+else()
+  message("Doxygen not found. The documentation will not be built.")
+endif()
--- a/UnitTestsSources/GenericToolboxTests.cpp	Mon Apr 20 18:26:32 2020 +0200
+++ b/UnitTestsSources/GenericToolboxTests.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/UnitTestsSources/TestMessageBroker.cpp	Wed Apr 22 14:05:47 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	Mon Apr 20 18:26:32 2020 +0200
+++ b/UnitTestsSources/UnitTestsMain.cpp	Wed Apr 22 14:05:47 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();