changeset 444:1e3d870ccd0d

Merged am-vsol-upgrade into default
author Alain Mazy <am@osimis.io>
date Wed, 16 Jan 2019 16:10:16 +0100
parents b70e9be013e4 (current diff) e9f84e52aad2 (diff)
children 3fd8e4e5f93e
files UnitTestsSources/TestMessageBroker2.cpp UnitTestsSources/TestMessageBroker2_connect_ok.cpp UnitTestsSources/TestMessageBroker2_promise_and_connect_ok.cpp
diffstat 57 files changed, 3021 insertions(+), 2096 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/Generic/NativeStoneApplicationContext.cpp	Mon Dec 24 13:41:12 2018 +0100
+++ b/Applications/Generic/NativeStoneApplicationContext.cpp	Wed Jan 16 16:10:16 2019 +0100
@@ -57,7 +57,7 @@
 
   void NativeStoneApplicationContext::Start()
   {
-    boost::mutex::scoped_lock lock(globalMutex_);
+    boost::recursive_mutex::scoped_lock lock(globalMutex_);
     
     if (stopped_ &&
         centralViewport_.HasAnimation())
--- a/Applications/Generic/NativeStoneApplicationContext.h	Mon Dec 24 13:41:12 2018 +0100
+++ b/Applications/Generic/NativeStoneApplicationContext.h	Wed Jan 16 16:10:16 2019 +0100
@@ -36,7 +36,7 @@
   private:
     static void UpdateThread(NativeStoneApplicationContext* that);
 
-    boost::mutex    globalMutex_;
+    boost::recursive_mutex    globalMutex_;
     WidgetViewport  centralViewport_;
     boost::thread   updateThread_;
     bool            stopped_;
@@ -46,8 +46,8 @@
     class GlobalMutexLocker: public boost::noncopyable
     {
     private:
-      NativeStoneApplicationContext&  that_;
-      boost::mutex::scoped_lock       lock_;
+      NativeStoneApplicationContext&        that_;
+      boost::recursive_mutex::scoped_lock   lock_;
       
     public:
       GlobalMutexLocker(NativeStoneApplicationContext& that) :
--- a/Applications/Generic/NativeStoneApplicationRunner.cpp	Mon Dec 24 13:41:12 2018 +0100
+++ b/Applications/Generic/NativeStoneApplicationRunner.cpp	Wed Jan 16 16:10:16 2019 +0100
@@ -27,6 +27,7 @@
 
 #include "../../Framework/Toolbox/MessagingToolbox.h"
 #include "../../Platforms/Generic/OracleWebService.h"
+#include "../../Platforms/Generic/OracleDelayedCallExecutor.h"
 #include "NativeStoneApplicationContext.h"
 
 #include <Core/Logging.h>
@@ -188,7 +189,7 @@
       NativeStoneApplicationContext context(broker_);
 
       {
-        Oracle oracle(4); // use 4 threads to download content
+        Oracle oracle(6); // use multiple threads to execute asynchronous tasks like download content
         oracle.Start();
 
         {
@@ -196,6 +197,9 @@
           context.SetWebService(webService);
           context.SetOrthancBaseUrl(webServiceParameters.GetUrl());
 
+          OracleDelayedCallExecutor delayedExecutor(broker_, oracle, context);
+          context.SetDelayedCallExecutor(delayedExecutor);
+
           application_.Initialize(&context, statusBar, parameters);
 
           {
--- a/Applications/Samples/CMakeLists.txt	Mon Dec 24 13:41:12 2018 +0100
+++ b/Applications/Samples/CMakeLists.txt	Wed Jan 16 16:10:16 2019 +0100
@@ -23,7 +23,7 @@
   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}/Applications/Samples/samples-library.js --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/WasmWebService.js --js-library ${STONE_SOURCES_DIR}/Platforms/Wasm/default-library.js  -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\"]'")
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --js-library ${STONE_SOURCES_DIR}/Applications/Samples/samples-library.js --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
@@ -160,8 +160,6 @@
         )
 
       ORTHANC_QT_WRAP_CPP(SIMPLE_VIEWER_APPLICATION_SOURCES
-        ${ORTHANC_STONE_ROOT}/Applications/Qt/QCairoWidget.h
-        ${ORTHANC_STONE_ROOT}/Applications/Qt/QStoneMainWindow.h
         ${ORTHANC_STONE_ROOT}/Applications/Samples/SimpleViewer/Qt/SimpleViewerMainWindow.h
         )
 
@@ -195,7 +193,6 @@
     ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestCommands.cpp
     ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestExceptions.cpp
     ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestMessageBroker.cpp
-    ${ORTHANC_STONE_ROOT}/UnitTestsSources/TestMessageBroker2.cpp
     ${ORTHANC_STONE_ROOT}/UnitTestsSources/UnitTestsMain.cpp
     )
 
--- a/Applications/Samples/SingleFrameEditorApplication.h	Mon Dec 24 13:41:12 2018 +0100
+++ b/Applications/Samples/SingleFrameEditorApplication.h	Wed Jan 16 16:10:16 2019 +0100
@@ -31,6 +31,8 @@
 #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 <Core/HttpClient.h>
 #include <Core/Images/FontRegistry.h>
@@ -48,8 +50,8 @@
   namespace Samples
   {
     class RadiographyEditorInteractor :
-      public IWorldSceneInteractor,
-      public IObserver
+        public IWorldSceneInteractor,
+        public IObserver
     {
     private:
       enum Tool
@@ -60,7 +62,7 @@
         Tool_Resize,
         Tool_Windowing
       };
-        
+
 
       StoneApplicationContext*  context_;
       UndoRedoStack             undoRedoStack_;
@@ -71,8 +73,8 @@
       {
         return 10.0;
       }
-    
-         
+
+
     public:
       RadiographyEditorInteractor(MessageBroker& broker) :
         IObserver(broker),
@@ -85,7 +87,7 @@
       {
         context_ = &context;
       }
-    
+
       virtual IWorldSceneMouseTracker* CreateMouseTracker(WorldSceneWidget& worldWidget,
                                                           const ViewportGeometry& view,
                                                           MouseButton button,
@@ -101,16 +103,16 @@
         if (button == MouseButton_Left)
         {
           size_t selected;
-        
+
           if (tool_ == Tool_Windowing)
           {
             return new RadiographyWindowingTracker(
-              undoRedoStack_, widget.GetScene(),
-              viewportX, viewportY,
-              RadiographyWindowingTracker::Action_DecreaseWidth,
-              RadiographyWindowingTracker::Action_IncreaseWidth,
-              RadiographyWindowingTracker::Action_DecreaseCenter,
-              RadiographyWindowingTracker::Action_IncreaseCenter);
+                  undoRedoStack_, widget.GetScene(),
+                  viewportX, viewportY,
+                  RadiographyWindowingTracker::Action_DecreaseWidth,
+                  RadiographyWindowingTracker::Action_IncreaseWidth,
+                  RadiographyWindowingTracker::Action_DecreaseCenter,
+                  RadiographyWindowingTracker::Action_IncreaseCenter);
           }
           else if (!widget.LookupSelectedLayer(selected))
           {
@@ -133,23 +135,23 @@
             {
               switch (tool_)
               {
-                case Tool_Crop:
-                  return new RadiographyLayerCropTracker
+              case Tool_Crop:
+                return new RadiographyLayerCropTracker
                     (undoRedoStack_, widget.GetScene(), view, selected, x, y, corner);
 
-                case Tool_Resize:
-                  return new RadiographyLayerResizeTracker
+              case Tool_Resize:
+                return new RadiographyLayerResizeTracker
                     (undoRedoStack_, widget.GetScene(), selected, x, y, corner,
                      (modifiers & KeyboardModifiers_Shift));
 
-                default:
-                  throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+              default:
+                throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
               }
             }
             else
             {
               size_t layer;
-            
+
               if (widget.GetScene().LookupLayer(layer, x, y))
               {
                 widget.Select(layer);
@@ -158,7 +160,7 @@
               {
                 widget.Unselect();
               }
-            
+
               return NULL;
             }
           }
@@ -172,18 +174,18 @@
               {
                 switch (tool_)
                 {
-                  case Tool_Move:
-                    return new RadiographyLayerMoveTracker
+                case Tool_Move:
+                  return new RadiographyLayerMoveTracker
                       (undoRedoStack_, widget.GetScene(), layer, x, y,
                        (modifiers & KeyboardModifiers_Shift));
 
-                  case Tool_Rotate:
-                    return new RadiographyLayerRotateTracker
+                case Tool_Rotate:
+                  return new RadiographyLayerRotateTracker
                       (undoRedoStack_, widget.GetScene(), view, layer, x, y,
                        (modifiers & KeyboardModifiers_Shift));
-                
-                  default:
-                    break;
+
+                default:
+                  break;
                 }
 
                 return NULL;
@@ -232,14 +234,14 @@
              tool_ == Tool_Resize))
         {
           RadiographyScene::LayerAccessor accessor(widget.GetScene(), selected);
-        
+
           Corner corner;
           if (accessor.GetLayer().LookupCorner(corner, x, y, view.GetZoom(), GetHandleSize()))
           {
             accessor.GetLayer().GetCorner(x, y, corner);
-          
+
             double z = 1.0 / view.GetZoom();
-          
+
             context.SetSourceColor(255, 0, 0);
             cairo_t* cr = context.GetObject();
             cairo_set_line_width(cr, 2.0 * z);
@@ -270,118 +272,141 @@
 
         switch (keyChar)
         {
-          case 'a':
-            widget.FitContent();
-            break;
+        case 'a':
+          widget.FitContent();
+          break;
+
+        case 'c':
+          tool_ = Tool_Crop;
+          break;
 
-          case 'c':
-            tool_ = Tool_Crop;
-            break;
+        case 'd':
+        {
+          // dump to json and reload
+          Json::Value snapshot;
+          RadiographySceneWriter writer;
+          writer.Write(snapshot, widget.GetScene());
 
-          case 'e':
-          {
-            Orthanc::DicomMap tags;
+          LOG(INFO) << "JSON export was successful: "
+                    << snapshot.toStyledString();
+
+          boost::shared_ptr<RadiographyScene> scene(new RadiographyScene(GetBroker()));
+          RadiographySceneReader reader(*scene, context_->GetOrthancApiClient());
+
+          Orthanc::FontRegistry fontRegistry;
+          fontRegistry.AddFromResource(Orthanc::EmbeddedResources::FONT_UBUNTU_MONO_BOLD_16);
+
+          reader.SetFontRegistry(fontRegistry);
+          reader.Read(snapshot);
 
-            // 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);
+          widget.SetScene(scene);
+        };break;
+
+        case 'e':
+        {
+          Orthanc::DicomMap tags;
 
-            if (context_ != NULL)
-            {
-              widget.GetScene().ExportDicom(context_->GetOrthancApiClient(),
-                                            tags, 0.1, 0.1, widget.IsInverted(),
-                                            widget.GetInterpolation(), EXPORT_USING_PAM);
-            }
-            
-            break;
+          // 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(),
+                                          widget.GetInterpolation(), EXPORT_USING_PAM);
           }
 
-          case 'i':
-            widget.SwitchInvert();
-            break;
-        
-          case 'm':
-            tool_ = Tool_Move;
+          break;
+        }
+
+        case 'i':
+          widget.SwitchInvert();
+          break;
+
+        case 'm':
+          tool_ = Tool_Move;
+          break;
+
+        case 'n':
+        {
+          switch (widget.GetInterpolation())
+          {
+          case ImageInterpolation_Nearest:
+            LOG(INFO) << "Switching to bilinear interpolation";
+            widget.SetInterpolation(ImageInterpolation_Bilinear);
             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;
+          case ImageInterpolation_Bilinear:
+            LOG(INFO) << "Switching to nearest neighbor interpolation";
+            widget.SetInterpolation(ImageInterpolation_Nearest);
             break;
 
-          case 's':
-            tool_ = Tool_Resize;
-            break;
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+          }
+          
+          break;
+        }
 
-          case 'w':
-            tool_ = Tool_Windowing;
-            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 'y':
+          if (modifiers & KeyboardModifiers_Control)
+          {
+            undoRedoStack_.Redo();
+            widget.NotifyContentChanged();
+          }
+          break;
 
-          case 'z':
-            if (modifiers & KeyboardModifiers_Control)
-            {
-              undoRedoStack_.Undo();
-              widget.NotifyContentChanged();
-            }
-            break;
-        
-          default:
-            break;
+        case 'z':
+          if (modifiers & KeyboardModifiers_Control)
+          {
+            undoRedoStack_.Undo();
+            widget.NotifyContentChanged();
+          }
+          break;
+
+        default:
+          break;
         }
       }
     };
 
-  
-  
+
+
     class SingleFrameEditorApplication :
-      public SampleSingleCanvasApplicationBase,
-      public IObserver
+        public SampleSingleCanvasApplicationBase,
+        public IObserver
     {
     private:
-      std::auto_ptr<RadiographyScene>  scene_;
-      RadiographyEditorInteractor      interactor_;
+      boost::shared_ptr<RadiographyScene>   scene_;
+      RadiographyEditorInteractor           interactor_;
+      Orthanc::FontRegistry                 fontRegistry_;
 
     public:
       SingleFrameEditorApplication(MessageBroker& broker) :
@@ -399,11 +424,11 @@
       {
         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")
-          ;
+            ("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);
       }
@@ -440,12 +465,11 @@
         std::string instance = parameters["instance"].as<std::string>();
         int frame = parameters["frame"].as<unsigned int>();
 
-        Orthanc::FontRegistry fonts;
-        fonts.AddFromResource(Orthanc::EmbeddedResources::FONT_UBUNTU_MONO_BOLD_16);
+        fontRegistry_.AddFromResource(Orthanc::EmbeddedResources::FONT_UBUNTU_MONO_BOLD_16);
         
         scene_.reset(new RadiographyScene(GetBroker()));
         //scene_->LoadDicomFrame(instance, frame, false); //.SetPan(200, 0);
-        scene_->LoadDicomFrame(context->GetOrthancApiClient(), "61f3143e-96f34791-ad6bbb8d-62559e75-45943e1b", 0, false);
+        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");
@@ -454,18 +478,18 @@
         //scene_->LoadDicomWebFrame(context->GetWebService());
         
         {
-          RadiographyLayer& layer = scene_->LoadText(fonts.GetFont(0), "Hello\nworld");
+          RadiographyLayer& layer = scene_->LoadText(fontRegistry_.GetFont(0), "Hello\nworld", NULL);
           layer.SetResizeable(true);
         }
         
         {
-          RadiographyLayer& layer = scene_->LoadTestBlock(100, 50);
+          RadiographyLayer& layer = scene_->LoadTestBlock(100, 50, NULL);
           layer.SetResizeable(true);
           layer.SetPan(0, 200);
         }
         
         
-        mainWidget_ = new RadiographyWidget(GetBroker(), *scene_, "main-widget");
+        mainWidget_ = new RadiographyWidget(GetBroker(), scene_, "main-widget");
         mainWidget_->SetTransmitMouseOver(true);
         mainWidget_->SetInteractor(interactor_);
 
--- a/Applications/Samples/build-wasm.sh	Mon Dec 24 13:41:12 2018 +0100
+++ b/Applications/Samples/build-wasm.sh	Wed Jan 16 16:10:16 2019 +0100
@@ -1,7 +1,16 @@
 #!/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)
 
@@ -10,7 +19,7 @@
 
 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
-make -j 5
+make -j 5 $target
 
 echo "-- building the web application -- "
 cd $currentDir
--- a/Applications/Samples/build-web.sh	Mon Dec 24 13:41:12 2018 +0100
+++ b/Applications/Samples/build-web.sh	Wed Jan 16 16:10:16 2019 +0100
@@ -2,6 +2,7 @@
 
 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)
@@ -15,29 +16,37 @@
 cp $samplesRootDir/Web/samples-styles.css $outputDir
 
 # build simple-viewer-single-file (obsolete project)
-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
+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
-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
+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
-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
+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
-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/
+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/StoneApplicationContext.h	Mon Dec 24 13:41:12 2018 +0100
+++ b/Applications/StoneApplicationContext.h	Wed Jan 16 16:10:16 2019 +0100
@@ -22,6 +22,7 @@
 #pragma once
 
 #include "../Framework/Toolbox/IWebService.h"
+#include "../Framework/Toolbox/IDelayedCallExecutor.h"
 #include "../Framework/Toolbox/OrthancApiClient.h"
 #include "../Framework/Viewport/WidgetViewport.h"
 
@@ -41,6 +42,7 @@
   private:
     MessageBroker&                   broker_;
     IWebService*                     webService_;
+    IDelayedCallExecutor*            delayedCallExecutor_;
     std::auto_ptr<OrthancApiClient>  orthanc_;
     std::string                      orthancBaseUrl_;
 
@@ -49,7 +51,8 @@
   public:
     StoneApplicationContext(MessageBroker& broker) :
       broker_(broker),
-      webService_(NULL)
+      webService_(NULL),
+      delayedCallExecutor_(NULL)
     {
     }
 
@@ -74,5 +77,15 @@
     void SetWebService(IWebService& webService);
 
     void SetOrthancBaseUrl(const std::string& baseUrl);
+
+    void SetDelayedCallExecutor(IDelayedCallExecutor& delayedCallExecutor)
+    {
+      delayedCallExecutor_ = &delayedCallExecutor;
+    }
+
+    IDelayedCallExecutor& GetDelayedCallExecutor()
+    {
+      return *delayedCallExecutor_;
+    }
   };
 }
--- a/Framework/Messages/IObservable.cpp	Mon Dec 24 13:41:12 2018 +0100
+++ b/Framework/Messages/IObservable.cpp	Wed Jan 16 16:10:16 2019 +0100
@@ -64,6 +64,25 @@
     callables_[messageType].insert(callable);
   }
 
+  void IObservable::Unregister(IObserver *observer)
+  {
+    // 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)
+        {
+          delete *itCallable;
+          itCallableSet->second.erase(itCallable++);
+        }
+        else
+          ++itCallable;
+      }
+    }
+  }
   
   void IObservable::EmitMessage(const IMessage& message)
   {
--- a/Framework/Messages/IObservable.h	Mon Dec 24 13:41:12 2018 +0100
+++ b/Framework/Messages/IObservable.h	Wed Jan 16 16:10:16 2019 +0100
@@ -58,6 +58,8 @@
     // Takes ownsership
     void RegisterObserverCallback(ICallable* callable);
 
+    void Unregister(IObserver* observer);
+
     void EmitMessage(const IMessage& message);
 
     // Takes ownsership
--- a/Framework/Messages/MessageForwarder.h	Mon Dec 24 13:41:12 2018 +0100
+++ b/Framework/Messages/MessageForwarder.h	Wed Jan 16 16:10:16 2019 +0100
@@ -59,10 +59,10 @@
    * 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
+   * 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
+   * 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>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Radiography/RadiographyAlphaLayer.cpp	Wed Jan 16 16:10:16 2019 +0100
@@ -0,0 +1,132 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public 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 "RadiographyAlphaLayer.h"
+
+#include "RadiographyScene.h"
+
+#include <Core/Images/Image.h>
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+
+  void RadiographyAlphaLayer::SetAlpha(Orthanc::ImageAccessor* image)
+  {
+    std::auto_ptr<Orthanc::ImageAccessor> raii(image);
+
+    if (image == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+
+    if (image->GetFormat() != Orthanc::PixelFormat_Grayscale8)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
+    }
+
+    SetSize(image->GetWidth(), image->GetHeight());
+    alpha_ = raii;
+  }
+
+  void RadiographyAlphaLayer::Render(Orthanc::ImageAccessor& buffer,
+                                     const AffineTransform2D& viewTransform,
+                                     ImageInterpolation interpolation) const
+  {
+    if (alpha_.get() == NULL)
+    {
+      return;
+    }
+
+    if (buffer.GetFormat() != Orthanc::PixelFormat_Float32)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
+    }
+
+    unsigned int cropX, cropY, cropWidth, cropHeight;
+    GetCrop(cropX, cropY, cropWidth, cropHeight);
+
+    const AffineTransform2D t = AffineTransform2D::Combine(
+          viewTransform, GetTransform(),
+          AffineTransform2D::CreateOffset(cropX, cropY));
+
+    Orthanc::ImageAccessor cropped;
+    alpha_->GetRegion(cropped, cropX, cropY, cropWidth, cropHeight);
+
+    Orthanc::Image tmp(Orthanc::PixelFormat_Grayscale8, buffer.GetWidth(), buffer.GetHeight(), false);
+
+    t.Apply(tmp, cropped, interpolation, true /* clear */);
+
+    // Blit
+    const unsigned int width = buffer.GetWidth();
+    const unsigned int height = buffer.GetHeight();
+
+    float value = foreground_;
+
+    if (useWindowing_)
+    {
+      float center, width;
+      if (scene_.GetWindowing(center, width))
+      {
+        value = center + width / 2.0f;  // set it to the maximum pixel value of the image
+      }
+    }
+
+    for (unsigned int y = 0; y < height; y++)
+    {
+      float *q = reinterpret_cast<float*>(buffer.GetRow(y));
+      const uint8_t *p = reinterpret_cast<uint8_t*>(tmp.GetRow(y));
+
+      for (unsigned int x = 0; x < width; x++, p++, q++)
+      {
+        float a = static_cast<float>(*p) / 255.0f;
+
+        *q = (a * value + (1.0f - a) * (*q));
+      }
+    }
+  }
+
+  bool RadiographyAlphaLayer::GetRange(float& minValue,
+                                       float& maxValue) const
+  {
+    if (useWindowing_)
+    {
+      return false;
+    }
+    else
+    {
+      minValue = 0;
+      maxValue = 0;
+
+      if (foreground_ < 0)
+      {
+        minValue = foreground_;
+      }
+
+      if (foreground_ > 0)
+      {
+        maxValue = foreground_;
+      }
+
+      return true;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Radiography/RadiographyAlphaLayer.h	Wed Jan 16 16:10:16 2019 +0100
@@ -0,0 +1,89 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public 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 "RadiographyLayer.h"
+
+namespace OrthancStone
+{
+  class RadiographyScene;
+
+  // creates a transparent layer whose alpha channel is provided as a UINT8 image to SetAlpha.
+  // The color of the "mask" is either defined by a ForegroundValue or by the center value of the
+  // windowing from the scene.
+  class RadiographyAlphaLayer : public RadiographyLayer
+  {
+  private:
+    const RadiographyScene&                scene_;
+    std::auto_ptr<Orthanc::ImageAccessor>  alpha_;      // Grayscale8
+    bool                                   useWindowing_;
+    float                                  foreground_;
+
+  public:
+    RadiographyAlphaLayer(const RadiographyScene& scene) :
+      scene_(scene),
+      useWindowing_(true),
+      foreground_(0)
+    {
+    }
+
+
+    void SetForegroundValue(float foreground)
+    {
+      useWindowing_ = false;
+      foreground_ = foreground;
+    }
+
+    float GetForegroundValue() const
+    {
+      return foreground_;
+    }
+
+    bool IsUsingWindowing() const
+    {
+      return useWindowing_;
+    }
+
+    void SetAlpha(Orthanc::ImageAccessor* image);
+
+    virtual bool GetDefaultWindowing(float& center,
+                                     float& width) const
+    {
+      return false;
+    }
+
+
+    virtual void Render(Orthanc::ImageAccessor& buffer,
+                        const AffineTransform2D& viewTransform,
+                        ImageInterpolation interpolation) const;
+
+    virtual bool GetRange(float& minValue,
+                          float& maxValue) const;
+
+    const Orthanc::ImageAccessor& GetAlpha() const
+    {
+      return *(alpha_.get());
+    }
+
+
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Radiography/RadiographyDicomLayer.cpp	Wed Jan 16 16:10:16 2019 +0100
@@ -0,0 +1,169 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public 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 "RadiographyDicomLayer.h"
+
+#include "RadiographyScene.h"
+#include "../Toolbox/DicomFrameConverter.h"
+
+#include <Core/OrthancException.h>
+#include <Core/Images/Image.h>
+#include <Core/Images/ImageProcessing.h>
+#include <Plugins/Samples/Common/DicomDatasetReader.h>
+
+static OrthancPlugins::DicomTag  ConvertTag(const Orthanc::DicomTag& tag)
+{
+  return OrthancPlugins::DicomTag(tag.GetGroup(), tag.GetElement());
+}
+
+namespace OrthancStone
+{
+
+  void RadiographyDicomLayer::ApplyConverter()
+  {
+    if (source_.get() != NULL &&
+        converter_.get() != NULL)
+    {
+      converted_.reset(converter_->ConvertFrame(*source_));
+    }
+  }
+
+
+  void RadiographyDicomLayer::SetDicomTags(const OrthancPlugins::FullOrthancDataset& dataset)
+  {
+    converter_.reset(new DicomFrameConverter);
+    converter_->ReadParameters(dataset);
+    ApplyConverter();
+
+    std::string tmp;
+    Vector pixelSpacing;
+
+    if (dataset.GetStringValue(tmp, ConvertTag(Orthanc::DICOM_TAG_PIXEL_SPACING)) &&
+        LinearAlgebra::ParseVector(pixelSpacing, tmp) &&
+        pixelSpacing.size() == 2)
+    {
+      SetPixelSpacing(pixelSpacing[0], pixelSpacing[1]);
+    }
+
+    OrthancPlugins::DicomDatasetReader reader(dataset);
+
+    unsigned int width, height;
+    if (!reader.GetUnsignedIntegerValue(width, ConvertTag(Orthanc::DICOM_TAG_COLUMNS)) ||
+        !reader.GetUnsignedIntegerValue(height, ConvertTag(Orthanc::DICOM_TAG_ROWS)))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+    else
+    {
+      SetSize(width, height);
+    }
+
+    if (dataset.GetStringValue(tmp, ConvertTag(Orthanc::DICOM_TAG_PHOTOMETRIC_INTERPRETATION)))
+    {
+      if (tmp == "MONOCHROME1")
+      {
+        SetPreferredPhotomotricDisplayMode(PhotometricDisplayMode_Monochrome1);
+      }
+      else if (tmp == "MONOCHROME2")
+      {
+        SetPreferredPhotomotricDisplayMode(PhotometricDisplayMode_Monochrome2);
+      }
+    }
+  }
+
+  void RadiographyDicomLayer::SetSourceImage(Orthanc::ImageAccessor* image)   // Takes ownership
+  {
+    std::auto_ptr<Orthanc::ImageAccessor> raii(image);
+
+    if (image == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+
+    SetSize(image->GetWidth(), image->GetHeight());
+
+    source_ = raii;
+    ApplyConverter();
+  }
+
+  void RadiographyDicomLayer::Render(Orthanc::ImageAccessor& buffer,
+                                     const AffineTransform2D& viewTransform,
+                                     ImageInterpolation interpolation) const
+  {
+    if (converted_.get() != NULL)
+    {
+      if (converted_->GetFormat() != Orthanc::PixelFormat_Float32)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+
+      unsigned int cropX, cropY, cropWidth, cropHeight;
+      GetCrop(cropX, cropY, cropWidth, cropHeight);
+
+      AffineTransform2D t = AffineTransform2D::Combine(
+            viewTransform, GetTransform(),
+            AffineTransform2D::CreateOffset(cropX, cropY));
+
+      Orthanc::ImageAccessor cropped;
+      converted_->GetRegion(cropped, cropX, cropY, cropWidth, cropHeight);
+
+      t.Apply(buffer, cropped, interpolation, false);
+    }
+  }
+
+
+  bool RadiographyDicomLayer::GetDefaultWindowing(float& center,
+                                                  float& width) const
+  {
+    if (converter_.get() != NULL &&
+        converter_->HasDefaultWindow())
+    {
+      center = static_cast<float>(converter_->GetDefaultWindowCenter());
+      width = static_cast<float>(converter_->GetDefaultWindowWidth());
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  bool RadiographyDicomLayer::GetRange(float& minValue,
+                                       float& maxValue) const
+  {
+    if (converted_.get() != NULL)
+    {
+      if (converted_->GetFormat() != Orthanc::PixelFormat_Float32)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+
+      Orthanc::ImageProcessing::GetMinMaxFloatValue(minValue, maxValue, *converted_);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Radiography/RadiographyDicomLayer.h	Wed Jan 16 16:10:16 2019 +0100
@@ -0,0 +1,74 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public 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 "RadiographyLayer.h"
+#include <Plugins/Samples/Common/FullOrthancDataset.h>
+
+namespace OrthancStone
+{
+  class RadiographyScene;
+  class DicomFrameConverter;
+
+  class RadiographyDicomLayer : public RadiographyLayer
+  {
+  private:
+    std::auto_ptr<Orthanc::ImageAccessor>  source_;  // Content of PixelData
+    std::auto_ptr<DicomFrameConverter>     converter_;
+    std::auto_ptr<Orthanc::ImageAccessor>  converted_;  // Float32
+    std::string                            instanceId_;
+    unsigned int                           frame_;
+
+    void ApplyConverter();
+
+  public:
+    void SetInstance(const std::string& instanceId, unsigned int frame)
+    {
+      instanceId_ = instanceId;
+      frame_ = frame;
+    }
+
+    std::string GetInstanceId() const
+    {
+      return instanceId_;
+    }
+
+    unsigned int GetFrame() const
+    {
+      return frame_;
+    }
+
+    void SetDicomTags(const OrthancPlugins::FullOrthancDataset& dataset);
+
+    void SetSourceImage(Orthanc::ImageAccessor* image);   // Takes ownership
+
+    virtual void Render(Orthanc::ImageAccessor& buffer,
+                        const AffineTransform2D& viewTransform,
+                        ImageInterpolation interpolation) const;
+
+    virtual bool GetDefaultWindowing(float& center,
+                                     float& width) const;
+
+    virtual bool GetRange(float& minValue,
+                          float& maxValue) const;
+  };
+}
--- a/Framework/Radiography/RadiographyLayer.cpp	Mon Dec 24 13:41:12 2018 +0100
+++ b/Framework/Radiography/RadiographyLayer.cpp	Wed Jan 16 16:10:16 2019 +0100
@@ -32,18 +32,41 @@
   }
 
 
+  RadiographyLayer::Geometry::Geometry() :
+    hasCrop_(false),
+    panX_(0),
+    panY_(0),
+    angle_(0),
+    resizeable_(false),
+    pixelSpacingX_(1),
+    pixelSpacingY_(1)
+  {
+
+  }
+
+  void RadiographyLayer::Geometry::GetCrop(unsigned int &x, unsigned int &y, unsigned int &width, unsigned int &height) const
+  {
+    if (!hasCrop_)
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);  // you should probably use RadiographyLayer::GetCrop() or at least call HasCrop() before
+
+    x = cropX_;
+    y = cropY_;
+    width = cropWidth_;
+    height = cropHeight_;
+  }
+
   void RadiographyLayer::UpdateTransform()
   {
-    transform_ = AffineTransform2D::CreateScaling(pixelSpacingX_, pixelSpacingY_);
+    transform_ = AffineTransform2D::CreateScaling(geometry_.GetPixelSpacingX(), geometry_.GetPixelSpacingY());
 
     double centerX, centerY;
     GetCenter(centerX, centerY);
 
     transform_ = AffineTransform2D::Combine(
-      AffineTransform2D::CreateOffset(panX_ + centerX, panY_ + centerY),
-      AffineTransform2D::CreateRotation(angle_),
-      AffineTransform2D::CreateOffset(-centerX, -centerY),
-      transform_);
+          AffineTransform2D::CreateOffset(geometry_.GetPanX() + centerX, geometry_.GetPanY() + centerY),
+          AffineTransform2D::CreateRotation(geometry_.GetAngle()),
+          AffineTransform2D::CreateOffset(-centerX, -centerY),
+          transform_);
 
     transformInverse_ = AffineTransform2D::Invert(transform_);
   }
@@ -73,28 +96,28 @@
 
     switch (corner)
     {
-      case Corner_TopLeft:
-        x = dx;
-        y = dy;
-        break;
+    case Corner_TopLeft:
+      x = dx;
+      y = dy;
+      break;
 
-      case Corner_TopRight:
-        x = dx + dwidth;
-        y = dy;
-        break;
+    case Corner_TopRight:
+      x = dx + dwidth;
+      y = dy;
+      break;
 
-      case Corner_BottomLeft:
-        x = dx;
-        y = dy + dheight;
-        break;
+    case Corner_BottomLeft:
+      x = dx;
+      y = dy + dheight;
+      break;
 
-      case Corner_BottomRight:
-        x = dx + dwidth;
-        y = dy + dheight;
-        break;
+    case Corner_BottomRight:
+      x = dx + dwidth;
+      y = dy + dheight;
+      break;
 
-      default:
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    default:
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
 
     transform_.Apply(x, y);
@@ -105,7 +128,7 @@
                                   double y) const
   {
     transformInverse_.Apply(x, y);
-        
+
     unsigned int cropX, cropY, cropWidth, cropHeight;
     GetCrop(cropX, cropY, cropWidth, cropHeight);
 
@@ -127,7 +150,7 @@
 
     cairo_t* cr = context.GetObject();
     cairo_set_line_width(cr, 2.0 / zoom);
-        
+
     double x, y;
     x = dx;
     y = dy;
@@ -163,17 +186,16 @@
     hasSize_(false),
     width_(0),
     height_(0),
-    hasCrop_(false),
-    pixelSpacingX_(1),
-    pixelSpacingY_(1),
-    panX_(0),
-    panY_(0),
-    angle_(0),
-    resizeable_(false)
+    prefferedPhotometricDisplayMode_(PhotometricDisplayMode_Default)
   {
     UpdateTransform();
   }
 
+  void RadiographyLayer::ResetCrop()
+  {
+    geometry_.ResetCrop();
+    UpdateTransform();
+  }
 
   void RadiographyLayer::SetCrop(unsigned int x,
                                  unsigned int y,
@@ -184,34 +206,36 @@
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
     }
-        
+
     if (x + width > width_ ||
         y + height > height_)
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
-        
-    hasCrop_ = true;
-    cropX_ = x;
-    cropY_ = y;
-    cropWidth_ = width;
-    cropHeight_ = height;
 
+    geometry_.SetCrop(x, y, width, height);
     UpdateTransform();
   }
 
-      
+  void RadiographyLayer::SetGeometry(const Geometry& geometry)
+  {
+    geometry_ = geometry;
+
+    if (hasSize_)
+    {
+      UpdateTransform();
+    }
+  }
+
+
   void RadiographyLayer::GetCrop(unsigned int& x,
                                  unsigned int& y,
                                  unsigned int& width,
                                  unsigned int& height) const
   {
-    if (hasCrop_)
+    if (GetGeometry().HasCrop())
     {
-      x = cropX_;
-      y = cropY_;
-      width = cropWidth_;
-      height = cropHeight_;
+      GetGeometry().GetCrop(x, y, width, height);
     }
     else 
     {
@@ -222,10 +246,10 @@
     }
   }
 
-      
+  
   void RadiographyLayer::SetAngle(double angle)
   {
-    angle_ = angle;
+    geometry_.SetAngle(angle);
     UpdateTransform();
   }
 
@@ -239,7 +263,7 @@
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize);
     }
-        
+
     hasSize_ = true;
     width_ = width;
     height_ = height;
@@ -251,7 +275,7 @@
   Extent2D RadiographyLayer::GetExtent() const
   {
     Extent2D extent;
-       
+
     unsigned int x, y, width, height;
     GetCrop(x, y, width, height);
 
@@ -264,7 +288,7 @@
     AddToExtent(extent, dx + dwidth, dy);
     AddToExtent(extent, dx, dy + dheight);
     AddToExtent(extent, dx + dwidth, dy + dheight);
-        
+
     return extent;
   }
 
@@ -282,7 +306,7 @@
     else
     {
       transformInverse_.Apply(sceneX, sceneY);
-        
+
       int x = static_cast<int>(std::floor(sceneX));
       int y = static_cast<int>(std::floor(sceneY));
 
@@ -320,8 +344,7 @@
   void RadiographyLayer::SetPan(double x,
                                 double y)
   {
-    panX_ = x;
-    panY_ = y;
+    geometry_.SetPan(x, y);
     UpdateTransform();
   }
 
@@ -329,8 +352,7 @@
   void RadiographyLayer::SetPixelSpacing(double x,
                                          double y)
   {
-    pixelSpacingX_ = x;
-    pixelSpacingY_ = y;
+    geometry_.SetPixelSpacing(x, y);
     UpdateTransform();
   }
 
@@ -352,8 +374,8 @@
     GetCrop(cropX, cropY, cropWidth, cropHeight);
     GetCornerInternal(x, y, corner, cropX, cropY, cropWidth, cropHeight);
   }
-      
-      
+
+
   bool RadiographyLayer::LookupCorner(Corner& corner /* out */,
                                       double x,
                                       double y,
@@ -366,26 +388,26 @@
       Corner_BottomLeft,
       Corner_BottomRight
     };
-        
+
     unsigned int cropX, cropY, cropWidth, cropHeight;
     GetCrop(cropX, cropY, cropWidth, cropHeight);
 
     double threshold = Square(viewportDistance / zoom);
-        
+
     for (size_t i = 0; i < 4; i++)
     {
       double cx, cy;
       GetCornerInternal(cx, cy, CORNERS[i], cropX, cropY, cropWidth, cropHeight);
 
       double d = Square(cx - x) + Square(cy - y);
-        
+
       if (d <= threshold)
       {
         corner = CORNERS[i];
         return true;
       }
     }
-        
+
     return false;
   }
 }
--- a/Framework/Radiography/RadiographyLayer.h	Mon Dec 24 13:41:12 2018 +0100
+++ b/Framework/Radiography/RadiographyLayer.h	Wed Jan 16 16:10:16 2019 +0100
@@ -30,25 +30,118 @@
   class RadiographyLayer : public boost::noncopyable
   {
     friend class RadiographyScene;
-      
+
+  public:
+    class Geometry
+    {
+      bool               hasCrop_;
+      unsigned int       cropX_;
+      unsigned int       cropY_;
+      unsigned int       cropWidth_;
+      unsigned int       cropHeight_;
+      double             panX_;
+      double             panY_;
+      double             angle_;
+      bool               resizeable_;
+      double             pixelSpacingX_;
+      double             pixelSpacingY_;
+
+    public:
+      Geometry();
+
+      void ResetCrop()
+      {
+        hasCrop_ = false;
+      }
+
+      void SetCrop(unsigned int x,
+                   unsigned int y,
+                   unsigned int width,
+                   unsigned int height)
+      {
+        hasCrop_ = true;
+        cropX_ = x;
+        cropY_ = y;
+        cropWidth_ = width;
+        cropHeight_ = height;
+      }
+
+      bool HasCrop() const
+      {
+        return hasCrop_;
+      }
+
+      void GetCrop(unsigned int& x,
+                   unsigned int& y,
+                   unsigned int& width,
+                   unsigned int& height) const;
+
+      void SetAngle(double angle)
+      {
+        angle_ = angle;
+      }
+
+      double GetAngle() const
+      {
+        return angle_;
+      }
+
+      void SetPan(double x,
+                  double y)
+      {
+        panX_ = x;
+        panY_ = y;
+      }
+
+      double GetPanX() const
+      {
+        return panX_;
+      }
+
+      double GetPanY() const
+      {
+        return panY_;
+      }
+
+      bool IsResizeable() const
+      {
+        return resizeable_;
+      }
+
+      void SetResizeable(bool resizeable)
+      {
+        resizeable_ = resizeable;
+      }
+
+      void SetPixelSpacing(double x,
+                           double y)
+      {
+        pixelSpacingX_ = x;
+        pixelSpacingY_ = y;
+      }
+
+      double GetPixelSpacingX() const
+      {
+        return pixelSpacingX_;
+      }
+
+      double GetPixelSpacingY() const
+      {
+        return pixelSpacingY_;
+      }
+
+    };
+
   private:
     size_t             index_;
     bool               hasSize_;
     unsigned int       width_;
     unsigned int       height_;
-    bool               hasCrop_;
-    unsigned int       cropX_;
-    unsigned int       cropY_;
-    unsigned int       cropWidth_;
-    unsigned int       cropHeight_;
     AffineTransform2D  transform_;
     AffineTransform2D  transformInverse_;
-    double             pixelSpacingX_;
-    double             pixelSpacingY_;
-    double             panX_;
-    double             panY_;
-    double             angle_;
-    bool               resizeable_;
+    Geometry           geometry_;
+    PhotometricDisplayMode  prefferedPhotometricDisplayMode_;
+
 
   protected:
     const AffineTransform2D& GetTransform() const
@@ -56,6 +149,11 @@
       return transform_;
     }
 
+    void SetPreferredPhotomotricDisplayMode(PhotometricDisplayMode  prefferedPhotometricDisplayMode)
+    {
+      prefferedPhotometricDisplayMode_ = prefferedPhotometricDisplayMode;
+    }
+
   private:
     void UpdateTransform();
       
@@ -94,11 +192,15 @@
       return index_;
     }
 
-    void ResetCrop()
+    const Geometry& GetGeometry() const
     {
-      hasCrop_ = false;
+      return geometry_;
     }
 
+    void SetGeometry(const Geometry& geometry);
+
+    void ResetCrop();
+
     void SetCrop(unsigned int x,
                  unsigned int y,
                  unsigned int width,
@@ -111,14 +213,22 @@
 
     void SetAngle(double angle);
 
-    double GetAngle() const
+    void SetPan(double x,
+                double y);
+
+    void SetResizeable(bool resizeable)
     {
-      return angle_;
+      geometry_.SetResizeable(resizeable);
     }
 
     void SetSize(unsigned int width,
                  unsigned int height);
 
+    bool HasSize() const
+    {
+      return hasSize_;
+    }
+
     unsigned int GetWidth() const
     {
       return width_;
@@ -136,32 +246,9 @@
                   double sceneX,
                   double sceneY) const;
 
-    void SetPan(double x,
-                double y);
-
     void SetPixelSpacing(double x,
                          double y);
 
-    double GetPixelSpacingX() const
-    {
-      return pixelSpacingX_;
-    }   
-
-    double GetPixelSpacingY() const
-    {
-      return pixelSpacingY_;
-    }   
-
-    double GetPanX() const
-    {
-      return panX_;
-    }
-
-    double GetPanY() const
-    {
-      return panY_;
-    }
-
     void GetCenter(double& centerX,
                    double& centerY) const;
 
@@ -175,19 +262,14 @@
                       double zoom,
                       double viewportDistance) const;
 
-    bool IsResizeable() const
-    {
-      return resizeable_;
-    }
-
-    void SetResizeable(bool resizeable)
-    {
-      resizeable_ = resizeable;
-    }
-
     virtual bool GetDefaultWindowing(float& center,
                                      float& width) const = 0;
 
+    PhotometricDisplayMode GetPreferredPhotomotricDisplayMode() const
+    {
+      return prefferedPhotometricDisplayMode_;
+    }
+
     virtual void Render(Orthanc::ImageAccessor& buffer,
                         const AffineTransform2D& viewTransform,
                         ImageInterpolation interpolation) const = 0;
--- a/Framework/Radiography/RadiographyLayerCropTracker.cpp	Mon Dec 24 13:41:12 2018 +0100
+++ b/Framework/Radiography/RadiographyLayerCropTracker.cpp	Wed Jan 16 16:10:16 2019 +0100
@@ -77,7 +77,7 @@
   {
     if (accessor_.IsValid())
     {
-      accessor_.GetLayer().GetCrop(cropX_, cropY_, cropWidth_, cropHeight_);          
+      accessor_.GetLayer().GetCrop(cropX_, cropY_, cropWidth_, cropHeight_);
     }
   }
 
--- a/Framework/Radiography/RadiographyLayerMoveTracker.cpp	Mon Dec 24 13:41:12 2018 +0100
+++ b/Framework/Radiography/RadiographyLayerMoveTracker.cpp	Wed Jan 16 16:10:16 2019 +0100
@@ -51,8 +51,8 @@
       RadiographySceneCommand(tracker.accessor_),
       sourceX_(tracker.panX_),
       sourceY_(tracker.panY_),
-      targetX_(tracker.accessor_.GetLayer().GetPanX()),
-      targetY_(tracker.accessor_.GetLayer().GetPanY())
+      targetX_(tracker.accessor_.GetLayer().GetGeometry().GetPanX()),
+      targetY_(tracker.accessor_.GetLayer().GetGeometry().GetPanY())
     {
     }
   };
@@ -72,8 +72,8 @@
   {
     if (accessor_.IsValid())
     {
-      panX_ = accessor_.GetLayer().GetPanX();
-      panY_ = accessor_.GetLayer().GetPanY();
+      panX_ = accessor_.GetLayer().GetGeometry().GetPanX();
+      panY_ = accessor_.GetLayer().GetGeometry().GetPanY();
     }
   }
 
--- a/Framework/Radiography/RadiographyLayerResizeTracker.cpp	Mon Dec 24 13:41:12 2018 +0100
+++ b/Framework/Radiography/RadiographyLayerResizeTracker.cpp	Wed Jan 16 16:10:16 2019 +0100
@@ -73,10 +73,10 @@
       sourceSpacingY_(tracker.originalSpacingY_),
       sourcePanX_(tracker.originalPanX_),
       sourcePanY_(tracker.originalPanY_),
-      targetSpacingX_(tracker.accessor_.GetLayer().GetPixelSpacingX()),
-      targetSpacingY_(tracker.accessor_.GetLayer().GetPixelSpacingY()),
-      targetPanX_(tracker.accessor_.GetLayer().GetPanX()),
-      targetPanY_(tracker.accessor_.GetLayer().GetPanY())
+      targetSpacingX_(tracker.accessor_.GetLayer().GetGeometry().GetPixelSpacingX()),
+      targetSpacingY_(tracker.accessor_.GetLayer().GetGeometry().GetPixelSpacingY()),
+      targetPanX_(tracker.accessor_.GetLayer().GetGeometry().GetPanX()),
+      targetPanY_(tracker.accessor_.GetLayer().GetGeometry().GetPanY())
     {
     }
   };
@@ -94,12 +94,12 @@
     roundScaling_(roundScaling)
   {
     if (accessor_.IsValid() &&
-        accessor_.GetLayer().IsResizeable())
+        accessor_.GetLayer().GetGeometry().IsResizeable())
     {
-      originalSpacingX_ = accessor_.GetLayer().GetPixelSpacingX();
-      originalSpacingY_ = accessor_.GetLayer().GetPixelSpacingY();
-      originalPanX_ = accessor_.GetLayer().GetPanX();
-      originalPanY_ = accessor_.GetLayer().GetPanY();
+      originalSpacingX_ = accessor_.GetLayer().GetGeometry().GetPixelSpacingX();
+      originalSpacingY_ = accessor_.GetLayer().GetGeometry().GetPixelSpacingY();
+      originalPanX_ = accessor_.GetLayer().GetGeometry().GetPanX();
+      originalPanY_ = accessor_.GetLayer().GetGeometry().GetPanY();
 
       switch (corner)
       {
@@ -149,7 +149,7 @@
   void RadiographyLayerResizeTracker::MouseUp()
   {
     if (accessor_.IsValid() &&
-        accessor_.GetLayer().IsResizeable())
+        accessor_.GetLayer().GetGeometry().IsResizeable())
     {
       undoRedoStack_.Add(new UndoRedoCommand(*this));
     }
@@ -164,7 +164,7 @@
     static const double ROUND_SCALING = 0.1;
         
     if (accessor_.IsValid() &&
-        accessor_.GetLayer().IsResizeable())
+        accessor_.GetLayer().GetGeometry().IsResizeable())
     {
       double scaling = ComputeDistance(oppositeX_, oppositeY_, sceneX, sceneY) * baseScaling_;
 
@@ -180,8 +180,8 @@
       // Keep the opposite corner at a fixed location
       double ox, oy;
       layer.GetCorner(ox, oy, oppositeCorner_);
-      layer.SetPan(layer.GetPanX() + oppositeX_ - ox,
-                   layer.GetPanY() + oppositeY_ - oy);
+      layer.SetPan(layer.GetGeometry().GetPanX() + oppositeX_ - ox,
+                   layer.GetGeometry().GetPanY() + oppositeY_ - oy);
     }
   }
 }
--- a/Framework/Radiography/RadiographyLayerRotateTracker.cpp	Mon Dec 24 13:41:12 2018 +0100
+++ b/Framework/Radiography/RadiographyLayerRotateTracker.cpp	Wed Jan 16 16:10:16 2019 +0100
@@ -58,7 +58,7 @@
     UndoRedoCommand(const RadiographyLayerRotateTracker& tracker) :
       RadiographySceneCommand(tracker.accessor_),
       sourceAngle_(tracker.originalAngle_),
-      targetAngle_(tracker.accessor_.GetLayer().GetAngle())
+      targetAngle_(tracker.accessor_.GetLayer().GetGeometry().GetAngle())
     {
     }
   };
@@ -100,7 +100,7 @@
     if (accessor_.IsValid())
     {
       accessor_.GetLayer().GetCenter(centerX_, centerY_);
-      originalAngle_ = accessor_.GetLayer().GetAngle();
+      originalAngle_ = accessor_.GetLayer().GetGeometry().GetAngle();
 
       double sceneX, sceneY;
       view.MapDisplayToScene(sceneX, sceneY, x, y);
--- a/Framework/Radiography/RadiographyScene.cpp	Mon Dec 24 13:41:12 2018 +0100
+++ b/Framework/Radiography/RadiographyScene.cpp	Wed Jan 16 16:10:16 2019 +0100
@@ -21,6 +21,9 @@
 
 #include "RadiographyScene.h"
 
+#include "RadiographyAlphaLayer.h"
+#include "RadiographyDicomLayer.h"
+#include "RadiographyTextLayer.h"
 #include "../Toolbox/DicomFrameConverter.h"
 
 #include <Core/Images/Image.h>
@@ -55,7 +58,7 @@
     }
   }
 
-      
+
   RadiographyScene::LayerAccessor::LayerAccessor(RadiographyScene& scene,
                                                  double x,
                                                  double y) :
@@ -65,7 +68,7 @@
     if (scene.LookupLayer(index_, x, y))
     {
       Layers::iterator layer = scene.layers_.find(index_);
-          
+
       if (layer == scene.layers_.end())
       {
         throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
@@ -119,289 +122,7 @@
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
     }
-  }    
-
-
-
-  class RadiographyScene::AlphaLayer : public RadiographyLayer
-  {
-  private:
-    const RadiographyScene&                scene_;
-    std::auto_ptr<Orthanc::ImageAccessor>  alpha_;      // Grayscale8
-    bool                                   useWindowing_;
-    float                                  foreground_;
-
-  public:
-    AlphaLayer(const RadiographyScene& scene) :
-      scene_(scene),
-      useWindowing_(true),
-      foreground_(0)
-    {
-    }
-
-
-    void SetForegroundValue(float foreground)
-    {
-      useWindowing_ = false;
-      foreground_ = foreground;
-    }
-      
-      
-    void SetAlpha(Orthanc::ImageAccessor* image)
-    {
-      std::auto_ptr<Orthanc::ImageAccessor> raii(image);
-        
-      if (image == NULL)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-      }
-
-      if (image->GetFormat() != Orthanc::PixelFormat_Grayscale8)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
-      }
-
-      SetSize(image->GetWidth(), image->GetHeight());
-      alpha_ = raii;
-    }
-
-
-    void LoadText(const Orthanc::Font& font,
-                  const std::string& utf8)
-    {
-      SetAlpha(font.RenderAlpha(utf8));
-    }                   
-
-
-    virtual bool GetDefaultWindowing(float& center,
-                                     float& width) const
-    {
-      return false;
-    }
-      
-
-    virtual void Render(Orthanc::ImageAccessor& buffer,
-                        const AffineTransform2D& viewTransform,
-                        ImageInterpolation interpolation) const
-    {
-      if (alpha_.get() == NULL)
-      {
-        return;
-      }
-        
-      if (buffer.GetFormat() != Orthanc::PixelFormat_Float32)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
-      }
-
-      unsigned int cropX, cropY, cropWidth, cropHeight;
-      GetCrop(cropX, cropY, cropWidth, cropHeight);
-
-      const AffineTransform2D t = AffineTransform2D::Combine(
-        viewTransform, GetTransform(),
-        AffineTransform2D::CreateOffset(cropX, cropY));
-
-      Orthanc::ImageAccessor cropped;
-      alpha_->GetRegion(cropped, cropX, cropY, cropWidth, cropHeight);
-        
-      Orthanc::Image tmp(Orthanc::PixelFormat_Grayscale8, buffer.GetWidth(), buffer.GetHeight(), false);
-      
-      t.Apply(tmp, cropped, interpolation, true /* clear */);
-
-      // Blit
-      const unsigned int width = buffer.GetWidth();
-      const unsigned int height = buffer.GetHeight();
-
-      float value = foreground_;
-        
-      if (useWindowing_)
-      {
-        float center, width;
-        if (scene_.GetWindowing(center, width))
-        {
-          value = center + width / 2.0f;
-        }
-      }
-        
-      for (unsigned int y = 0; y < height; y++)
-      {
-        float *q = reinterpret_cast<float*>(buffer.GetRow(y));
-        const uint8_t *p = reinterpret_cast<uint8_t*>(tmp.GetRow(y));
-
-        for (unsigned int x = 0; x < width; x++, p++, q++)
-        {
-          float a = static_cast<float>(*p) / 255.0f;
-            
-          *q = (a * value + (1.0f - a) * (*q));
-        }
-      }        
-    }
-
-      
-    virtual bool GetRange(float& minValue,
-                          float& maxValue) const
-    {
-      if (useWindowing_)
-      {
-        return false;
-      }
-      else
-      {
-        minValue = 0;
-        maxValue = 0;
-
-        if (foreground_ < 0)
-        {
-          minValue = foreground_;
-        }
-
-        if (foreground_ > 0)
-        {
-          maxValue = foreground_;
-        }
-
-        return true;
-      }
-    }
-  };
-    
-    
-
-  class RadiographyScene::DicomLayer : public RadiographyLayer
-  {
-  private:
-    std::auto_ptr<Orthanc::ImageAccessor>  source_;  // Content of PixelData
-    std::auto_ptr<DicomFrameConverter>     converter_;
-    std::auto_ptr<Orthanc::ImageAccessor>  converted_;  // Float32
-
-    static OrthancPlugins::DicomTag  ConvertTag(const Orthanc::DicomTag& tag)
-    {
-      return OrthancPlugins::DicomTag(tag.GetGroup(), tag.GetElement());
-    }
-      
-
-    void ApplyConverter()
-    {
-      if (source_.get() != NULL &&
-          converter_.get() != NULL)
-      {
-        converted_.reset(converter_->ConvertFrame(*source_));
-      }
-    }
-      
-  public:
-    void SetDicomTags(const OrthancPlugins::FullOrthancDataset& dataset)
-    {
-      converter_.reset(new DicomFrameConverter);
-      converter_->ReadParameters(dataset);
-      ApplyConverter();
-
-      std::string tmp;
-      Vector pixelSpacing;
-        
-      if (dataset.GetStringValue(tmp, ConvertTag(Orthanc::DICOM_TAG_PIXEL_SPACING)) &&
-          LinearAlgebra::ParseVector(pixelSpacing, tmp) &&
-          pixelSpacing.size() == 2)
-      {
-        SetPixelSpacing(pixelSpacing[0], pixelSpacing[1]);
-      }
-
-      //SetPan(-0.5 * GetPixelSpacingX(), -0.5 * GetPixelSpacingY());
-      
-      OrthancPlugins::DicomDatasetReader reader(dataset);
-
-      unsigned int width, height;
-      if (!reader.GetUnsignedIntegerValue(width, ConvertTag(Orthanc::DICOM_TAG_COLUMNS)) ||
-          !reader.GetUnsignedIntegerValue(height, ConvertTag(Orthanc::DICOM_TAG_ROWS)))
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
-      }
-      else
-      {
-        SetSize(width, height);
-      }
-    }
-
-      
-    void SetSourceImage(Orthanc::ImageAccessor* image)   // Takes ownership
-    {
-      std::auto_ptr<Orthanc::ImageAccessor> raii(image);
-        
-      if (image == NULL)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-      }
-
-      SetSize(image->GetWidth(), image->GetHeight());
-        
-      source_ = raii;
-      ApplyConverter();
-    }
-
-      
-    virtual void Render(Orthanc::ImageAccessor& buffer,
-                        const AffineTransform2D& viewTransform,
-                        ImageInterpolation interpolation) const
-    {
-      if (converted_.get() != NULL)
-      {
-        if (converted_->GetFormat() != Orthanc::PixelFormat_Float32)
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-        }
-
-        unsigned int cropX, cropY, cropWidth, cropHeight;
-        GetCrop(cropX, cropY, cropWidth, cropHeight);
-
-        AffineTransform2D t = AffineTransform2D::Combine(
-          viewTransform, GetTransform(),
-          AffineTransform2D::CreateOffset(cropX, cropY));
-
-        Orthanc::ImageAccessor cropped;
-        converted_->GetRegion(cropped, cropX, cropY, cropWidth, cropHeight);
-        
-        t.Apply(buffer, cropped, interpolation, false);
-      }
-    }
-
-
-    virtual bool GetDefaultWindowing(float& center,
-                                     float& width) const
-    {
-      if (converter_.get() != NULL &&
-          converter_->HasDefaultWindow())
-      {
-        center = static_cast<float>(converter_->GetDefaultWindowCenter());
-        width = static_cast<float>(converter_->GetDefaultWindowWidth());
-        return true;
-      }
-      else
-      {
-        return false;
-      }
-    }
-
-
-    virtual bool GetRange(float& minValue,
-                          float& maxValue) const
-    {
-      if (converted_.get() != NULL)
-      {
-        if (converted_->GetFormat() != Orthanc::PixelFormat_Float32)
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-        }
-
-        Orthanc::ImageProcessing::GetMinMaxFloatValue(minValue, maxValue, *converted_);
-        return true;
-      }
-      else
-      {
-        return false;
-      }
-    }
-  };
-
+  }
 
   RadiographyLayer& RadiographyScene::RegisterLayer(RadiographyLayer* layer)
   {
@@ -411,17 +132,19 @@
     }
 
     std::auto_ptr<RadiographyLayer> raii(layer);
-      
+
+    LOG(INFO) << "Registering layer: " << countLayers_;
+
     size_t index = countLayers_++;
     raii->SetIndex(index);
     layers_[index] = raii.release();
 
-    EmitMessage(GeometryChangedMessage(*this));
-    EmitMessage(ContentChangedMessage(*this));
+    EmitMessage(GeometryChangedMessage(*this, *layer));
+    EmitMessage(ContentChangedMessage(*this, *layer));
 
     return *layer;
   }
-    
+
 
   RadiographyScene::RadiographyScene(MessageBroker& broker) :
     IObserver(broker),
@@ -443,6 +166,52 @@
     }
   }
 
+  PhotometricDisplayMode RadiographyScene::GetPreferredPhotomotricDisplayMode() const
+  {
+    // return the mode of the first layer who "cares" about its display mode (normaly, the one and only layer that is a DicomLayer)
+    for (Layers::const_iterator it = layers_.begin(); it != layers_.end(); it++)
+    {
+      if (it->second->GetPreferredPhotomotricDisplayMode() != PhotometricDisplayMode_Default)
+      {
+        return it->second->GetPreferredPhotomotricDisplayMode();
+      }
+    }
+
+    return PhotometricDisplayMode_Default;
+  }
+
+
+  void RadiographyScene::GetLayersIndexes(std::vector<size_t>& output) const
+  {
+    for (Layers::const_iterator it = layers_.begin(); it != layers_.end(); it++)
+    {
+      output.push_back(it->first);
+    }
+  }
+
+  void RadiographyScene::RemoveLayer(size_t layerIndex)
+  {
+    LOG(INFO) << "Removing layer: " << layerIndex;
+
+    if (layerIndex > countLayers_)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    delete layers_[layerIndex];
+    layers_.erase(layerIndex);
+    countLayers_--;
+    LOG(INFO) << "Removing layer, there are now : " << countLayers_ << " layers";
+  }
+
+  const RadiographyLayer& RadiographyScene::GetLayer(size_t layerIndex) const
+  {
+    if (layerIndex > countLayers_)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    return *(layers_.at(layerIndex));
+  }
 
   bool RadiographyScene::GetWindowing(float& center,
                                       float& width) const
@@ -481,17 +250,23 @@
 
 
   RadiographyLayer& RadiographyScene::LoadText(const Orthanc::Font& font,
-                                               const std::string& utf8)
+                                               const std::string& utf8,
+                                               RadiographyLayer::Geometry* geometry)
   {
-    std::auto_ptr<AlphaLayer>  alpha(new AlphaLayer(*this));
+    std::auto_ptr<RadiographyTextLayer>  alpha(new RadiographyTextLayer(*this));
     alpha->LoadText(font, utf8);
+    if (geometry != NULL)
+    {
+      alpha->SetGeometry(*geometry);
+    }
 
     return RegisterLayer(alpha.release());
   }
 
-    
+
   RadiographyLayer& RadiographyScene::LoadTestBlock(unsigned int width,
-                                                    unsigned int height)
+                                                    unsigned int height,
+                                                    RadiographyLayer::Geometry* geometry)
   {
     std::auto_ptr<Orthanc::Image>  block(new Orthanc::Image(Orthanc::PixelFormat_Grayscale8, width, height, false));
 
@@ -514,29 +289,44 @@
       Orthanc::ImageProcessing::Set(region, color);
     }
 
-    std::auto_ptr<AlphaLayer>  alpha(new AlphaLayer(*this));
-    alpha->SetAlpha(block.release());
+    return LoadAlphaBitmap(block.release(), geometry);
+  }
+
+  RadiographyLayer& RadiographyScene::LoadAlphaBitmap(Orthanc::ImageAccessor* bitmap, RadiographyLayer::Geometry *geometry)
+  {
+    std::auto_ptr<RadiographyAlphaLayer>  alpha(new RadiographyAlphaLayer(*this));
+    alpha->SetAlpha(bitmap);
+    if (geometry != NULL)
+    {
+      alpha->SetGeometry(*geometry);
+    }
 
     return RegisterLayer(alpha.release());
   }
 
-    
   RadiographyLayer& RadiographyScene::LoadDicomFrame(OrthancApiClient& orthanc,
                                                      const std::string& instance,
                                                      unsigned int frame,
-                                                     bool httpCompression)
+                                                     bool httpCompression,
+                                                     RadiographyLayer::Geometry* geometry)
   {
-    RadiographyLayer& layer = RegisterLayer(new DicomLayer);
+    RadiographyDicomLayer& layer = dynamic_cast<RadiographyDicomLayer&>(RegisterLayer(new RadiographyDicomLayer));
+    layer.SetInstance(instance, frame);
+
+    if (geometry != NULL)
+    {
+      layer.SetGeometry(*geometry);
+    }
 
     {
       IWebService::HttpHeaders headers;
       std::string uri = "/instances/" + instance + "/tags";
-        
+
       orthanc.GetBinaryAsync(
-        uri, headers,
-        new Callable<RadiographyScene, OrthancApiClient::BinaryResponseReadyMessage>
-        (*this, &RadiographyScene::OnTagsReceived), NULL,
-        new Orthanc::SingleValueObject<size_t>(layer.GetIndex()));
+            uri, headers,
+            new Callable<RadiographyScene, OrthancApiClient::BinaryResponseReadyMessage>
+            (*this, &RadiographyScene::OnTagsReceived), NULL,
+            new Orthanc::SingleValueObject<size_t>(layer.GetIndex()));
     }
 
     {
@@ -547,15 +337,15 @@
       {
         headers["Accept-Encoding"] = "gzip";
       }
-        
+
       std::string uri = ("/instances/" + instance + "/frames/" +
                          boost::lexical_cast<std::string>(frame) + "/image-uint16");
-        
+
       orthanc.GetBinaryAsync(
-        uri, headers,
-        new Callable<RadiographyScene, OrthancApiClient::BinaryResponseReadyMessage>
-        (*this, &RadiographyScene::OnFrameReceived), NULL,
-        new Orthanc::SingleValueObject<size_t>(layer.GetIndex()));
+            uri, headers,
+            new Callable<RadiographyScene, OrthancApiClient::BinaryResponseReadyMessage>
+            (*this, &RadiographyScene::OnFrameReceived), NULL,
+            new Orthanc::SingleValueObject<size_t>(layer.GetIndex()));
     }
 
     return layer;
@@ -564,29 +354,29 @@
 
   RadiographyLayer& RadiographyScene::LoadDicomWebFrame(IWebService& web)
   {
-    RadiographyLayer& layer = RegisterLayer(new DicomLayer);
+    RadiographyLayer& layer = RegisterLayer(new RadiographyDicomLayer);
 
-      
+
     return layer;
   }
 
 
-    
+
   void RadiographyScene::OnTagsReceived(const OrthancApiClient::BinaryResponseReadyMessage& message)
   {
     size_t index = dynamic_cast<const Orthanc::SingleValueObject<size_t>&>
-      (message.GetPayload()).GetValue();
+        (message.GetPayload()).GetValue();
 
     LOG(INFO) << "JSON received: " << message.GetUri().c_str()
               << " (" << message.GetAnswerSize() << " bytes) for layer " << index;
-      
+
     Layers::iterator layer = layers_.find(index);
     if (layer != layers_.end())
     {
       assert(layer->second != NULL);
-        
+
       OrthancPlugins::FullOrthancDataset dicom(message.GetAnswer(), message.GetAnswerSize());
-      dynamic_cast<DicomLayer*>(layer->second)->SetDicomTags(dicom);
+      dynamic_cast<RadiographyDicomLayer*>(layer->second)->SetDicomTags(dicom);
 
       float c, w;
       if (!hasWindowing_ &&
@@ -597,18 +387,18 @@
         windowingWidth_ = w;
       }
 
-      EmitMessage(GeometryChangedMessage(*this));
+      EmitMessage(GeometryChangedMessage(*this, *(layer->second)));
     }
   }
-    
+
 
   void RadiographyScene::OnFrameReceived(const OrthancApiClient::BinaryResponseReadyMessage& message)
   {
     size_t index = dynamic_cast<const Orthanc::SingleValueObject<size_t>&>(message.GetPayload()).GetValue();
-      
+
     LOG(INFO) << "DICOM frame received: " << message.GetUri().c_str()
               << " (" << message.GetAnswerSize() << " bytes) for layer " << index;
-      
+
     Layers::iterator layer = layers_.find(index);
     if (layer != layers_.end())
     {
@@ -619,12 +409,12 @@
       {
         content.assign(reinterpret_cast<const char*>(message.GetAnswer()), message.GetAnswerSize());
       }
-        
+
       std::auto_ptr<Orthanc::PamReader> reader(new Orthanc::PamReader);
       reader->ReadFromMemory(content);
-      dynamic_cast<DicomLayer*>(layer->second)->SetSourceImage(reader.release());
+      dynamic_cast<RadiographyDicomLayer*>(layer->second)->SetSourceImage(reader.release());
 
-      EmitMessage(ContentChangedMessage(*this));
+      EmitMessage(ContentChangedMessage(*this, *(layer->second)));
     }
   }
 
@@ -642,14 +432,12 @@
 
     return extent;
   }
-    
+
 
   void RadiographyScene::Render(Orthanc::ImageAccessor& buffer,
                                 const AffineTransform2D& viewTransform,
                                 ImageInterpolation interpolation) const
   {
-    Orthanc::ImageProcessing::Set(buffer, 0);
-
     // Render layers in the background-to-foreground order
     for (size_t index = 0; index < countLayers_; index++)
     {
@@ -685,13 +473,13 @@
     return false;
   }
 
-    
+
   void RadiographyScene::DrawBorder(CairoContext& context,
                                     unsigned int layer,
                                     double zoom)
   {
     Layers::const_iterator found = layers_.find(layer);
-        
+
     if (found != layers_.end())
     {
       context.SetSourceColor(255, 0, 0);
@@ -704,7 +492,7 @@
                                   float& maxValue) const
   {
     bool first = true;
-      
+
     for (Layers::const_iterator it = layers_.begin();
          it != layers_.end(); it++)
     {
@@ -735,22 +523,47 @@
   }
 
 
-  // Export using PAM is faster than using PNG, but requires Orthanc
-  // core >= 1.4.3
   void RadiographyScene::ExportDicom(OrthancApiClient& orthanc,
                                      const Orthanc::DicomMap& dicom,
+                                     const std::string& parentOrthancId,
                                      double pixelSpacingX,
                                      double pixelSpacingY,
                                      bool invert,
                                      ImageInterpolation interpolation,
                                      bool usePam)
   {
+    Json::Value createDicomRequestContent;
+
+    ExportToCreateDicomRequest(createDicomRequestContent, dicom, pixelSpacingX, pixelSpacingY, invert, interpolation, usePam);
+
+    if (!parentOrthancId.empty())
+    {
+      createDicomRequestContent["Parent"] = parentOrthancId;
+    }
+
+    orthanc.PostJsonAsyncExpectJson(
+          "/tools/create-dicom", createDicomRequestContent,
+          new Callable<RadiographyScene, OrthancApiClient::JsonResponseReadyMessage>
+          (*this, &RadiographyScene::OnDicomExported),
+          NULL, NULL);
+  }
+
+  // Export using PAM is faster than using PNG, but requires Orthanc
+  // core >= 1.4.3
+  void RadiographyScene::ExportToCreateDicomRequest(Json::Value& createDicomRequestContent,
+                                const Orthanc::DicomMap& dicom,
+                                double pixelSpacingX,
+                                double pixelSpacingY,
+                                bool invert,
+                                ImageInterpolation interpolation,
+                                bool usePam)
+  {
     if (pixelSpacingX <= 0 ||
         pixelSpacingY <= 0)
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
-      
+
     LOG(INFO) << "Exporting DICOM";
 
     Extent2D extent = GetSceneExtent();
@@ -768,9 +581,12 @@
                           static_cast<unsigned int>(h), false);
 
     AffineTransform2D view = AffineTransform2D::Combine(
-      AffineTransform2D::CreateScaling(1.0 / pixelSpacingX, 1.0 / pixelSpacingY),
-      AffineTransform2D::CreateOffset(-extent.GetX1(), -extent.GetY1()));
-      
+          AffineTransform2D::CreateScaling(1.0 / pixelSpacingX, 1.0 / pixelSpacingY),
+          AffineTransform2D::CreateOffset(-extent.GetX1(), -extent.GetY1()));
+
+    // wipe background before rendering
+    Orthanc::ImageProcessing::Set(layers, 0);
+
     Render(layers, view, interpolation);
 
     Orthanc::Image rendered(Orthanc::PixelFormat_Grayscale16,
@@ -799,22 +615,29 @@
     std::set<Orthanc::DicomTag> tags;
     dicom.GetTags(tags);
 
-    Json::Value json = Json::objectValue;
-    json["Tags"] = Json::objectValue;
-           
+    createDicomRequestContent["Tags"] = Json::objectValue;
+
     for (std::set<Orthanc::DicomTag>::const_iterator
-           tag = tags.begin(); tag != tags.end(); ++tag)
+         tag = tags.begin(); tag != tags.end(); ++tag)
     {
       const Orthanc::DicomValue& value = dicom.GetValue(*tag);
       if (!value.IsNull() &&
           !value.IsBinary())
       {
-        json["Tags"][tag->Format()] = value.GetContent();
+        createDicomRequestContent["Tags"][tag->Format()] = value.GetContent();
       }
     }
 
-    json["Tags"][Orthanc::DICOM_TAG_PHOTOMETRIC_INTERPRETATION.Format()] =
-      (invert ? "MONOCHROME1" : "MONOCHROME2");
+    PhotometricDisplayMode photometricMode = GetPreferredPhotomotricDisplayMode();
+    if ((invert && photometricMode != PhotometricDisplayMode_Monochrome2) ||
+        (!invert && photometricMode == PhotometricDisplayMode_Monochrome1))
+    {
+      createDicomRequestContent["Tags"][Orthanc::DICOM_TAG_PHOTOMETRIC_INTERPRETATION.Format()] = "MONOCHROME1";
+    }
+    else
+    {
+      createDicomRequestContent["Tags"][Orthanc::DICOM_TAG_PHOTOMETRIC_INTERPRETATION.Format()] = "MONOCHROME2";
+    }
 
     // WARNING: The order of PixelSpacing is Y/X. We use "%0.8f" to
     // avoid floating-point numbers to grow over 16 characters,
@@ -822,29 +645,24 @@
     // ("dciodvfy" would complain).
     char buf[32];
     sprintf(buf, "%0.8f\\%0.8f", pixelSpacingY, pixelSpacingX);
-      
-    json["Tags"][Orthanc::DICOM_TAG_PIXEL_SPACING.Format()] = buf;
+
+    createDicomRequestContent["Tags"][Orthanc::DICOM_TAG_PIXEL_SPACING.Format()] = buf;
 
     float center, width;
     if (GetWindowing(center, width))
     {
-      json["Tags"][Orthanc::DICOM_TAG_WINDOW_CENTER.Format()] =
-        boost::lexical_cast<std::string>(boost::math::iround(center));
+      createDicomRequestContent["Tags"][Orthanc::DICOM_TAG_WINDOW_CENTER.Format()] =
+          boost::lexical_cast<std::string>(boost::math::iround(center));
 
-      json["Tags"][Orthanc::DICOM_TAG_WINDOW_WIDTH.Format()] =
-        boost::lexical_cast<std::string>(boost::math::iround(width));
+      createDicomRequestContent["Tags"][Orthanc::DICOM_TAG_WINDOW_WIDTH.Format()] =
+          boost::lexical_cast<std::string>(boost::math::iround(width));
     }
 
+
     // This is Data URI scheme: https://en.wikipedia.org/wiki/Data_URI_scheme
-    json["Content"] = ("data:" +
-                       std::string(usePam ? Orthanc::MIME_PAM : Orthanc::MIME_PNG) +
-                       ";base64," + base64);
-
-    orthanc.PostJsonAsyncExpectJson(
-      "/tools/create-dicom", json,
-      new Callable<RadiographyScene, OrthancApiClient::JsonResponseReadyMessage>
-      (*this, &RadiographyScene::OnDicomExported),
-      NULL, NULL);
+    createDicomRequestContent["Content"] = ("data:" +
+                                            std::string(usePam ? Orthanc::MIME_PAM : Orthanc::MIME_PNG) +
+                                            ";base64," + base64);
   }
 
 
@@ -861,7 +679,7 @@
 
     const IWebService::HttpHeaders& h = message.GetAnswerHttpHeaders();
     for (IWebService::HttpHeaders::const_iterator
-           it = h.begin(); it != h.end(); ++it)
+         it = h.begin(); it != h.end(); ++it)
     {
       printf("[%s] = [%s]\n", it->first.c_str(), it->second.c_str());
     }
--- a/Framework/Radiography/RadiographyScene.h	Mon Dec 24 13:41:12 2018 +0100
+++ b/Framework/Radiography/RadiographyScene.h	Wed Jan 16 16:10:16 2019 +0100
@@ -23,17 +23,55 @@
 
 #include "RadiographyLayer.h"
 #include "../Toolbox/OrthancApiClient.h"
-
+#include "Framework/StoneEnumerations.h"
 
 namespace OrthancStone
 {
   class RadiographyScene :
-    public IObserver,
-    public IObservable
+      public IObserver,
+      public IObservable
   {
   public:
-    typedef OriginMessage<MessageType_Widget_GeometryChanged, RadiographyScene> GeometryChangedMessage;
-    typedef OriginMessage<MessageType_Widget_ContentChanged, RadiographyScene> ContentChangedMessage;
+    class GeometryChangedMessage :
+        public OriginMessage<MessageType_Scene_GeometryChanged, RadiographyScene>
+    {
+    private:
+      RadiographyLayer&        layer_;
+
+    public:
+      GeometryChangedMessage(const RadiographyScene& origin,
+                             RadiographyLayer& layer) :
+        OriginMessage(origin),
+        layer_(layer)
+      {
+      }
+
+      RadiographyLayer& GetLayer() const
+      {
+        return layer_;
+      }
+    };
+
+    class ContentChangedMessage :
+        public OriginMessage<MessageType_Scene_ContentChanged, RadiographyScene>
+    {
+    private:
+      RadiographyLayer&        layer_;
+
+    public:
+      ContentChangedMessage(const RadiographyScene& origin,
+                            RadiographyLayer& layer) :
+        OriginMessage(origin),
+        layer_(layer)
+      {
+      }
+
+      RadiographyLayer& GetLayer() const
+      {
+        return layer_;
+      }
+    };
+
 
     class LayerAccessor : public boost::noncopyable
     {
@@ -69,17 +107,15 @@
 
 
   private:
-    class AlphaLayer;    
-    class DicomLayer;
+    typedef std::map<size_t, RadiographyLayer*>  Layers;
 
-    typedef std::map<size_t, RadiographyLayer*>  Layers;
-        
     size_t  countLayers_;
     bool    hasWindowing_;
     float   windowingCenter_;
     float   windowingWidth_;
     Layers  layers_;
 
+  protected:
     RadiographyLayer& RegisterLayer(RadiographyLayer* layer);
 
     void OnTagsReceived(const OrthancApiClient::BinaryResponseReadyMessage& message);
@@ -104,19 +140,33 @@
     void SetWindowing(float center,
                       float width);
 
+    PhotometricDisplayMode GetPreferredPhotomotricDisplayMode() const;
+
     RadiographyLayer& LoadText(const Orthanc::Font& font,
-                               const std::string& utf8);
+                               const std::string& utf8,
+                               RadiographyLayer::Geometry* geometry);
     
     RadiographyLayer& LoadTestBlock(unsigned int width,
-                                    unsigned int height);
-    
-    RadiographyLayer& LoadDicomFrame(OrthancApiClient& orthanc,
-                                     const std::string& instance,
-                                     unsigned int frame,
-                                     bool httpCompression);
+                                    unsigned int height,
+                                    RadiographyLayer::Geometry* geometry);
+
+    RadiographyLayer& LoadAlphaBitmap(Orthanc::ImageAccessor* bitmap,  // takes ownership
+                                      RadiographyLayer::Geometry* geometry);
+
+    virtual RadiographyLayer& LoadDicomFrame(OrthancApiClient& orthanc,
+                                             const std::string& instance,
+                                             unsigned int frame,
+                                             bool httpCompression,
+                                             RadiographyLayer::Geometry* geometry); // pass NULL if you want default geometry
 
     RadiographyLayer& LoadDicomWebFrame(IWebService& web);
 
+    void RemoveLayer(size_t layerIndex);
+
+    const RadiographyLayer& GetLayer(size_t layerIndex) const;
+
+    void GetLayersIndexes(std::vector<size_t>& output) const;
+
     Extent2D GetSceneExtent() const;
 
     void Render(Orthanc::ImageAccessor& buffer,
@@ -138,10 +188,20 @@
     // core >= 1.4.3
     void ExportDicom(OrthancApiClient& orthanc,
                      const Orthanc::DicomMap& dicom,
+                     const std::string& parentOrthancId,
                      double pixelSpacingX,
                      double pixelSpacingY,
                      bool invert,
                      ImageInterpolation interpolation,
                      bool usePam);
+
+    // temporary version used by VSOL because we need to send the same request at another url
+    void ExportToCreateDicomRequest(Json::Value& createDicomRequestContent,
+                                    const Orthanc::DicomMap& dicom,
+                                    double pixelSpacingX,
+                                    double pixelSpacingY,
+                                    bool invert,
+                                    ImageInterpolation interpolation,
+                                    bool usePam);
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Radiography/RadiographySceneReader.cpp	Wed Jan 16 16:10:16 2019 +0100
@@ -0,0 +1,112 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public 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 "RadiographySceneReader.h"
+
+#include <Core/Images/FontRegistry.h>
+#include <Core/Images/PngReader.h>
+#include <Core/OrthancException.h>
+#include <Core/Toolbox.h>
+
+namespace OrthancStone
+{
+  void RadiographySceneReader::Read(const Json::Value& input)
+  {
+    unsigned int version = input["version"].asUInt();
+
+    if (version != 1)
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+
+    for(size_t layerIndex = 0; layerIndex < input["layers"].size(); layerIndex++)
+    {
+      const Json::Value& jsonLayer = input["layers"][(int)layerIndex];
+      RadiographyLayer::Geometry geometry;
+
+      if (jsonLayer["type"].asString() == "dicom")
+      {
+        ReadLayerGeometry(geometry, jsonLayer);
+        scene_.LoadDicomFrame(orthancApiClient_, jsonLayer["instanceId"].asString(), jsonLayer["frame"].asUInt(), false, &geometry);
+      }
+      else if (jsonLayer["type"].asString() == "text")
+      {
+        if (fontRegistry_ == NULL || fontRegistry_->GetSize() == 0)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); // you must provide a FontRegistry if you need to re-create text layers.
+        }
+
+        ReadLayerGeometry(geometry, jsonLayer);
+        const Orthanc::Font* font = fontRegistry_->FindFont(jsonLayer["fontName"].asString());
+        if (font == NULL) // if not found, take the first font in the registry
+        {
+          font = &(fontRegistry_->GetFont(0));
+        }
+        scene_.LoadText(*font, jsonLayer["text"].asString(), &geometry);
+      }
+      else if (jsonLayer["type"].asString() == "alpha")
+      {
+        ReadLayerGeometry(geometry, jsonLayer);
+
+        const std::string& pngContentBase64 = jsonLayer["content"].asString();
+        std::string pngContent;
+        std::string mimeType;
+        Orthanc::Toolbox::DecodeDataUriScheme(mimeType, pngContent, pngContentBase64);
+
+        std::auto_ptr<Orthanc::ImageAccessor>  image;
+        if (mimeType == "image/png")
+        {
+          image.reset(new Orthanc::PngReader());
+          dynamic_cast<Orthanc::PngReader*>(image.get())->ReadFromMemory(pngContent);
+        }
+        else
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+
+        RadiographyAlphaLayer& layer = dynamic_cast<RadiographyAlphaLayer&>(scene_.LoadAlphaBitmap(image.release(), &geometry));
+
+        if (!jsonLayer["isUsingWindowing"].asBool())
+        {
+          layer.SetForegroundValue((float)(jsonLayer["foreground"].asDouble()));
+        }
+      }
+      else
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+  }
+
+  void RadiographySceneReader::ReadLayerGeometry(RadiographyLayer::Geometry& geometry, const Json::Value& jsonLayer)
+  {
+    {// crop
+      unsigned int x, y, width, height;
+      if (jsonLayer["crop"]["hasCrop"].asBool())
+      {
+        x = jsonLayer["crop"]["x"].asUInt();
+        y = jsonLayer["crop"]["y"].asUInt();
+        width = jsonLayer["crop"]["width"].asUInt();
+        height = jsonLayer["crop"]["height"].asUInt();
+        geometry.SetCrop(x, y, width, height);
+      }
+    }
+
+    geometry.SetAngle(jsonLayer["angle"].asDouble());
+    geometry.SetResizeable(jsonLayer["isResizable"].asBool());
+    geometry.SetPan(jsonLayer["pan"]["x"].asDouble(), jsonLayer["pan"]["y"].asDouble());
+    geometry.SetPixelSpacing(jsonLayer["pixelSpacing"]["x"].asDouble(), jsonLayer["pixelSpacing"]["y"].asDouble());
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Radiography/RadiographySceneReader.h	Wed Jan 16 16:10:16 2019 +0100
@@ -0,0 +1,59 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public 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 "RadiographyScene.h"
+#include "RadiographyAlphaLayer.h"
+#include "RadiographyDicomLayer.h"
+#include "RadiographyTextLayer.h"
+#include <json/value.h>
+#include <Core/Images/FontRegistry.h>
+
+namespace OrthancStone
+{
+  class OrthancApiClient;
+
+  class RadiographySceneReader : public boost::noncopyable
+  {
+    RadiographyScene&             scene_;
+    OrthancApiClient&             orthancApiClient_;
+    const Orthanc::FontRegistry*  fontRegistry_;
+
+  public:
+    RadiographySceneReader(RadiographyScene& scene, OrthancApiClient& orthancApiClient) :
+      scene_(scene),
+      orthancApiClient_(orthancApiClient),
+      fontRegistry_(NULL)
+    {
+    }
+
+    void Read(const Json::Value& output);
+
+    void SetFontRegistry(const Orthanc::FontRegistry& fontRegistry)
+    {
+      fontRegistry_ = &fontRegistry;
+    }
+
+  private:
+    void ReadLayerGeometry(RadiographyLayer::Geometry& geometry, const Json::Value& output);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Radiography/RadiographySceneWriter.cpp	Wed Jan 16 16:10:16 2019 +0100
@@ -0,0 +1,136 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public 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 "RadiographySceneWriter.h"
+
+#include <Core/OrthancException.h>
+#include <Core/Images/PngWriter.h>
+#include <Core/Toolbox.h>
+
+namespace OrthancStone
+{
+  void RadiographySceneWriter::Write(Json::Value& output, const RadiographyScene& scene)
+  {
+    output["version"] = 1;
+    output["layers"] = Json::arrayValue;
+
+    std::vector<size_t> layersIndexes;
+    scene.GetLayersIndexes(layersIndexes);
+
+    for (std::vector<size_t>::iterator itLayerIndex = layersIndexes.begin(); itLayerIndex < layersIndexes.end(); itLayerIndex++)
+    {
+      Json::Value layer;
+      WriteLayer(layer, scene.GetLayer(*itLayerIndex));
+      output["layers"].append(layer);
+    }
+  }
+
+  void RadiographySceneWriter::WriteLayer(Json::Value& output, const RadiographyDicomLayer& layer)
+  {
+    output["type"] = "dicom";
+    output["instanceId"] = layer.GetInstanceId();
+    output["frame"] = layer.GetFrame();
+  }
+
+  void RadiographySceneWriter::WriteLayer(Json::Value& output, const RadiographyTextLayer& layer)
+  {
+    output["type"] = "text";
+    output["text"] = layer.GetText();
+    output["fontName"] = layer.GetFontName();
+  }
+
+  void RadiographySceneWriter::WriteLayer(Json::Value& output, const RadiographyAlphaLayer& layer)
+  {
+    output["type"] = "alpha";
+
+    //output["bitmap"] =
+    const Orthanc::ImageAccessor& alpha = layer.GetAlpha();
+
+    Orthanc::PngWriter pngWriter;
+    std::string pngContent;
+    std::string pngContentBase64;
+    pngWriter.WriteToMemory(pngContent, alpha);
+
+    Orthanc::Toolbox::EncodeDataUriScheme(pngContentBase64, "image/png", pngContent);
+    output["content"] = pngContentBase64;
+    output["foreground"] = layer.GetForegroundValue();
+    output["isUsingWindowing"] = layer.IsUsingWindowing();
+  }
+
+  void RadiographySceneWriter::WriteLayer(Json::Value& output, const RadiographyLayer& layer)
+  {
+    const RadiographyLayer::Geometry& geometry = layer.GetGeometry();
+
+    {// crop
+      Json::Value crop;
+      if (geometry.HasCrop())
+      {
+        unsigned int x, y, width, height;
+        geometry.GetCrop(x, y, width, height);
+        crop["hasCrop"] = true;
+        crop["x"] = x;
+        crop["y"] = y;
+        crop["width"] = width;
+        crop["height"] = height;
+      }
+      else
+      {
+        crop["hasCrop"] = false;
+      }
+
+      output["crop"] = crop;
+    }
+
+    output["angle"] = geometry.GetAngle();
+    output["isResizable"] = geometry.IsResizeable();
+
+    {// pan
+      Json::Value pan;
+      pan["x"] = geometry.GetPanX();
+      pan["y"] = geometry.GetPanY();
+      output["pan"] = pan;
+    }
+
+    {// pixelSpacing
+      Json::Value pan;
+      pan["x"] = geometry.GetPixelSpacingX();
+      pan["y"] = geometry.GetPixelSpacingY();
+      output["pixelSpacing"] = pan;
+    }
+
+    if (dynamic_cast<const RadiographyTextLayer*>(&layer) != NULL)
+    {
+      WriteLayer(output, dynamic_cast<const RadiographyTextLayer&>(layer));
+    }
+    else if (dynamic_cast<const RadiographyDicomLayer*>(&layer) != NULL)
+    {
+      WriteLayer(output, dynamic_cast<const RadiographyDicomLayer&>(layer));
+    }
+    else if (dynamic_cast<const RadiographyAlphaLayer*>(&layer) != NULL)
+    {
+      WriteLayer(output, dynamic_cast<const RadiographyAlphaLayer&>(layer));
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Radiography/RadiographySceneWriter.h	Wed Jan 16 16:10:16 2019 +0100
@@ -0,0 +1,50 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public 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 "RadiographyScene.h"
+#include "RadiographyAlphaLayer.h"
+#include "RadiographyDicomLayer.h"
+#include "RadiographyTextLayer.h"
+#include <json/value.h>
+
+namespace OrthancStone
+{
+  class RadiographyScene;
+
+  class RadiographySceneWriter : public boost::noncopyable
+  {
+
+  public:
+    RadiographySceneWriter()
+    {
+    }
+
+    void Write(Json::Value& output, const RadiographyScene& scene);
+
+  private:
+    void WriteLayer(Json::Value& output, const RadiographyLayer& layer);
+    void WriteLayer(Json::Value& output, const RadiographyDicomLayer& layer);
+    void WriteLayer(Json::Value& output, const RadiographyTextLayer& layer);
+    void WriteLayer(Json::Value& output, const RadiographyAlphaLayer& layer);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Radiography/RadiographyTextLayer.cpp	Wed Jan 16 16:10:16 2019 +0100
@@ -0,0 +1,36 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public 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 "RadiographyTextLayer.h"
+
+#include "RadiographyScene.h"
+
+namespace OrthancStone
+{
+  void RadiographyTextLayer::LoadText(const Orthanc::Font& font,
+                                      const std::string& utf8)
+  {
+    text_ = utf8;
+    fontName_ = font.GetName();
+
+    SetAlpha(font.RenderAlpha(utf8));
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Radiography/RadiographyTextLayer.h	Wed Jan 16 16:10:16 2019 +0100
@@ -0,0 +1,55 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public 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 "RadiographyAlphaLayer.h"
+
+namespace OrthancStone
+{
+  class RadiographyScene;
+
+  class RadiographyTextLayer : public RadiographyAlphaLayer
+  {
+  private:
+    std::string                text_;
+    std::string                fontName_;
+
+  public:
+    RadiographyTextLayer(const RadiographyScene& scene) :
+      RadiographyAlphaLayer(scene)
+    {
+    }
+
+    void LoadText(const Orthanc::Font& font,
+                  const std::string& utf8);
+
+    const std::string& GetText() const
+    {
+      return text_;
+    }
+
+    const std::string& GetFontName() const
+    {
+      return fontName_;
+    }
+  };
+}
--- a/Framework/Radiography/RadiographyWidget.cpp	Mon Dec 24 13:41:12 2018 +0100
+++ b/Framework/Radiography/RadiographyWidget.cpp	Wed Jan 16 16:10:16 2019 +0100
@@ -21,18 +21,53 @@
 
 #include "RadiographyWidget.h"
 
+#include <Core/OrthancException.h>
 #include <Core/Images/Image.h>
+#include <Core/Images/ImageProcessing.h>
 
 
 namespace OrthancStone
 {
+
+  bool RadiographyWidget::IsInvertedInternal() const
+  {
+    return (scene_->GetPreferredPhotomotricDisplayMode() == PhotometricDisplayMode_Monochrome1) ^ invert_; // MONOCHROME1 images must be inverted and the user can invert the image too -> XOR the two
+  }
+
+  void RadiographyWidget::RenderBackground(Orthanc::ImageAccessor& image, float minValue, float maxValue)
+  {
+    // wipe background before rendering
+    float backgroundValue = minValue;
+
+    switch (scene_->GetPreferredPhotomotricDisplayMode())
+    {
+    case PhotometricDisplayMode_Monochrome1:
+    case PhotometricDisplayMode_Default:
+      if (IsInvertedInternal())
+        backgroundValue = maxValue;
+      else
+        backgroundValue = minValue;
+      break;
+    case PhotometricDisplayMode_Monochrome2:
+      if (IsInvertedInternal())
+        backgroundValue = minValue;
+      else
+        backgroundValue = maxValue;
+      break;
+    default:
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+
+    Orthanc::ImageProcessing::Set(image, backgroundValue);
+  }
+
   bool RadiographyWidget::RenderInternal(unsigned int width,
                                          unsigned int height,
                                          ImageInterpolation interpolation)
   {
     float windowCenter, windowWidth;
-    scene_.GetWindowingWithDefault(windowCenter, windowWidth);
-      
+    scene_->GetWindowingWithDefault(windowCenter, windowWidth);
+
     float x0 = windowCenter - windowWidth / 2.0f;
     float x1 = windowCenter + windowWidth / 2.0f;
 
@@ -56,8 +91,10 @@
         cairoBuffer_.reset(new CairoSurface(width, height));
       }
 
-      scene_.Render(*floatBuffer_, GetView().GetMatrix(), interpolation);
-        
+      RenderBackground(*floatBuffer_, x0, x1);
+
+      scene_->Render(*floatBuffer_, GetView().GetMatrix(), interpolation);
+
       // Conversion from Float32 to BGRA32 (cairo). Very similar to
       // GrayscaleFrameRenderer => TODO MERGE?
 
@@ -65,7 +102,9 @@
       cairoBuffer_->GetWriteableAccessor(target);
 
       float scaling = 255.0f / (x1 - x0);
-        
+
+      bool invert = IsInvertedInternal();
+
       for (unsigned int y = 0; y < height; y++)
       {
         const float* p = reinterpret_cast<const float*>(floatBuffer_->GetConstRow(y));
@@ -88,7 +127,7 @@
             v = static_cast<uint8_t>(scaling * (*p - x0));  // (*)
           }
 
-          if (invert_)
+          if (invert)
           {
             v = 255 - v;
           }
@@ -128,7 +167,7 @@
 
     if (hasSelection_)
     {
-      scene_.DrawBorder(context, selectedLayer_, view.GetZoom());
+      scene_->DrawBorder(context, selectedLayer_, view.GetZoom());
     }
 
     return true;
@@ -136,23 +175,16 @@
 
 
   RadiographyWidget::RadiographyWidget(MessageBroker& broker,
-                                       RadiographyScene& scene,
+                                       boost::shared_ptr<RadiographyScene> scene,
                                        const std::string& name) :
     WorldSceneWidget(name),
     IObserver(broker),
-    scene_(scene),
     invert_(false),
     interpolation_(ImageInterpolation_Nearest),
     hasSelection_(false),
     selectedLayer_(0)    // Dummy initialization
   {
-    scene.RegisterObserverCallback(
-      new Callable<RadiographyWidget, RadiographyScene::GeometryChangedMessage>
-      (*this, &RadiographyWidget::OnGeometryChanged));
-
-    scene.RegisterObserverCallback(
-      new Callable<RadiographyWidget, RadiographyScene::ContentChangedMessage>
-      (*this, &RadiographyWidget::OnContentChanged));
+    SetScene(scene);
   }
 
 
@@ -179,14 +211,15 @@
   
   void RadiographyWidget::OnGeometryChanged(const RadiographyScene::GeometryChangedMessage& message)
   {
-    LOG(INFO) << "Geometry has changed";
+    LOG(INFO) << "Scene geometry has changed";
+
     FitContent();
   }
 
   
   void RadiographyWidget::OnContentChanged(const RadiographyScene::ContentChangedMessage& message)
   {
-    LOG(INFO) << "Content has changed";
+    LOG(INFO) << "Scene content has changed";
     NotifyContentChanged();
   }
 
@@ -216,4 +249,27 @@
       NotifyContentChanged();
     }
   }
+
+  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));
+
+    NotifyContentChanged();
+
+    // force redraw
+    FitContent();
+  }
 }
--- a/Framework/Radiography/RadiographyWidget.h	Mon Dec 24 13:41:12 2018 +0100
+++ b/Framework/Radiography/RadiographyWidget.h	Wed Jan 16 16:10:16 2019 +0100
@@ -32,7 +32,7 @@
     public IObserver
   {
   private:
-    RadiographyScene&                      scene_;
+    boost::shared_ptr<RadiographyScene>    scene_;
     std::auto_ptr<Orthanc::ImageAccessor>  floatBuffer_;
     std::auto_ptr<CairoSurface>            cairoBuffer_;
     bool                                   invert_;
@@ -47,22 +47,28 @@
   protected:
     virtual Extent2D GetSceneExtent()
     {
-      return scene_.GetSceneExtent();
+      return scene_->GetSceneExtent();
     }
 
     virtual bool RenderScene(CairoContext& context,
                              const ViewportGeometry& view);
 
+    virtual void RenderBackground(Orthanc::ImageAccessor& image, float minValue, float maxValue);
+
+    bool IsInvertedInternal() const;
+
   public:
     RadiographyWidget(MessageBroker& broker,
-                      RadiographyScene& scene,
+                      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
     {
-      return scene_;
+      return *scene_;
     }
 
+    void SetScene(boost::shared_ptr<RadiographyScene> scene);
+
     void Unselect()
     {
       hasSelection_ = false;
--- a/Framework/StoneEnumerations.h	Mon Dec 24 13:41:12 2018 +0100
+++ b/Framework/StoneEnumerations.h	Wed Jan 16 16:10:16 2019 +0100
@@ -157,8 +157,13 @@
     MessageType_OrthancApi_GenericHttpError_Ready,
     MessageType_OrthancApi_GenericEmptyResponse_Ready,
 
+    MessageType_Scene_GeometryChanged,
+    MessageType_Scene_ContentChanged,
+
     MessageType_ViewportChanged,
 
+    MessageType_Timeout,
+
     // used in unit tests only
     MessageType_Test1,
     MessageType_Test2,
@@ -175,6 +180,14 @@
     Corner_BottomRight
   };
 
+  enum PhotometricDisplayMode
+  {
+    PhotometricDisplayMode_Default,
+
+    PhotometricDisplayMode_Monochrome1,
+    PhotometricDisplayMode_Monochrome2
+  };
+
   
   bool StringToSopClassUid(SopClassUid& result,
                            const std::string& source);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Toolbox/BaseWebService.cpp	Wed Jan 16 16:10:16 2019 +0100
@@ -0,0 +1,143 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public 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 "BaseWebService.h"
+
+#include <Core/OrthancException.h>
+#include "Framework/Messages/IObservable.h"
+#include "Platforms/Generic/IOracleCommand.h"
+#include <boost/shared_ptr.hpp>
+
+namespace OrthancStone
+{
+
+
+  class BaseWebService::BaseWebServicePayload : public Orthanc::IDynamicObject
+  {
+  private:
+    std::auto_ptr< MessageHandler<IWebService::HttpRequestSuccessMessage> >   userSuccessHandler_;
+    std::auto_ptr< MessageHandler<IWebService::HttpRequestErrorMessage> >     userFailureHandler_;
+    std::auto_ptr< Orthanc::IDynamicObject>                                   userPayload_;
+
+  public:
+    BaseWebServicePayload(MessageHandler<IWebService::HttpRequestSuccessMessage>* userSuccessHandler,
+                          MessageHandler<IWebService::HttpRequestErrorMessage>* userFailureHandler,
+                          Orthanc::IDynamicObject* userPayload) :
+      userSuccessHandler_(userSuccessHandler),
+      userFailureHandler_(userFailureHandler),
+      userPayload_(userPayload)
+    {
+    }
+
+    void HandleSuccess(const IWebService::HttpRequestSuccessMessage& message) const
+    {
+      if (userSuccessHandler_.get() != NULL)
+      {
+        // recreate a success message with the user payload
+        IWebService::HttpRequestSuccessMessage successMessage(message.GetUri(),
+                                                              message.GetAnswer(),
+                                                              message.GetAnswerSize(),
+                                                              message.GetAnswerHttpHeaders(),
+                                                              userPayload_.get());
+        userSuccessHandler_->Apply(successMessage);
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+    }
+
+    void HandleFailure(const IWebService::HttpRequestErrorMessage& message) const
+    {
+      if (userFailureHandler_.get() != NULL)
+      {
+        // recreate a failure message with the user payload
+        IWebService::HttpRequestErrorMessage failureMessage(message.GetUri(),
+                                                            userPayload_.get());
+
+        userFailureHandler_->Apply(failureMessage);
+      }
+    }
+
+  };
+
+
+  void BaseWebService::GetAsync(const std::string& uri,
+                                const HttpHeaders& headers,
+                                Orthanc::IDynamicObject* payload  /* takes ownership */,
+                                MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,
+                                MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
+                                unsigned int timeoutInSeconds)
+  {
+    if (cache_.find(uri) == cache_.end())
+    {
+      GetAsyncInternal(uri, headers,
+                       new BaseWebService::BaseWebServicePayload(successCallback, failureCallback, payload), // ownership is transfered
+                       new Callable<BaseWebService, IWebService::HttpRequestSuccessMessage>
+                       (*this, &BaseWebService::CacheAndNotifyHttpSuccess),
+                       new Callable<BaseWebService, IWebService::HttpRequestErrorMessage>
+                       (*this, &BaseWebService::NotifyHttpError),
+                       timeoutInSeconds);
+    }
+    else
+    {
+      // create a command and "post" it to the Oracle so it is executed and commited "later"
+      NotifyHttpSuccessLater(cache_[uri], payload, successCallback);
+    }
+
+  }
+
+
+
+  void BaseWebService::NotifyHttpSuccess(const IWebService::HttpRequestSuccessMessage& message)
+  {
+    if (message.HasPayload())
+    {
+      dynamic_cast<const BaseWebServicePayload&>(message.GetPayload()).HandleSuccess(message);
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+  }
+
+  void BaseWebService::CacheAndNotifyHttpSuccess(const IWebService::HttpRequestSuccessMessage& message)
+  {
+    cache_[message.GetUri()] = boost::shared_ptr<CachedHttpRequestSuccessMessage>(new CachedHttpRequestSuccessMessage(message));
+    NotifyHttpSuccess(message);
+  }
+
+  void BaseWebService::NotifyHttpError(const IWebService::HttpRequestErrorMessage& message)
+  {
+    if (message.HasPayload())
+    {
+      dynamic_cast<const BaseWebServicePayload&>(message.GetPayload()).HandleFailure(message);
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+  }
+
+
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Toolbox/BaseWebService.h	Wed Jan 16 16:10:16 2019 +0100
@@ -0,0 +1,131 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public 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 "IWebService.h"
+
+#include <string>
+#include <map>
+
+namespace OrthancStone
+{
+  // This is an intermediate of IWebService that implements some caching on
+  // the HTTP GET requests
+  class BaseWebService : public IWebService, public IObserver
+  {
+  public:
+    class CachedHttpRequestSuccessMessage
+    {
+    protected:
+      std::string                    uri_;
+      void*                          answer_;
+      size_t                         answerSize_;
+      IWebService::HttpHeaders       answerHeaders_;
+
+    public:
+      CachedHttpRequestSuccessMessage(const IWebService::HttpRequestSuccessMessage& message) :
+        uri_(message.GetUri()),
+        answerSize_(message.GetAnswerSize()),
+        answerHeaders_(message.GetAnswerHttpHeaders())
+      {
+        answer_ =  malloc(answerSize_);
+        memcpy(answer_, message.GetAnswer(), answerSize_);
+      }
+
+      ~CachedHttpRequestSuccessMessage()
+      {
+        free(answer_);
+      }
+
+      const std::string& GetUri() const
+      {
+        return uri_;
+      }
+
+      const void* GetAnswer() const
+      {
+        return answer_;
+      }
+
+      size_t GetAnswerSize() const
+      {
+        return answerSize_;
+      }
+
+      const IWebService::HttpHeaders&  GetAnswerHttpHeaders() const
+      {
+        return answerHeaders_;
+      }
+
+    };
+  protected:
+    class BaseWebServicePayload;
+
+    bool          cacheEnabled_;
+    std::map<std::string, boost::shared_ptr<CachedHttpRequestSuccessMessage>> cache_;  // TODO: this is currently an infinite cache !
+
+  public:
+
+    BaseWebService(MessageBroker& broker) :
+      IWebService(broker),
+      IObserver(broker),
+      cacheEnabled_(true)
+    {
+    }
+
+    virtual ~BaseWebService()
+    {
+    }
+
+    virtual void EnableCache(bool enable)
+    {
+      cacheEnabled_ = enable;
+    }
+
+    virtual void GetAsync(const std::string& uri,
+                          const HttpHeaders& headers,
+                          Orthanc::IDynamicObject* payload  /* takes ownership */,
+                          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 */,
+                          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
+                                        MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback) = 0;
+
+  private:
+    void NotifyHttpSuccess(const IWebService::HttpRequestSuccessMessage& message);
+
+    void NotifyHttpError(const IWebService::HttpRequestErrorMessage& message);
+
+    void CacheAndNotifyHttpSuccess(const IWebService::HttpRequestSuccessMessage& message);
+
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Toolbox/IDelayedCallExecutor.h	Wed Jan 16 16:10:16 2019 +0100
@@ -0,0 +1,59 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public 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/Messages/IObserver.h"
+#include "../../Framework/Messages/ICallable.h"
+
+#include <Core/IDynamicObject.h>
+#include <Core/Logging.h>
+
+#include <string>
+#include <map>
+
+namespace OrthancStone
+{
+  // The IDelayedCall executes a callback after a delay (equivalent to timeout() function in javascript).
+  class IDelayedCallExecutor : public boost::noncopyable
+  {
+  protected:
+    MessageBroker& broker_;
+    
+  public:
+
+    typedef NoPayloadMessage<MessageType_Timeout> TimeoutMessage;
+
+    IDelayedCallExecutor(MessageBroker& broker) :
+      broker_(broker)
+    {
+    }
+
+    
+    virtual ~IDelayedCallExecutor()
+    {
+    }
+
+    
+    virtual void Schedule(MessageHandler<IDelayedCallExecutor::TimeoutMessage>* callback,
+                         unsigned int timeoutInMs = 1000) = 0;
+  };
+}
--- a/Framework/Toolbox/IWebService.h	Mon Dec 24 13:41:12 2018 +0100
+++ b/Framework/Toolbox/IWebService.h	Wed Jan 16 16:10:16 2019 +0100
@@ -135,6 +135,7 @@
     {
     }
 
+    virtual void EnableCache(bool enable) = 0;
     
     virtual void GetAsync(const std::string& uri,
                           const HttpHeaders& headers,
--- a/Framework/Toolbox/OrthancApiClient.cpp	Mon Dec 24 13:41:12 2018 +0100
+++ b/Framework/Toolbox/OrthancApiClient.cpp	Wed Jan 16 16:10:16 2019 +0100
@@ -154,7 +154,7 @@
       {
         throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
       }
-    }      
+    }
 
     void HandleFailure(const IWebService::HttpRequestErrorMessage& message) const
     {
@@ -163,7 +163,7 @@
         failureHandler_->Apply(IWebService::HttpRequestErrorMessage
                                (message.GetUri(), userPayload_.get()));
       }
-    }      
+    }
   };
 
 
@@ -179,13 +179,14 @@
 
 
   void OrthancApiClient::GetJsonAsync(
-    const std::string& uri,
-    MessageHandler<JsonResponseReadyMessage>* successCallback,
-    MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
-    Orthanc::IDynamicObject* payload)
+      const std::string& uri,
+      MessageHandler<JsonResponseReadyMessage>* successCallback,
+      MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
+      Orthanc::IDynamicObject* payload)
   {
+    IWebService::HttpHeaders emptyHeaders;
     web_.GetAsync(baseUrl_ + uri,
-                  IWebService::HttpHeaders(),
+                  emptyHeaders,
                   new WebServicePayload(successCallback, failureCallback, payload),
                   new Callable<OrthancApiClient, IWebService::HttpRequestSuccessMessage>
                   (*this, &OrthancApiClient::NotifyHttpSuccess),
@@ -195,27 +196,26 @@
 
 
   void OrthancApiClient::GetBinaryAsync(
-    const std::string& uri,
-    const std::string& contentType,
-    MessageHandler<BinaryResponseReadyMessage>* successCallback,
-    MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
-    Orthanc::IDynamicObject* payload)
+      const std::string& uri,
+      const std::string& contentType,
+      MessageHandler<BinaryResponseReadyMessage>* successCallback,
+      MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
+      Orthanc::IDynamicObject* payload)
   {
     IWebService::HttpHeaders headers;
     headers["Accept"] = contentType;
     GetBinaryAsync(uri, headers, successCallback, failureCallback, payload);
   }
-  
 
   void OrthancApiClient::GetBinaryAsync(
-    const std::string& uri,
-    const IWebService::HttpHeaders& headers,
-    MessageHandler<BinaryResponseReadyMessage>* successCallback,
-    MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
-    Orthanc::IDynamicObject* payload)
+      const std::string& uri,
+      const IWebService::HttpHeaders& headers,
+      MessageHandler<BinaryResponseReadyMessage>* successCallback,
+      MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
+      Orthanc::IDynamicObject* payload)
   {
-    printf("GET [%s] [%s]\n", baseUrl_.c_str(), uri.c_str());
-    
+    // printf("GET [%s] [%s]\n", baseUrl_.c_str(), uri.c_str());
+
     web_.GetAsync(baseUrl_ + uri, headers,
                   new WebServicePayload(successCallback, failureCallback, payload),
                   new Callable<OrthancApiClient, IWebService::HttpRequestSuccessMessage>
@@ -226,11 +226,11 @@
 
   
   void OrthancApiClient::PostBinaryAsyncExpectJson(
-    const std::string& uri,
-    const std::string& body,
-    MessageHandler<JsonResponseReadyMessage>* successCallback,
-    MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
-    Orthanc::IDynamicObject* payload)
+      const std::string& uri,
+      const std::string& body,
+      MessageHandler<JsonResponseReadyMessage>* successCallback,
+      MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
+      Orthanc::IDynamicObject* payload)
   {
     web_.PostAsync(baseUrl_ + uri, IWebService::HttpHeaders(), body,
                    new WebServicePayload(successCallback, failureCallback, payload),
@@ -241,25 +241,39 @@
 
   }
 
-  
+  void OrthancApiClient::PostBinaryAsync(
+      const std::string& uri,
+      const std::string& body)
+  {
+    web_.PostAsync(baseUrl_ + uri, IWebService::HttpHeaders(), body, NULL, NULL, NULL);
+  }
+
   void OrthancApiClient::PostJsonAsyncExpectJson(
-    const std::string& uri,
-    const Json::Value& data,
-    MessageHandler<JsonResponseReadyMessage>* successCallback,
-    MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
-    Orthanc::IDynamicObject* payload)
+      const std::string& uri,
+      const Json::Value& data,
+      MessageHandler<JsonResponseReadyMessage>* successCallback,
+      MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
+      Orthanc::IDynamicObject* payload)
   {
     std::string body;
     MessagingToolbox::JsonToString(body, data);
     return PostBinaryAsyncExpectJson(uri, body, successCallback, failureCallback, payload);
   }
 
-  
+  void OrthancApiClient::PostJsonAsync(
+      const std::string& uri,
+      const Json::Value& data)
+  {
+    std::string body;
+    MessagingToolbox::JsonToString(body, data);
+    return PostBinaryAsync(uri, body);
+  }
+
   void OrthancApiClient::DeleteAsync(
-    const std::string& uri,
-    MessageHandler<EmptyResponseReadyMessage>* successCallback,
-    MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
-    Orthanc::IDynamicObject* payload)
+      const std::string& uri,
+      MessageHandler<EmptyResponseReadyMessage>* successCallback,
+      MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback,
+      Orthanc::IDynamicObject* payload)
   {
     web_.DeleteAsync(baseUrl_ + uri, IWebService::HttpHeaders(),
                      new WebServicePayload(successCallback, failureCallback, payload),
@@ -282,7 +296,6 @@
     }
   }
 
-  
   void OrthancApiClient::NotifyHttpError(const IWebService::HttpRequestErrorMessage& message)
   {
     if (message.HasPayload())
@@ -293,5 +306,5 @@
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
     }
-  }    
+  }
 }
--- a/Framework/Toolbox/OrthancApiClient.h	Mon Dec 24 13:41:12 2018 +0100
+++ b/Framework/Toolbox/OrthancApiClient.h	Wed Jan 16 16:10:16 2019 +0100
@@ -31,12 +31,12 @@
 namespace OrthancStone
 {
   class OrthancApiClient :
-    public IObservable,
-    public IObserver
+      public IObservable,
+      public IObserver
   {
   public:
     class JsonResponseReadyMessage :
-      public BaseMessage<MessageType_OrthancApi_GenericGetJson_Ready>
+        public BaseMessage<MessageType_OrthancApi_GenericGetJson_Ready>
     {
     private:
       const std::string&              uri_;
@@ -73,7 +73,7 @@
     
 
     class BinaryResponseReadyMessage :
-      public BaseMessage<MessageType_OrthancApi_GenericGetBinary_Ready>
+        public BaseMessage<MessageType_OrthancApi_GenericGetBinary_Ready>
     {
     private:
       const std::string&              uri_;
@@ -118,7 +118,7 @@
 
 
     class EmptyResponseReadyMessage :
-      public BaseMessage<MessageType_OrthancApi_GenericEmptyResponse_Ready>
+        public BaseMessage<MessageType_OrthancApi_GenericEmptyResponse_Ready>
     {
     private:
       const std::string&              uri_;
@@ -149,7 +149,7 @@
 
   private:
     class WebServicePayload;
-    
+
   protected:
     IWebService&  web_;
     std::string   baseUrl_;
@@ -197,6 +197,14 @@
                                  MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,
                                  Orthanc::IDynamicObject* payload = NULL   /* takes ownership */);
 
+    // schedule a POST request and don't mind the response.
+    void PostJsonAsync(const std::string& uri,
+                       const Json::Value& data);
+
+    // schedule a POST request and don't mind the response.
+    void PostBinaryAsync(const std::string& uri,
+                         const std::string& body);
+
     // schedule a DELETE request expecting an empty response.
     void DeleteAsync(const std::string& uri,
                      MessageHandler<EmptyResponseReadyMessage>* successCallback,
@@ -206,5 +214,10 @@
     void NotifyHttpSuccess(const IWebService::HttpRequestSuccessMessage& message);
 
     void NotifyHttpError(const IWebService::HttpRequestErrorMessage& message);
+
+  private:
+    void HandleFromCache(const std::string& uri,
+                         const IWebService::HttpHeaders& headers,
+                         Orthanc::IDynamicObject* payload /* takes ownership */);
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Platforms/Generic/DelayedCallCommand.cpp	Wed Jan 16 16:10:16 2019 +0100
@@ -0,0 +1,66 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public 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 "DelayedCallCommand.h"
+#include "boost/thread/thread.hpp"
+
+#include <iostream>
+
+namespace OrthancStone
+{
+  DelayedCallCommand::DelayedCallCommand(MessageBroker& broker,
+                                         MessageHandler<IDelayedCallExecutor::TimeoutMessage>* callback,  // takes ownership
+                                         unsigned int timeoutInMs,
+                                         Orthanc::IDynamicObject* payload /* takes ownership */,
+                                         NativeStoneApplicationContext& context
+                                         ) :
+    IObservable(broker),
+    callback_(callback),
+    payload_(payload),
+    context_(context),
+    expirationTimePoint_(boost::chrono::system_clock::now() + boost::chrono::milliseconds(timeoutInMs)),
+    timeoutInMs_(timeoutInMs)
+  {
+  }
+
+
+  void DelayedCallCommand::Execute()
+  {
+    while (boost::chrono::system_clock::now() < expirationTimePoint_)
+    {
+      boost::this_thread::sleep_for(boost::chrono::milliseconds(1));
+    }
+  }
+
+  void DelayedCallCommand::Commit()
+  {
+    // We want to make sure that, i.e, the UpdateThread is not
+    // triggered while we are updating the "model" with the result of
+    // an OracleCommand
+    NativeStoneApplicationContext::GlobalMutexLocker lock(context_);
+
+    if (callback_.get() != NULL)
+    {
+      IDelayedCallExecutor::TimeoutMessage message; // TODO: add payload
+      callback_->Apply(message);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Platforms/Generic/DelayedCallCommand.h	Wed Jan 16 16:10:16 2019 +0100
@@ -0,0 +1,56 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public 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 "../../Framework/Toolbox/IDelayedCallExecutor.h"
+#include "../../Framework/Messages/IObservable.h"
+#include "../../Framework/Messages/ICallable.h"
+#include "../../Applications/Generic/NativeStoneApplicationContext.h"
+#include <boost/chrono.hpp>
+
+namespace OrthancStone
+{
+  class DelayedCallCommand : public IOracleCommand, IObservable
+  {
+  protected:
+    std::auto_ptr<MessageHandler<IDelayedCallExecutor::TimeoutMessage> >  callback_;
+    std::auto_ptr<Orthanc::IDynamicObject>  payload_;
+    NativeStoneApplicationContext&          context_;
+    boost::chrono::system_clock::time_point expirationTimePoint_;
+    unsigned int                            timeoutInMs_;
+
+  public:
+    DelayedCallCommand(MessageBroker& broker,
+                       MessageHandler<IDelayedCallExecutor::TimeoutMessage>* callback,  // takes ownership
+                       unsigned int timeoutInMs,
+                       Orthanc::IDynamicObject* payload /* takes ownership */,
+                       NativeStoneApplicationContext& context
+                       );
+
+    virtual void Execute();
+
+    virtual void Commit();
+  };
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Platforms/Generic/OracleDelayedCallExecutor.h	Wed Jan 16 16:10:16 2019 +0100
@@ -0,0 +1,54 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public 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/Toolbox/IDelayedCallExecutor.h"
+#include "Oracle.h"
+#include "../../Applications/Generic/NativeStoneApplicationContext.h"
+#include "DelayedCallCommand.h"
+
+namespace OrthancStone
+{
+  // The OracleTimeout executes callbacks after a delay.
+  class OracleDelayedCallExecutor : public IDelayedCallExecutor
+  {
+  private:
+    Oracle&                        oracle_;
+    NativeStoneApplicationContext& context_;
+
+  public:
+    OracleDelayedCallExecutor(MessageBroker& broker,
+                              Oracle& oracle,
+                              NativeStoneApplicationContext& context) :
+      IDelayedCallExecutor(broker),
+      oracle_(oracle),
+      context_(context)
+    {
+    }
+
+    virtual void Schedule(MessageHandler<IDelayedCallExecutor::TimeoutMessage>* callback,
+                          unsigned int timeoutInMs = 1000)
+    {
+      oracle_.Submit(new DelayedCallCommand(broker_, callback, timeoutInMs, NULL, context_));
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Platforms/Generic/OracleWebService.cpp	Wed Jan 16 16:10:16 2019 +0100
@@ -0,0 +1,82 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public 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 "OracleWebService.h"
+#include "../../Framework/Toolbox/IWebService.h"
+
+namespace OrthancStone
+{
+
+
+  class OracleWebService::WebServiceCachedGetCommand : public IOracleCommand, IObservable
+  {
+  protected:
+    std::auto_ptr<MessageHandler<IWebService::HttpRequestSuccessMessage> >  successCallback_;
+    std::auto_ptr<Orthanc::IDynamicObject>                                  payload_;
+    boost::shared_ptr<BaseWebService::CachedHttpRequestSuccessMessage>      cachedMessage_;
+    NativeStoneApplicationContext&                                          context_;
+
+  public:
+    WebServiceCachedGetCommand(MessageBroker& broker,
+                               MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,  // takes ownership
+                               boost::shared_ptr<BaseWebService::CachedHttpRequestSuccessMessage> cachedMessage,
+                               Orthanc::IDynamicObject* payload /* takes ownership */,
+                               NativeStoneApplicationContext& context
+                               ) :
+      IObservable(broker),
+      successCallback_(successCallback),
+      payload_(payload),
+      cachedMessage_(cachedMessage),
+      context_(context)
+    {
+    }
+
+    virtual void Execute()
+    {
+      // nothing to do, everything is in the commit
+    }
+
+    virtual void Commit()
+    {
+      // We want to make sure that, i.e, the UpdateThread is not
+      // triggered while we are updating the "model" with the result of
+      // a WebServiceCommand
+      NativeStoneApplicationContext::GlobalMutexLocker lock(context_);
+
+      IWebService::HttpRequestSuccessMessage successMessage(cachedMessage_->GetUri(),
+                                                            cachedMessage_->GetAnswer(),
+                                                            cachedMessage_->GetAnswerSize(),
+                                                            cachedMessage_->GetAnswerHttpHeaders(),
+                                                            payload_.get());
+
+      successCallback_->Apply(successMessage);
+    }
+  };
+
+  void OracleWebService::NotifyHttpSuccessLater(boost::shared_ptr<BaseWebService::CachedHttpRequestSuccessMessage> cachedMessage,
+                                                Orthanc::IDynamicObject* payload, // takes ownership
+                                                MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback)
+  {
+    oracle_.Submit(new WebServiceCachedGetCommand(GetBroker(), successCallback, cachedMessage, payload, context_));
+  }
+
+
+}
--- a/Platforms/Generic/OracleWebService.h	Mon Dec 24 13:41:12 2018 +0100
+++ b/Platforms/Generic/OracleWebService.h	Wed Jan 16 16:10:16 2019 +0100
@@ -21,7 +21,7 @@
 
 #pragma once
 
-#include "../../Framework/Toolbox/IWebService.h"
+#include "../../Framework/Toolbox/BaseWebService.h"
 #include "Oracle.h"
 #include "WebServiceGetCommand.h"
 #include "WebServicePostCommand.h"
@@ -33,35 +33,27 @@
   // The OracleWebService performs HTTP requests in a native environment.
   // It uses a thread pool to handle multiple HTTP requests in a same time.
   // It works asynchronously to mimick the behaviour of the WebService running in a WASM environment.
-  class OracleWebService : public IWebService
+  class OracleWebService : public BaseWebService
   {
   private:
     Oracle&                        oracle_;
     NativeStoneApplicationContext& context_;
     Orthanc::WebServiceParameters  parameters_;
 
+    class WebServiceCachedGetCommand;
+
   public:
     OracleWebService(MessageBroker& broker,
                      Oracle& oracle,
                      const Orthanc::WebServiceParameters& parameters,
                      NativeStoneApplicationContext& context) :
-      IWebService(broker),
+      BaseWebService(broker),
       oracle_(oracle),
       context_(context),
       parameters_(parameters)
     {
     }
 
-    virtual void GetAsync(const std::string& uri,
-                          const HttpHeaders& headers,
-                          Orthanc::IDynamicObject* payload, // takes ownership
-                          MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback,   // takes ownership
-                          MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,// takes ownership
-                          unsigned int timeoutInSeconds = 60)
-    {
-      oracle_.Submit(new WebServiceGetCommand(broker_, successCallback, failureCallback, parameters_, uri, headers, timeoutInSeconds, payload, context_));
-    }
-
     virtual void PostAsync(const std::string& uri,
                            const HttpHeaders& headers,
                            const std::string& body,
@@ -70,7 +62,7 @@
                            MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL, // takes ownership
                            unsigned int timeoutInSeconds = 60)
     {
-      oracle_.Submit(new WebServicePostCommand(broker_, successCallback, failureCallback, parameters_, uri, headers, timeoutInSeconds, body, payload, context_));
+      oracle_.Submit(new WebServicePostCommand(GetBroker(), successCallback, failureCallback, parameters_, uri, headers, timeoutInSeconds, body, payload, context_));
     }
 
     virtual void DeleteAsync(const std::string& uri,
@@ -80,7 +72,23 @@
                              MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallback = NULL,
                              unsigned int timeoutInSeconds = 60)
     {
-      oracle_.Submit(new WebServiceDeleteCommand(broker_, successCallback, failureCallback, parameters_, uri, headers, timeoutInSeconds, payload, context_));
+      oracle_.Submit(new WebServiceDeleteCommand(GetBroker(), successCallback, failureCallback, parameters_, uri, headers, timeoutInSeconds, payload, context_));
     }
+
+  protected:
+    virtual void GetAsyncInternal(const std::string& uri,
+                                  const HttpHeaders& headers,
+                                  Orthanc::IDynamicObject* payload, // 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_));
+    }
+
+    virtual void NotifyHttpSuccessLater(boost::shared_ptr<BaseWebService::CachedHttpRequestSuccessMessage> cachedHttpMessage,
+                                        Orthanc::IDynamicObject* payload, // takes ownership
+                                        MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback);
+
   };
 }
--- a/Platforms/Wasm/Defaults.cpp	Mon Dec 24 13:41:12 2018 +0100
+++ b/Platforms/Wasm/Defaults.cpp	Wed Jan 16 16:10:16 2019 +0100
@@ -1,6 +1,7 @@
 #include "Defaults.h"
 
 #include "WasmWebService.h"
+#include "WasmDelayedCallExecutor.h"
 #include <Framework/dev.h>
 #include "Framework/Widgets/TestCairoWidget.h"
 #include <Framework/Viewport/WidgetViewport.h>
@@ -73,6 +74,7 @@
     application.reset(CreateUserApplication(broker));
     applicationWasmAdapter.reset(CreateWasmApplicationAdapter(broker, application.get())); 
     WasmWebService::SetBroker(broker);
+    WasmDelayedCallExecutor::SetBroker(broker);
 
     startupParametersBuilder.Clear();
   }
@@ -96,6 +98,7 @@
     context->SetOrthancBaseUrl(baseUri);
     printf("Base URL to Orthanc API: [%s]\n", baseUri);
     context->SetWebService(OrthancStone::WasmWebService::GetInstance());
+    context->SetDelayedCallExecutor(OrthancStone::WasmDelayedCallExecutor::GetInstance());
     application->Initialize(context.get(), statusBar_, parameters);
     application->InitializeWasm();
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Platforms/Wasm/WasmDelayedCallExecutor.cpp	Wed Jan 16 16:10:16 2019 +0100
@@ -0,0 +1,46 @@
+#include "WasmDelayedCallExecutor.h"
+#include "json/value.h"
+#include "json/writer.h"
+#include <emscripten/emscripten.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+  extern void WasmDelayedCallExecutor_Schedule(void* callable,
+                                      unsigned int timeoutInMs
+                                      /*void* payload*/);
+
+  void EMSCRIPTEN_KEEPALIVE WasmDelayedCallExecutor_ExecuteCallback(void* callable
+                                                       //void* payload
+                                                       )
+  {
+    if (callable == NULL)
+    {
+      throw;
+    }
+    else
+    {
+      reinterpret_cast<OrthancStone::MessageHandler<OrthancStone::IDelayedCallExecutor::TimeoutMessage>*>(callable)->
+        Apply(OrthancStone::IDelayedCallExecutor::TimeoutMessage()); // uri, reinterpret_cast<Orthanc::IDynamicObject*>(payload)));
+    }
+  }
+
+
+#ifdef __cplusplus
+}
+#endif
+
+
+
+namespace OrthancStone
+{
+  MessageBroker* WasmDelayedCallExecutor::broker_ = NULL;
+
+
+  void WasmDelayedCallExecutor::Schedule(MessageHandler<IDelayedCallExecutor::TimeoutMessage>* callback,
+                         unsigned int timeoutInMs)
+  {
+    WasmDelayedCallExecutor_Schedule(callback, timeoutInMs);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Platforms/Wasm/WasmDelayedCallExecutor.h	Wed Jan 16 16:10:16 2019 +0100
@@ -0,0 +1,40 @@
+#pragma once
+
+#include <Framework/Toolbox/IDelayedCallExecutor.h>
+#include <Core/OrthancException.h>
+
+namespace OrthancStone
+{
+  class WasmDelayedCallExecutor : public IDelayedCallExecutor
+  {
+  private:
+    static MessageBroker* broker_;
+
+    // Private constructor => Singleton design pattern
+    WasmDelayedCallExecutor(MessageBroker& broker) :
+      IDelayedCallExecutor(broker)
+    {
+    }
+
+  public:
+    static WasmDelayedCallExecutor& GetInstance()
+    {
+      if (broker_ == NULL)
+      {
+        printf("WasmDelayedCallExecutor::GetInstance(): broker not initialized\n");
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      static WasmDelayedCallExecutor instance(*broker_);
+      return instance;
+    }
+
+    static void SetBroker(MessageBroker& broker)
+    {
+      broker_ = &broker;
+    }
+
+    virtual void Schedule(MessageHandler<IDelayedCallExecutor::TimeoutMessage>* callback,
+                         unsigned int timeoutInMs = 1000);
+
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Platforms/Wasm/WasmDelayedCallExecutor.js	Wed Jan 16 16:10:16 2019 +0100
@@ -0,0 +1,7 @@
+mergeInto(LibraryManager.library, {
+  WasmDelayedCallExecutor_Schedule: function(callable, timeoutInMs/*, payload*/) {
+    setTimeout(function() {
+      WasmDelayedCallExecutor_ExecuteCallback(callable/*, payload*/);
+    }, timeoutInMs);
+  }
+});
--- a/Platforms/Wasm/WasmWebService.cpp	Mon Dec 24 13:41:12 2018 +0100
+++ b/Platforms/Wasm/WasmWebService.cpp	Wed Jan 16 16:10:16 2019 +0100
@@ -2,6 +2,15 @@
 #include "json/value.h"
 #include "json/writer.h"
 #include <emscripten/emscripten.h>
+#include <boost/shared_ptr.hpp>
+
+struct CachedSuccessNotification
+{
+  boost::shared_ptr<OrthancStone::BaseWebService::CachedHttpRequestSuccessMessage>    cachedMessage;
+  std::auto_ptr<Orthanc::IDynamicObject>                                              payload;
+  OrthancStone::MessageHandler<OrthancStone::IWebService::HttpRequestSuccessMessage>* successCallback;
+};
+
 
 #ifdef __cplusplus
 extern "C" {
@@ -14,6 +23,8 @@
                                       void* payload,
                                       unsigned int timeoutInSeconds);
 
+  extern void WasmWebService_ScheduleLaterCachedSuccessNotification(void* brol);
+
   extern void WasmWebService_PostAsync(void* callableSuccess,
                                        void* callableFailure,
                                        const char* uri,
@@ -34,17 +45,27 @@
                                                        const char* uri,
                                                        void* payload)
   {
-    if (failureCallable == NULL)
-    {
-      throw;
-    }
-    else
+    if (failureCallable != NULL)
     {
       reinterpret_cast<OrthancStone::MessageHandler<OrthancStone::IWebService::HttpRequestErrorMessage>*>(failureCallable)->
         Apply(OrthancStone::IWebService::HttpRequestErrorMessage(uri, reinterpret_cast<Orthanc::IDynamicObject*>(payload)));
     }
   }
 
+  void EMSCRIPTEN_KEEPALIVE WasmWebService_NotifyCachedSuccess(void* notification_)
+  {
+    // notification has been allocated in C++ and passed to JS.  It must be deleted by this method
+    std::auto_ptr<CachedSuccessNotification> notification(reinterpret_cast<CachedSuccessNotification*>(notification_));
+
+    notification->successCallback->Apply(OrthancStone::IWebService::HttpRequestSuccessMessage(
+      notification->cachedMessage->GetUri(), 
+      notification->cachedMessage->GetAnswer(),
+      notification->cachedMessage->GetAnswerSize(),
+      notification->cachedMessage->GetAnswerHttpHeaders(),
+      notification->payload.get()
+      ));
+  }
+
   void EMSCRIPTEN_KEEPALIVE WasmWebService_NotifySuccess(void* successCallable,
                                                          const char* uri,
                                                          const void* body,
@@ -52,16 +73,12 @@
                                                          const char* answerHeaders,
                                                          void* payload)
   {
-    if (successCallable == NULL)
-    {
-      throw;
-    }
-    else
+    if (successCallable != NULL)
     {
       OrthancStone::IWebService::HttpHeaders headers;
 
       // TODO - Parse "answerHeaders"
-      printf("[%s]\n", answerHeaders);
+      printf("TODO: parse headers [%s]\n", answerHeaders);
       
       reinterpret_cast<OrthancStone::MessageHandler<OrthancStone::IWebService::HttpRequestSuccessMessage>*>(successCallable)->
         Apply(OrthancStone::IWebService::HttpRequestSuccessMessage(uri, body, bodySize, headers,
@@ -122,16 +139,29 @@
                                payload, timeoutInSeconds);
   }
 
-  void WasmWebService::GetAsync(const std::string& relativeUri,
-                                const HttpHeaders& headers,
-                                Orthanc::IDynamicObject* payload,
-                                MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallable,
-                                MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallable,
-                                unsigned int timeoutInSeconds)
+  void WasmWebService::GetAsyncInternal(const std::string &relativeUri,
+                                        const HttpHeaders &headers,
+                                        Orthanc::IDynamicObject *payload,
+                                        MessageHandler<IWebService::HttpRequestSuccessMessage> *successCallable,
+                                        MessageHandler<IWebService::HttpRequestErrorMessage> *failureCallable,
+                                        unsigned int timeoutInSeconds)
   {
     std::string headersInJsonString;
     ToJsonString(headersInJsonString, headers);
     WasmWebService_GetAsync(successCallable, failureCallable, relativeUri.c_str(),
                             headersInJsonString.c_str(), payload, timeoutInSeconds);
   }
+
+  void WasmWebService::NotifyHttpSuccessLater(boost::shared_ptr<BaseWebService::CachedHttpRequestSuccessMessage> cachedMessage,
+                                                Orthanc::IDynamicObject* payload, // takes ownership
+                                                MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallback)
+  {
+    CachedSuccessNotification* notification = new CachedSuccessNotification();  // allocated on the heap, it will be passed to JS and deleted when coming back to C++
+    notification->cachedMessage = cachedMessage;
+    notification->payload.reset(payload);
+    notification->successCallback = successCallback;
+
+    WasmWebService_ScheduleLaterCachedSuccessNotification(notification);
+  }
+
 }
--- a/Platforms/Wasm/WasmWebService.h	Mon Dec 24 13:41:12 2018 +0100
+++ b/Platforms/Wasm/WasmWebService.h	Wed Jan 16 16:10:16 2019 +0100
@@ -1,66 +1,62 @@
 #pragma once
 
-#include <Framework/Toolbox/IWebService.h>
+#include <Framework/Toolbox/BaseWebService.h>
 #include <Core/OrthancException.h>
 
 namespace OrthancStone
 {
-  class WasmWebService : public IWebService
-  {
-  private:
-    static MessageBroker* broker_;
+class WasmWebService : public BaseWebService
+{
+private:
+  static MessageBroker *broker_;
 
-    // Private constructor => Singleton design pattern
-    WasmWebService(MessageBroker& broker) :
-      IWebService(broker)
-    {
-    }
+  // Private constructor => Singleton design pattern
+  WasmWebService(MessageBroker &broker) : BaseWebService(broker)
+  {
+  }
 
-  public:
-    static WasmWebService& GetInstance()
+public:
+  static WasmWebService &GetInstance()
+  {
+    if (broker_ == NULL)
     {
-      if (broker_ == NULL)
-      {
-        printf("WasmWebService::GetInstance(): broker not initialized\n");
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-      }
-      static WasmWebService instance(*broker_);
-      return instance;
+      printf("WasmWebService::GetInstance(): broker not initialized\n");
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
     }
+    static WasmWebService instance(*broker_);
+    return instance;
+  }
 
-    static void SetBroker(MessageBroker& broker)
-    {
-      broker_ = &broker;
-    }
+  static void SetBroker(MessageBroker &broker)
+  {
+    broker_ = &broker;
+  }
 
-    virtual void GetAsync(const std::string& uri,
-                          const HttpHeaders& headers,
-                          Orthanc::IDynamicObject* payload,
-                          MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallable,
-                          MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallable = NULL,
-                          unsigned int timeoutInSeconds = 60);
+  virtual void PostAsync(const std::string &uri,
+                         const HttpHeaders &headers,
+                         const std::string &body,
+                         Orthanc::IDynamicObject *payload,
+                         MessageHandler<IWebService::HttpRequestSuccessMessage> *successCallable,
+                         MessageHandler<IWebService::HttpRequestErrorMessage> *failureCallable = NULL,
+                         unsigned int timeoutInSeconds = 60);
 
-    virtual void PostAsync(const std::string& uri,
-                           const HttpHeaders& headers,
-                           const std::string& body,
-                           Orthanc::IDynamicObject* payload,
-                           MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallable,
-                           MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallable = NULL,
+  virtual void DeleteAsync(const std::string &uri,
+                           const HttpHeaders &headers,
+                           Orthanc::IDynamicObject *payload,
+                           MessageHandler<IWebService::HttpRequestSuccessMessage> *successCallable,
+                           MessageHandler<IWebService::HttpRequestErrorMessage> *failureCallable = NULL,
                            unsigned int timeoutInSeconds = 60);
 
-    virtual void DeleteAsync(const std::string& uri,
-                             const HttpHeaders& headers,
-                             Orthanc::IDynamicObject* payload,
-                             MessageHandler<IWebService::HttpRequestSuccessMessage>* successCallable,
-                             MessageHandler<IWebService::HttpRequestErrorMessage>* failureCallable = NULL,
-                             unsigned int timeoutInSeconds = 60);
+protected:
+  virtual void GetAsyncInternal(const std::string &uri,
+                                const HttpHeaders &headers,
+                                Orthanc::IDynamicObject *payload,
+                                MessageHandler<IWebService::HttpRequestSuccessMessage> *successCallable,
+                                MessageHandler<IWebService::HttpRequestErrorMessage> *failureCallable = NULL,
+                                unsigned int timeoutInSeconds = 60);
 
-    virtual void Start()
-    {
-    }
-    
-    virtual void Stop()
-    {
-    }
-  };
-}
+  virtual void NotifyHttpSuccessLater(boost::shared_ptr<BaseWebService::CachedHttpRequestSuccessMessage> cachedHttpMessage,
+                                      Orthanc::IDynamicObject *payload, // takes ownership
+                                      MessageHandler<IWebService::HttpRequestSuccessMessage> *successCallback);
+};
+} // namespace OrthancStone
--- a/Platforms/Wasm/WasmWebService.js	Mon Dec 24 13:41:12 2018 +0100
+++ b/Platforms/Wasm/WasmWebService.js	Wed Jan 16 16:10:16 2019 +0100
@@ -35,6 +35,12 @@
     xhr.send();
   },
 
+  WasmWebService_ScheduleLaterCachedSuccessNotification: function (brol) {
+    setTimeout(function() {
+      WasmWebService_NotifyCachedSuccess(brol);
+    }, 0);
+  },
+
   WasmWebService_PostAsync: function(callableSuccess, callableFailure, url, headersInJsonString, body, bodySize, payload, timeoutInSeconds) {
     var xhr = new XMLHttpRequest();
     var url_ = UTF8ToString(url);
--- a/Platforms/Wasm/wasm-application-runner.ts	Mon Dec 24 13:41:12 2018 +0100
+++ b/Platforms/Wasm/wasm-application-runner.ts	Wed Jan 16 16:10:16 2019 +0100
@@ -10,6 +10,8 @@
 // global functions
 var WasmWebService_NotifyError: Function = null;
 var WasmWebService_NotifySuccess: Function = null;
+var WasmWebService_NotifyCachedSuccess: Function = null;
+var WasmDelayedCallExecutor_ExecuteCallback: Function = null;
 var WasmDoAnimation: Function = null;
 var SetStartupParameter: Function = null;
 var CreateWasmApplication: Function = null;
@@ -91,8 +93,10 @@
     ReleaseCppViewport = StoneFrameworkModule.cwrap('ReleaseCppViewport', null, ['number']);
     StartWasmApplication = StoneFrameworkModule.cwrap('StartWasmApplication', null, ['string']);
 
+    WasmWebService_NotifyCachedSuccess = StoneFrameworkModule.cwrap('WasmWebService_NotifyCachedSuccess', null, ['number']);
     WasmWebService_NotifySuccess = StoneFrameworkModule.cwrap('WasmWebService_NotifySuccess', null, ['number', 'string', 'array', 'number', 'number']);
     WasmWebService_NotifyError = StoneFrameworkModule.cwrap('WasmWebService_NotifyError', null, ['number', 'string', 'number']);
+    WasmDelayedCallExecutor_ExecuteCallback = StoneFrameworkModule.cwrap('WasmDelayedCallExecutor_ExecuteCallback', null, ['number']);
     WasmDoAnimation = StoneFrameworkModule.cwrap('WasmDoAnimation', null, []);
 
     SendMessageToStoneApplication = StoneFrameworkModule.cwrap('SendMessageToStoneApplication', 'string', ['string']);
--- a/Resources/CMake/OrthancStoneConfiguration.cmake	Mon Dec 24 13:41:12 2018 +0100
+++ b/Resources/CMake/OrthancStoneConfiguration.cmake	Wed Jan 16 16:10:16 2019 +0100
@@ -186,8 +186,10 @@
     ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceGetCommand.cpp
     ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServicePostCommand.cpp
     ${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/OracleWebService.h
+    ${ORTHANC_STONE_ROOT}/Platforms/Generic/OracleWebService.cpp
+    ${ORTHANC_STONE_ROOT}/Platforms/Generic/OracleDelayedCallExecutor.h
     )
 
   if (ENABLE_SDL OR ENABLE_QT)
@@ -212,6 +214,7 @@
 
   set(STONE_WASM_SOURCES
     ${ORTHANC_STONE_ROOT}/Platforms/Wasm/Defaults.cpp
+    ${ORTHANC_STONE_ROOT}/Platforms/Wasm/WasmDelayedCallExecutor.cpp
     ${ORTHANC_STONE_ROOT}/Platforms/Wasm/WasmWebService.cpp
     ${ORTHANC_STONE_ROOT}/Platforms/Wasm/WasmViewport.cpp
     ${ORTHANC_STONE_ROOT}/Platforms/Wasm/WasmPlatformApplicationAdapter.cpp
@@ -226,6 +229,10 @@
     COMMAND ${CMAKE_COMMAND} -E touch "${AUTOGENERATED_DIR}/WasmWebService.c" ""
     DEPENDS "${ORTHANC_STONE_ROOT}/Platforms/Wasm/WasmWebService.js")
   add_custom_command(
+    OUTPUT "${AUTOGENERATED_DIR}/WasmDelayedCallExecutor.c"
+    COMMAND ${CMAKE_COMMAND} -E touch "${AUTOGENERATED_DIR}/WasmDelayedCallExecutor.c" ""
+    DEPENDS "${ORTHANC_STONE_ROOT}/Platforms/Wasm/WasmDelayedCallExecutor.js")
+  add_custom_command(
     OUTPUT "${AUTOGENERATED_DIR}/default-library.c"
     COMMAND ${CMAKE_COMMAND} -E touch "${AUTOGENERATED_DIR}/default-library.c" ""
     DEPENDS "${ORTHANC_STONE_ROOT}/Platforms/Wasm/default-library.js")
@@ -246,6 +253,8 @@
   ${ORTHANC_STONE_ROOT}/Framework/Layers/LineMeasureTracker.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Layers/RenderStyle.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Layers/SliceOutlineRenderer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyAlphaLayer.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyDicomLayer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerCropTracker.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerMoveTracker.cpp
@@ -253,12 +262,16 @@
   ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyLayerRotateTracker.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyScene.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographySceneCommand.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographySceneReader.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographySceneWriter.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyTextLayer.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyWidget.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Radiography/RadiographyWindowingTracker.cpp
   ${ORTHANC_STONE_ROOT}/Framework/SmartLoader.cpp
   ${ORTHANC_STONE_ROOT}/Framework/StoneEnumerations.cpp
   ${ORTHANC_STONE_ROOT}/Framework/StoneException.h
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/AffineTransform2D.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Toolbox/BaseWebService.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/CoordinateSystem3D.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/DicomFrameConverter.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/DicomStructureSet.cpp
@@ -266,6 +279,7 @@
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/Extent2D.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/FiniteProjectiveCamera.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/GeometryToolbox.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Toolbox/IDelayedCallExecutor.h
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/IWebService.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/ImageGeometry.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/LinearAlgebra.cpp
--- a/Resources/CMake/QtConfiguration.cmake	Mon Dec 24 13:41:12 2018 +0100
+++ b/Resources/CMake/QtConfiguration.cmake	Wed Jan 16 16:10:16 2019 +0100
@@ -64,6 +64,11 @@
   ${ORTHANC_STONE_ROOT}/Applications/Qt/QStoneMainWindow.cpp
   )
 
+ORTHANC_QT_WRAP_CPP(QT_SOURCES
+  ${ORTHANC_STONE_ROOT}/Applications/Qt/QCairoWidget.h
+  ${ORTHANC_STONE_ROOT}/Applications/Qt/QStoneMainWindow.h
+  )
+
 
 # NB: Including CMAKE_CURRENT_BINARY_DIR is mandatory, as the CMake
 # macros for Qt will put their result in that directory, which cannot
--- a/UnitTestsSources/TestMessageBroker.cpp	Mon Dec 24 13:41:12 2018 +0100
+++ b/UnitTestsSources/TestMessageBroker.cpp	Wed Jan 16 16:10:16 2019 +0100
@@ -1,158 +1,416 @@
-///**
-// * Stone of Orthanc
-// * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
-// * Department, University Hospital of Liege, Belgium
-// * Copyright (C) 2017-2019 Osimis S.A., Belgium
-// *
-// * This program is free software: you can redistribute it and/or
-// * modify it under the terms of the GNU Affero General Public License
-// * as published by the Free Software Foundation, either version 3 of
-// * the License, or (at your option) any later version.
-// *
-// * This program is distributed in the hope that it will be useful, but
-// * WITHOUT ANY WARRANTY; without even the implied warranty of
-// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-// * Affero General Public License for more details.
-// *
-// * You should have received a copy of the GNU Affero General Public License
-// * along with this program. If not, see <http://www.gnu.org/licenses/>.
-// **/
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
 
 
-//#include "gtest/gtest.h"
+#include "gtest/gtest.h"
+
+#include "Framework/Messages/MessageBroker.h"
+#include "Framework/Messages/Promise.h"
+#include "Framework/Messages/IObservable.h"
+#include "Framework/Messages/IObserver.h"
+#include "Framework/Messages/MessageForwarder.h"
+
 
-//#include "../Framework/Messages/MessageBroker.h"
-//#include "../Framework/Messages/IMessage.h"
-//#include "../Framework/Messages/IObservable.h"
-//#include "../Framework/Messages/IObserver.h"
-//#include "../Framework/StoneEnumerations.h"
+int testCounter = 0;
+namespace {
+
+  using namespace OrthancStone;
+
+
+  enum CustomMessageType
+  {
+    CustomMessageType_First = MessageType_CustomMessage + 1,
+
+    CustomMessageType_Completed,
+    CustomMessageType_Increment
+  };
 
 
-//static int test1Counter = 0;
-//static int test2Counter = 0;
-//class MyFullObserver : public OrthancStone::IObserver
-//{
+  class MyObservable : public IObservable
+  {
+  public:
+    struct MyCustomMessage: public BaseMessage<CustomMessageType_Completed>
+    {
+      int payload_;
+
+      MyCustomMessage(int payload)
+        : BaseMessage(),
+          payload_(payload)
+      {}
+    };
+
+    MyObservable(MessageBroker& broker)
+      : IObservable(broker)
+    {}
+
+  };
 
-//public:
-//  MyFullObserver(OrthancStone::MessageBroker& broker)
-//    : OrthancStone::IObserver(broker)
-//  {
-////    DeclareHandledMessage(OrthancStone::MessageType_Test1);
-////    DeclareIgnoredMessage(OrthancStone::MessageType_Test2);
-//  }
+  class MyObserver : public IObserver
+  {
+  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));
+    }
+  };
 
 
-//  void HandleMessage(OrthancStone::IObservable& from, const OrthancStone::IMessage& message) {
-//    switch (message.GetType())
-//    {
-//    case OrthancStone::MessageType_Test1:
-//      test1Counter++;
-//      break;
-//    case OrthancStone::MessageType_Test2:
-//      test2Counter++;
-//      break;
-//    default:
-//      throw OrthancStone::MessageNotDeclaredException(message.GetType());
-//    }
-//  }
+  class MyPromiseSource : public IObservable
+  {
+    Promise* currentPromise_;
+  public:
+    struct MyPromiseMessage: public BaseMessage<MessageType_Test1>
+    {
+      int increment;
+
+      MyPromiseMessage(int increment)
+        : BaseMessage(),
+          increment(increment)
+      {}
+    };
+
+    MyPromiseSource(MessageBroker& broker)
+      : IObservable(broker),
+        currentPromise_(NULL)
+    {}
+
+    Promise& StartSomethingAsync()
+    {
+      currentPromise_ = new Promise(GetBroker());
+      return *currentPromise_;
+    }
 
-//};
+    void CompleteSomethingAsyncWithSuccess(int payload)
+    {
+      currentPromise_->Success(MyPromiseMessage(payload));
+      delete currentPromise_;
+    }
 
-//class MyPartialObserver : public OrthancStone::IObserver
-//{
+    void CompleteSomethingAsyncWithFailure(int payload)
+    {
+      currentPromise_->Failure(MyPromiseMessage(payload));
+      delete currentPromise_;
+    }
+  };
+
 
-//public:
-//  MyPartialObserver(OrthancStone::MessageBroker& broker)
-//    : OrthancStone::IObserver(broker)
-//  {
-////    DeclareHandledMessage(OrthancStone::MessageType_Test1);
-//    // don't declare Test2 on purpose
-//  }
+  class MyPromiseTarget : public IObserver
+  {
+  public:
+    MyPromiseTarget(MessageBroker& broker)
+      : IObserver(broker)
+    {}
+
+    void IncrementCounter(const MyPromiseSource::MyPromiseMessage& args)
+    {
+      testCounter += args.increment;
+    }
+
+    void DecrementCounter(const MyPromiseSource::MyPromiseMessage& args)
+    {
+      testCounter -= args.increment;
+    }
+  };
+}
 
 
-//  void HandleMessage(OrthancStone::IObservable& from, const OrthancStone::IMessage& message) {
-//    switch (message.GetType())
-//    {
-//    case OrthancStone::MessageType_Test1:
-//      test1Counter++;
-//      break;
-//    case OrthancStone::MessageType_Test2:
-//      test2Counter++;
-//      break;
-//    default:
-//      throw OrthancStone::MessageNotDeclaredException(message.GetType());
-//    }
-//  }
+TEST(MessageBroker, TestPermanentConnectionSimpleUseCase)
+{
+  MessageBroker broker;
+  MyObservable  observable(broker);
+  MyObserver    observer(broker);
+
+  // create a permanent connection between an observable and an observer
+  observable.RegisterObserverCallback(new Callable<MyObserver, MyObservable::MyCustomMessage>(observer, &MyObserver::HandleCompletedMessage));
+
+  testCounter = 0;
+  observable.EmitMessage(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.EmitMessage(MyObservable::MyCustomMessage(20));
+  ASSERT_EQ(20, testCounter);
+
+  // Unregister the observer; make sure it's not called anymore
+  observable.Unregister(&observer);
+  testCounter = 0;
+  observable.EmitMessage(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.EmitMessage(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.EmitMessage(MyObservable::MyCustomMessage(20));
+  ASSERT_EQ(20, testCounter);
+}
+
+TEST(MessageBroker, TestPermanentConnectionDeleteObserver)
+{
+  MessageBroker broker;
+  MyObservable  observable(broker);
+  MyObserver*   observer = new MyObserver(broker);
 
-//};
+  // create a permanent connection between an observable and an observer
+  observable.RegisterObserverCallback(new Callable<MyObserver, MyObservable::MyCustomMessage>(*observer, &MyObserver::HandleCompletedMessage));
 
+  testCounter = 0;
+  observable.EmitMessage(MyObservable::MyCustomMessage(12));
+  ASSERT_EQ(12, testCounter);
+
+  // delete the observer and check that the callback is not called anymore
+  delete observer;
 
-//class MyObservable : public OrthancStone::IObservable
-//{
+  // the connection is permanent; if we emit the same message again, the observer will be notified again
+  testCounter = 0;
+  observable.EmitMessage(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));
 
-//public:
-//  MyObservable(OrthancStone::MessageBroker& broker)
-//    : OrthancStone::IObservable(broker)
-//  {
-//    DeclareEmittableMessage(OrthancStone::MessageType_Test1);
-//    DeclareEmittableMessage(OrthancStone::MessageType_Test2);
-//  }
+  testCounter = 0;
+  observable.EmitMessage(MyObservable::MyCustomMessage(12));
+  ASSERT_EQ(12, testCounter);
+
+  delete intermediate;
+
+  observable.EmitMessage(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.EmitMessage(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.EmitMessage(MyObservable::MyCustomMessage(20));
+  ASSERT_EQ(20, testCounter);
+}
 
 
-//TEST(MessageBroker, NormalUsage)
-//{
-//  OrthancStone::MessageBroker broker;
-//  MyObservable observable(broker);
+TEST(MessageBroker, TestPromiseSuccessFailure)
+{
+  MessageBroker broker;
+  MyPromiseSource  source(broker);
+  MyPromiseTarget target(broker);
+
+  // test a successful promise
+  source.StartSomethingAsync()
+      .Then(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(target, &MyPromiseTarget::IncrementCounter))
+      .Else(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(target, &MyPromiseTarget::DecrementCounter));
+
+  testCounter = 0;
+  source.CompleteSomethingAsyncWithSuccess(10);
+  ASSERT_EQ(10, testCounter);
 
-//  test1Counter = 0;
+  // test a failing promise
+  source.StartSomethingAsync()
+      .Then(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(target, &MyPromiseTarget::IncrementCounter))
+      .Else(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(target, &MyPromiseTarget::DecrementCounter));
+
+  testCounter = 0;
+  source.CompleteSomethingAsyncWithFailure(15);
+  ASSERT_EQ(-15, testCounter);
+}
 
-//  // no observers have been registered -> nothing shall happen
-//  observable.EmitMessage(OrthancStone::IMessage(OrthancStone::MessageType_Test1));
+TEST(MessageBroker, TestPromiseDeleteTarget)
+{
+  MessageBroker broker;
+  MyPromiseSource source(broker);
+  MyPromiseTarget* target = new MyPromiseTarget(broker);
 
-//  ASSERT_EQ(0, test1Counter);
+  // create the promise
+  source.StartSomethingAsync()
+      .Then(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(*target, &MyPromiseTarget::IncrementCounter))
+      .Else(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(*target, &MyPromiseTarget::DecrementCounter));
 
-//  // register an observer, check it is called
-//  MyFullObserver fullObserver(broker);
-//  ASSERT_NO_THROW(observable.RegisterObserver(fullObserver));
+  // delete the promise target
+  delete target;
+
+  // trigger the promise, make sure it does not throw and does not call the callback
+  testCounter = 0;
+  source.CompleteSomethingAsyncWithSuccess(10);
+  ASSERT_EQ(0, testCounter);
 
-//  observable.EmitMessage(OrthancStone::IMessage(OrthancStone::MessageType_Test1));
+  // test a failing promise
+  source.StartSomethingAsync()
+      .Then(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(*target, &MyPromiseTarget::IncrementCounter))
+      .Else(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(*target, &MyPromiseTarget::DecrementCounter));
+
+  testCounter = 0;
+  source.CompleteSomethingAsyncWithFailure(15);
+  ASSERT_EQ(0, testCounter);
+}
+
+#if __cplusplus >= 201103L
 
-//  ASSERT_EQ(1, test1Counter);
+#include <functional>
+
+namespace OrthancStone {
+
+  template <typename TMessage>
+  class LambdaCallable : public MessageHandler<TMessage>
+  {
+  private:
 
-//  // register an invalid observer, check it raises an exception
-//  MyPartialObserver partialObserver(broker);
-//  ASSERT_THROW(observable.RegisterObserver(partialObserver), OrthancStone::MessageNotDeclaredException);
+    IObserver&      observer_;
+    std::function<void (const TMessage&)> lambda_;
 
-//  // check an exception is thrown when the observable emits an undeclared message
-//  ASSERT_THROW(observable.EmitMessage(OrthancStone::IMessage(OrthancStone::MessageType_VolumeSlicer_GeometryReady)), OrthancStone::MessageNotDeclaredException);
+  public:
+    LambdaCallable(IObserver& observer,
+                    std::function<void (const TMessage&)> lambdaFunction) :
+             observer_(observer),
+             lambda_(lambdaFunction)
+    {
+    }
 
-//  // unregister the observer, make sure nothing happens afterwards
-//  observable.UnregisterObserver(fullObserver);
-//  observable.EmitMessage(OrthancStone::IMessage(OrthancStone::MessageType_Test1));
-//  ASSERT_EQ(1, test1Counter);
-//}
+    virtual void Apply(const IMessage& message)
+    {
+      lambda_(dynamic_cast<const TMessage&>(message));
+    }
+
+    virtual MessageType GetMessageType() const
+    {
+      return static_cast<MessageType>(TMessage::Type);
+    }
+
+    virtual IObserver* GetObserver() const
+    {
+      return &observer_;
+    }
+  };
+
 
-//TEST(MessageBroker, DeleteObserverWhileRegistered)
-//{
-//  OrthancStone::MessageBroker broker;
-//  MyObservable observable(broker);
+}
+
+TEST(MessageBroker, TestLambdaSimpleUseCase)
+{
+  MessageBroker broker;
+  MyObservable  observable(broker);
+  MyObserver*   observer = new MyObserver(broker);
 
-//  test1Counter = 0;
+  // 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.EmitMessage(MyObservable::MyCustomMessage(12));
+  ASSERT_EQ(24, testCounter);
+
+  // delete the observer and check that the callback is not called anymore
+  delete observer;
 
-//  {
-//    // register an observer, check it is called
-//    MyFullObserver observer(broker);
-//    observable.RegisterObserver(observer);
+  // the connection is permanent; if we emit the same message again, the observer will be notified again
+  testCounter = 0;
+  observable.EmitMessage(MyObservable::MyCustomMessage(20));
+  ASSERT_EQ(0, testCounter);
+}
 
-//    observable.EmitMessage(OrthancStone::IMessage(OrthancStone::MessageType_Test1));
+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_;
+      }));
 
-//    ASSERT_EQ(1, test1Counter);
-//  }
+    }
+  };
+}
+
+TEST(MessageBroker, TestLambdaCaptureThisAndAccessPrivateMembers)
+{
+  MessageBroker broker;
+  MyObservable  observable(broker);
+  MyObserverWithLambda*   observer = new MyObserverWithLambda(broker, 3, observable);
 
-//  // at this point, the observer has been deleted, the handle shall not be called again (and it shall not crash !)
-//  observable.EmitMessage(OrthancStone::IMessage(OrthancStone::MessageType_Test1));
+  testCounter = 0;
+  observable.EmitMessage(MyObservable::MyCustomMessage(12));
+  ASSERT_EQ(36, testCounter);
+
+  // delete the observer and check that the callback is not called anymore
+  delete observer;
 
-//  ASSERT_EQ(1, test1Counter);
-//}
+  // the connection is permanent; if we emit the same message again, the observer will be notified again
+  testCounter = 0;
+  observable.EmitMessage(MyObservable::MyCustomMessage(20));
+  ASSERT_EQ(0, testCounter);
+}
+
+#endif // C++ 11
--- a/UnitTestsSources/TestMessageBroker2.cpp	Mon Dec 24 13:41:12 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,410 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2019 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Affero General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "gtest/gtest.h"
-
-#include "Framework/Messages/MessageBroker.h"
-#include "Framework/Messages/Promise.h"
-#include "Framework/Messages/IObservable.h"
-#include "Framework/Messages/IObserver.h"
-#include "Framework/Messages/MessageForwarder.h"
-
-
-int testCounter = 0;
-namespace {
-
-  using namespace OrthancStone;
-
-
-  enum CustomMessageType
-  {
-    CustomMessageType_First = MessageType_CustomMessage + 1,
-
-    CustomMessageType_Completed,
-    CustomMessageType_Increment
-  };
-
-
-  class MyObservable : public IObservable
-  {
-  public:
-    struct MyCustomMessage: public BaseMessage<CustomMessageType_Completed>
-    {
-      int payload_;
-
-      MyCustomMessage(int payload)
-        : BaseMessage(),
-          payload_(payload)
-      {}
-    };
-
-    MyObservable(MessageBroker& broker)
-      : IObservable(broker)
-    {}
-
-  };
-
-  class MyObserver : public IObserver
-  {
-  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));
-    }
-  };
-
-
-  class MyPromiseSource : public IObservable
-  {
-    Promise* currentPromise_;
-  public:
-    struct MyPromiseMessage: public BaseMessage<MessageType_Test1>
-    {
-      int increment;
-
-      MyPromiseMessage(int increment)
-        : BaseMessage(),
-          increment(increment)
-      {}
-    };
-
-    MyPromiseSource(MessageBroker& broker)
-      : IObservable(broker),
-        currentPromise_(NULL)
-    {}
-
-    Promise& StartSomethingAsync()
-    {
-      currentPromise_ = new Promise(GetBroker());
-      return *currentPromise_;
-    }
-
-    void CompleteSomethingAsyncWithSuccess(int payload)
-    {
-      currentPromise_->Success(MyPromiseMessage(payload));
-      delete currentPromise_;
-    }
-
-    void CompleteSomethingAsyncWithFailure(int payload)
-    {
-      currentPromise_->Failure(MyPromiseMessage(payload));
-      delete currentPromise_;
-    }
-  };
-
-
-  class MyPromiseTarget : public IObserver
-  {
-  public:
-    MyPromiseTarget(MessageBroker& broker)
-      : IObserver(broker)
-    {}
-
-    void IncrementCounter(const MyPromiseSource::MyPromiseMessage& args)
-    {
-      testCounter += args.increment;
-    }
-
-    void DecrementCounter(const MyPromiseSource::MyPromiseMessage& args)
-    {
-      testCounter -= args.increment;
-    }
-  };
-}
-
-
-TEST(MessageBroker2, TestPermanentConnectionSimpleUseCase)
-{
-  MessageBroker broker;
-  MyObservable  observable(broker);
-  MyObserver    observer(broker);
-
-  // create a permanent connection between an observable and an observer
-  observable.RegisterObserverCallback(new Callable<MyObserver, MyObservable::MyCustomMessage>(observer, &MyObserver::HandleCompletedMessage));
-
-  testCounter = 0;
-  observable.EmitMessage(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.EmitMessage(MyObservable::MyCustomMessage(20));
-  ASSERT_EQ(20, testCounter);
-}
-
-TEST(MessageBroker2, 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.EmitMessage(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.EmitMessage(MyObservable::MyCustomMessage(20));
-  ASSERT_EQ(20, testCounter);
-}
-
-TEST(MessageBroker2, TestPermanentConnectionDeleteObserver)
-{
-  MessageBroker broker;
-  MyObservable  observable(broker);
-  MyObserver*   observer = new MyObserver(broker);
-
-  // create a permanent connection between an observable and an observer
-  observable.RegisterObserverCallback(new Callable<MyObserver, MyObservable::MyCustomMessage>(*observer, &MyObserver::HandleCompletedMessage));
-
-  testCounter = 0;
-  observable.EmitMessage(MyObservable::MyCustomMessage(12));
-  ASSERT_EQ(12, 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.EmitMessage(MyObservable::MyCustomMessage(20));
-  ASSERT_EQ(0, testCounter);
-}
-
-TEST(MessageBroker2, 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.EmitMessage(MyObservable::MyCustomMessage(12));
-  ASSERT_EQ(12, testCounter);
-
-  delete intermediate;
-
-  observable.EmitMessage(MyObservable::MyCustomMessage(20));
-  ASSERT_EQ(12, testCounter);
-}
-
-TEST(MessageBroker2, 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.EmitMessage(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.EmitMessage(MyObservable::MyCustomMessage(20));
-  ASSERT_EQ(20, testCounter);
-}
-
-
-TEST(MessageBroker2, TestPromiseSuccessFailure)
-{
-  MessageBroker broker;
-  MyPromiseSource  source(broker);
-  MyPromiseTarget target(broker);
-
-  // test a successful promise
-  source.StartSomethingAsync()
-      .Then(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(target, &MyPromiseTarget::IncrementCounter))
-      .Else(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(target, &MyPromiseTarget::DecrementCounter));
-
-  testCounter = 0;
-  source.CompleteSomethingAsyncWithSuccess(10);
-  ASSERT_EQ(10, testCounter);
-
-  // test a failing promise
-  source.StartSomethingAsync()
-      .Then(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(target, &MyPromiseTarget::IncrementCounter))
-      .Else(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(target, &MyPromiseTarget::DecrementCounter));
-
-  testCounter = 0;
-  source.CompleteSomethingAsyncWithFailure(15);
-  ASSERT_EQ(-15, testCounter);
-}
-
-TEST(MessageBroker2, TestPromiseDeleteTarget)
-{
-  MessageBroker broker;
-  MyPromiseSource source(broker);
-  MyPromiseTarget* target = new MyPromiseTarget(broker);
-
-  // create the promise
-  source.StartSomethingAsync()
-      .Then(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(*target, &MyPromiseTarget::IncrementCounter))
-      .Else(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(*target, &MyPromiseTarget::DecrementCounter));
-
-  // delete the promise target
-  delete target;
-
-  // trigger the promise, make sure it does not throw and does not call the callback
-  testCounter = 0;
-  source.CompleteSomethingAsyncWithSuccess(10);
-  ASSERT_EQ(0, testCounter);
-
-  // test a failing promise
-  source.StartSomethingAsync()
-      .Then(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(*target, &MyPromiseTarget::IncrementCounter))
-      .Else(new Callable<MyPromiseTarget, MyPromiseSource::MyPromiseMessage>(*target, &MyPromiseTarget::DecrementCounter));
-
-  testCounter = 0;
-  source.CompleteSomethingAsyncWithFailure(15);
-  ASSERT_EQ(0, testCounter);
-}
-
-#if __cplusplus >= 201103L
-
-#include <functional>
-
-namespace OrthancStone {
-
-  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 MessageType GetMessageType() const
-    {
-      return static_cast<MessageType>(TMessage::Type);
-    }
-
-    virtual IObserver* GetObserver() const
-    {
-      return &observer_;
-    }
-  };
-
-
-}
-
-TEST(MessageBroker2, 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.EmitMessage(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.EmitMessage(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(MessageBroker2, TestLambdaCaptureThisAndAccessPrivateMembers)
-{
-  MessageBroker broker;
-  MyObservable  observable(broker);
-  MyObserverWithLambda*   observer = new MyObserverWithLambda(broker, 3, observable);
-
-  testCounter = 0;
-  observable.EmitMessage(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.EmitMessage(MyObservable::MyCustomMessage(20));
-  ASSERT_EQ(0, testCounter);
-}
-
-#endif // C++ 11
--- a/UnitTestsSources/TestMessageBroker2_connect_ok.cpp	Mon Dec 24 13:41:12 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,226 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2019 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Affero General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "gtest/gtest.h"
-
-#include <boost/noncopyable.hpp>
-#include <boost/function.hpp>
-#include <boost/bind.hpp>
-
-#include <string>
-#include <map>
-#include <set>
-
-int testCounter = 0;
-namespace {
-
-  enum MessageType
-  {
-    // used in unit tests only
-    MessageType_Test1,
-    MessageType_Test2,
-
-    MessageType_LastGenericStoneMessage
-  };
-
-  struct IMessage  : public boost::noncopyable
-  {
-    MessageType messageType_;
-  public:
-    IMessage(const MessageType& messageType)
-      : messageType_(messageType)
-    {}
-    virtual ~IMessage() {}
-
-    MessageType GetType() const {return messageType_;}
-  };
-
-
-  class IObserver;
-  class IObservable;
-
-  /*
-   * 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 dead observer.
-   */
-  class MessageBroker : public boost::noncopyable
-  {
-
-    std::set<IObserver*> activeObservers_;  // the list of observers that are currently alive (that have not been deleted)
-
-  public:
-
-    void Register(IObserver& observer)
-    {
-      activeObservers_.insert(&observer);
-    }
-
-    void Unregister(IObserver& observer)
-    {
-      activeObservers_.erase(&observer);
-    }
-
-    void EmitMessage(IObservable& from, std::set<IObserver*> observers, const IMessage& message);
-  };
-
-
-  class IObserver : public boost::noncopyable
-  {
-  protected:
-    MessageBroker&                    broker_;
-
-  public:
-    IObserver(MessageBroker& broker)
-      : broker_(broker)
-    {
-      broker_.Register(*this);
-    }
-
-    virtual ~IObserver()
-    {
-      broker_.Unregister(*this);
-    }
-
-    void HandleMessage_(IObservable &from, const IMessage &message)
-    {
-
-      HandleMessage(from, message);
-    }
-
-    virtual void HandleMessage(IObservable& from, const IMessage& message) = 0;
-
-
-  protected:
-
-
-  };
-
-//  struct ICallableObserver
-//  {
-//    IObserver* observer;
-//  };
-
-//  typedef void (IObserver::*ObserverSingleMesssageHandler)(IObservable& from, const IMessage& message);
-
-//  template <typename TObserver>
-//  struct CallableObserver : public ICallableObserver
-//  {
-//    void (TObserver::*ptrToMemberHandler)(IObservable& from, const IMessage& message);
-//  };
-
-  struct CallableObserver
-  {
-    IObserver* observer;
-    boost::function<void (IObservable& from, const IMessage& message)> f;
-  };
-
-  class IObservable : public boost::noncopyable
-  {
-  protected:
-    MessageBroker&                     broker_;
-
-    std::set<IObserver*>              observers_;
-
-    std::map<MessageType, std::set<CallableObserver*> > callables_;
-  public:
-
-    IObservable(MessageBroker& broker)
-      : broker_(broker)
-    {
-    }
-    virtual ~IObservable()
-    {
-    }
-
-    void EmitMessage(const IMessage& message)
-    {
-      //broker_.EmitMessage(*this, observers_, message);
-
-      // TODO check if observer is still alive and call !
-      CallableObserver* callable = *(callables_[message.GetType()].begin());
-      callable->f(*this, message);
-    }
-
-    void RegisterObserver(IObserver& observer)
-    {
-      observers_.insert(&observer);
-    }
-
-    void UnregisterObserver(IObserver& observer)
-    {
-      observers_.erase(&observer);
-    }
-
-
-    //template<typename TObserver> void Connect(MessageType messageType, IObserver& observer, void (TObserver::*ptrToMemberHandler)(IObservable& from, const IMessage& message))
-    void Connect(MessageType messageType, IObserver& observer, boost::function<void (IObservable& from, const IMessage& message)> f)
-    {
-      callables_[messageType] = std::set<CallableObserver*>();
-      CallableObserver* callable = new CallableObserver();
-      callable->observer = &observer;
-      callable->f = f;
-      callables_[messageType].insert(callable);
-    }
-  };
-
-
-  class MyObservable : public IObservable
-  {
-  public:
-    MyObservable(MessageBroker& broker)
-      : IObservable(broker)
-    {}
-  };
-
-  class MyObserver : public IObserver
-  {
-  public:
-    MyObserver(MessageBroker& broker)
-      : IObserver(broker)
-    {}
-    virtual void HandleMessage(IObservable& from, const IMessage& message) {}
-    void HandleSpecificMessage(IObservable& from, const IMessage& message)
-    {
-      testCounter++;
-    }
-
-  };
-
-}
-
-//#define STONE_CONNECT(observabe, messageType, observerPtr, observerMemberFnPtr)
-
-TEST(MessageBroker2, Test1)
-{
-  MessageBroker broker;
-  MyObservable  observable(broker);
-  MyObserver    observer(broker);
-
-
-  observable.Connect(MessageType_Test1, observer, boost::bind(&MyObserver::HandleSpecificMessage, &observer, _1, _2));
-  //STONE_CONNECT(observable, MessageType_Test1, observer, &MyObserver::HandleSpecificMessage)
-  observable.EmitMessage(IMessage(MessageType_Test1));
-
-  ASSERT_EQ(1, testCounter);
-}
-
-
--- a/UnitTestsSources/TestMessageBroker2_promise_and_connect_ok.cpp	Mon Dec 24 13:41:12 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,520 +0,0 @@
-/**
- * Stone of Orthanc
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2019 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Affero General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "gtest/gtest.h"
-
-#include <boost/noncopyable.hpp>
-#include <boost/function.hpp>
-#include <boost/bind.hpp>
-
-#include <string>
-#include <map>
-#include <set>
-
-int testCounter = 0;
-namespace {
-
-  enum MessageType
-  {
-    MessageType_Test1,
-    MessageType_Test2,
-
-    MessageType_CustomMessage,
-    MessageType_LastGenericStoneMessage
-  };
-
-  struct IMessage  : public boost::noncopyable
-  {
-    MessageType messageType_;
-  public:
-    IMessage(const MessageType& messageType)
-      : messageType_(messageType)
-    {}
-    virtual ~IMessage() {}
-
-    virtual int GetType() const {return messageType_;}
-  };
-
-
-  struct ICustomMessage  : public IMessage
-  {
-    int customMessageType_;
-  public:
-    ICustomMessage(int customMessageType)
-      : IMessage(MessageType_CustomMessage),
-        customMessageType_(customMessageType)
-    {}
-    virtual ~ICustomMessage() {}
-
-    virtual int GetType() const {return customMessageType_;}
-  };
-
-
-  class IObserver;
-  class IObservable;
-  class IPromiseTarget;
-  class IPromiseSource;
-  class Promise;
-
-  /*
-   * 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 delete observer.
-   * It does the same book-keeping for the IPromiseTarget and IPromiseSource
-   */
-  class MessageBroker : public boost::noncopyable
-  {
-
-    std::set<IObserver*> activeObservers_;  // the list of observers that are currently alive (that have not been deleted)
-    std::set<IPromiseTarget*> activePromiseTargets_;
-    std::set<IPromiseSource*> activePromiseSources_;
-
-  public:
-
-    void Register(IObserver& observer)
-    {
-      activeObservers_.insert(&observer);
-    }
-
-    void Unregister(IObserver& observer)
-    {
-      activeObservers_.erase(&observer);
-    }
-
-    void Register(IPromiseTarget& target)
-    {
-      activePromiseTargets_.insert(&target);
-    }
-
-    void Unregister(IPromiseTarget& target)
-    {
-      activePromiseTargets_.erase(&target);
-    }
-
-    void Register(IPromiseSource& source)
-    {
-      activePromiseSources_.insert(&source);
-    }
-
-    void Unregister(IPromiseSource& source)
-    {
-      activePromiseSources_.erase(&source);
-    }
-
-    void EmitMessage(IObservable& from, std::set<IObserver*> observers, const IMessage& message);
-
-    bool IsActive(IPromiseTarget* target)
-    {
-      return activePromiseTargets_.find(target) != activePromiseTargets_.end();
-    }
-
-    bool IsActive(IPromiseSource* source)
-    {
-      return activePromiseSources_.find(source) != activePromiseSources_.end();
-    }
-
-    bool IsActive(IObserver* observer)
-    {
-      return activeObservers_.find(observer) != activeObservers_.end();
-    }
-  };
-
-  struct IPromiseArgs
-  {
-public:
-    virtual ~IPromiseArgs() {}
-  };
-
-  class EmptyPromiseArguments : public IPromiseArgs
-  {
-
-  };
-
-  class Promise : public boost::noncopyable
-  {
-  protected:
-    MessageBroker&                    broker_;
-
-    IPromiseTarget*                                           successTarget_;
-    boost::function<void (const IPromiseArgs& message)>       successCallable_;
-
-    IPromiseTarget*                                           failureTarget_;
-    boost::function<void (const IPromiseArgs& message)>       failureCallable_;
-
-  public:
-    Promise(MessageBroker& broker)
-      : broker_(broker),
-        successTarget_(NULL),
-        failureTarget_(NULL)
-    {
-    }
-
-    void Success(const IPromiseArgs& message)
-    {
-      // check the target is still alive in the broker
-      if (broker_.IsActive(successTarget_))
-      {
-        successCallable_(message);
-      }
-    }
-
-    void Failure(const IPromiseArgs& message)
-    {
-      // check the target is still alive in the broker
-      if (broker_.IsActive(failureTarget_))
-      {
-        failureCallable_(message);
-      }
-    }
-
-    Promise& Then(IPromiseTarget* target, boost::function<void (const IPromiseArgs& message)> f)
-    {
-      if (successTarget_ != NULL)
-      {
-        // TODO: throw throw new "Promise may only have a single success target"
-      }
-      successTarget_ = target;
-      successCallable_ = f;
-      return *this;
-    }
-
-    Promise& Else(IPromiseTarget* target, boost::function<void (const IPromiseArgs& message)> f)
-    {
-      if (failureTarget_ != NULL)
-      {
-        // TODO: throw throw new "Promise may only have a single failure target"
-      }
-      failureTarget_ = target;
-      failureCallable_ = f;
-      return *this;
-    }
-
-  };
-
-  class IObserver : public boost::noncopyable
-  {
-  protected:
-    MessageBroker&                    broker_;
-
-  public:
-    IObserver(MessageBroker& broker)
-      : broker_(broker)
-    {
-      broker_.Register(*this);
-    }
-
-    virtual ~IObserver()
-    {
-      broker_.Unregister(*this);
-    }
-
-  };
-
-  class IPromiseTarget : public boost::noncopyable
-  {
-  protected:
-    MessageBroker&                    broker_;
-
-  public:
-    IPromiseTarget(MessageBroker& broker)
-      : broker_(broker)
-    {
-      broker_.Register(*this);
-    }
-
-    virtual ~IPromiseTarget()
-    {
-      broker_.Unregister(*this);
-    }
-  };
-
-  class IPromiseSource : public boost::noncopyable
-  {
-  protected:
-    MessageBroker&                    broker_;
-
-  public:
-    IPromiseSource(MessageBroker& broker)
-      : broker_(broker)
-    {
-      broker_.Register(*this);
-    }
-
-    virtual ~IPromiseSource()
-    {
-      broker_.Unregister(*this);
-    }
-  };
-
-
-  struct CallableObserver
-  {
-    IObserver* observer;
-    boost::function<void (IObservable& from, const IMessage& message)> f;
-  };
-
-  class IObservable : public boost::noncopyable
-  {
-  protected:
-    MessageBroker&                     broker_;
-
-    std::set<IObserver*>              observers_;
-
-    std::map<int, std::set<CallableObserver*> > callables_;
-  public:
-
-    IObservable(MessageBroker& broker)
-      : broker_(broker)
-    {
-    }
-    virtual ~IObservable()
-    {
-    }
-
-    void EmitMessage(const IMessage& message)
-    {
-      //broker_.EmitMessage(*this, observers_, message);
-      int messageType = message.GetType();
-      if (callables_.find(messageType) != callables_.end())
-      {
-        for (std::set<CallableObserver*>::iterator observer = callables_[messageType].begin(); observer != callables_[messageType].end(); observer++)
-        {
-          CallableObserver* callable = *observer;
-          if (broker_.IsActive(callable->observer))
-          {
-            callable->f(*this, message);
-          }
-        }
-      }
-
-    }
-
-    void RegisterObserver(IObserver& observer)
-    {
-      observers_.insert(&observer);
-    }
-
-    void UnregisterObserver(IObserver& observer)
-    {
-      observers_.erase(&observer);
-    }
-
-    //template<typename TObserver> void Connect(MessageType messageType, IObserver& observer, void (TObserver::*ptrToMemberHandler)(IObservable& from, const IMessage& message))
-    void Connect(int messageType, IObserver& observer, boost::function<void (IObservable& from, const IMessage& message)> f)
-    {
-      callables_[messageType] = std::set<CallableObserver*>();
-      CallableObserver* callable = new CallableObserver();
-      callable->observer = &observer;
-      callable->f = f;
-      callables_[messageType].insert(callable);
-    }
-  };
-
-
-  enum CustomMessageType
-  {
-    CustomMessageType_First = MessageType_LastGenericStoneMessage + 1,
-
-    CustomMessageType_Completed
-  };
-
-  class MyObservable : public IObservable
-  {
-  public:
-    struct MyCustomMessage: public ICustomMessage
-    {
-      int payload_;
-      MyCustomMessage(int payload)
-        : ICustomMessage(CustomMessageType_Completed),
-          payload_(payload)
-      {}
-    };
-
-    MyObservable(MessageBroker& broker)
-      : IObservable(broker)
-    {}
-
-  };
-
-  class MyObserver : public IObserver
-  {
-  public:
-    MyObserver(MessageBroker& broker)
-      : IObserver(broker)
-    {}
-    void HandleCompletedMessage(IObservable& from, const IMessage& message)
-    {
-      const MyObservable::MyCustomMessage& msg = dynamic_cast<const MyObservable::MyCustomMessage&>(message);
-      testCounter += msg.payload_;
-    }
-
-  };
-
-
-  class MyPromiseSource : public IPromiseSource
-  {
-    Promise* currentPromise_;
-  public:
-    struct MyPromiseArgs : public IPromiseArgs
-    {
-      int increment;
-    };
-
-    MyPromiseSource(MessageBroker& broker)
-      : IPromiseSource(broker),
-        currentPromise_(NULL)
-    {}
-
-    Promise& StartSomethingAsync()
-    {
-      currentPromise_ = new Promise(broker_);
-      return *currentPromise_;
-    }
-
-    void CompleteSomethingAsyncWithSuccess()
-    {
-      currentPromise_->Success(EmptyPromiseArguments());
-      delete currentPromise_;
-    }
-
-    void CompleteSomethingAsyncWithFailure()
-    {
-      currentPromise_->Failure(EmptyPromiseArguments());
-      delete currentPromise_;
-    }
-  };
-
-
-  class MyPromiseTarget : public IPromiseTarget
-  {
-  public:
-    MyPromiseTarget(MessageBroker& broker)
-      : IPromiseTarget(broker)
-    {}
-
-    void IncrementCounter(const IPromiseArgs& args)
-    {
-      testCounter++;
-    }
-
-    void DecrementCounter(const IPromiseArgs& args)
-    {
-      testCounter--;
-    }
-  };
-}
-
-#define CONNECT_MESSAGES(observablePtr, messageType, observerPtr, observerFnPtr) (observablePtr)->Connect(messageType, *(observerPtr), boost::bind(observerFnPtr, observerPtr, _1, _2))
-#define PTHEN(targetPtr, targetFnPtr) Then(targetPtr, boost::bind(targetFnPtr, targetPtr, _1))
-#define PELSE(targetPtr, targetFnPtr) Else(targetPtr, boost::bind(targetFnPtr, targetPtr, _1))
-
-
-TEST(MessageBroker2, TestPermanentConnectionSimpleUseCase)
-{
-  MessageBroker broker;
-  MyObservable  observable(broker);
-  MyObserver    observer(broker);
-
-  // create a permanent connection between an observable and an observer
-  CONNECT_MESSAGES(&observable, CustomMessageType_Completed, &observer, &MyObserver::HandleCompletedMessage);
-
-  testCounter = 0;
-  observable.EmitMessage(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.EmitMessage(MyObservable::MyCustomMessage(20));
-  ASSERT_EQ(20, testCounter);
-}
-
-TEST(MessageBroker2, TestPermanentConnectionDeleteObserver)
-{
-  MessageBroker broker;
-  MyObservable  observable(broker);
-  MyObserver*   observer = new MyObserver(broker);
-
-  // create a permanent connection between an observable and an observer
-  CONNECT_MESSAGES(&observable, CustomMessageType_Completed, observer, &MyObserver::HandleCompletedMessage);
-
-  testCounter = 0;
-  observable.EmitMessage(MyObservable::MyCustomMessage(12));
-  ASSERT_EQ(12, 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.EmitMessage(MyObservable::MyCustomMessage(20));
-  ASSERT_EQ(0, testCounter);
-}
-
-
-TEST(MessageBroker2, TestPromiseSuccessFailure)
-{
-  MessageBroker broker;
-  MyPromiseSource  source(broker);
-  MyPromiseTarget target(broker);
-
-  // test a successful promise
-  source.StartSomethingAsync()
-      .PTHEN(&target, &MyPromiseTarget::IncrementCounter)
-      .PELSE(&target, &MyPromiseTarget::DecrementCounter);
-
-  testCounter = 0;
-  source.CompleteSomethingAsyncWithSuccess();
-  ASSERT_EQ(1, testCounter);
-
-  // test a failing promise
-  source.StartSomethingAsync()
-      .PTHEN(&target, &MyPromiseTarget::IncrementCounter)
-      .PELSE(&target, &MyPromiseTarget::DecrementCounter);
-
-  testCounter = 0;
-  source.CompleteSomethingAsyncWithFailure();
-  ASSERT_EQ(-1, testCounter);
-}
-
-//TEST(MessageBroker2, TestPromiseDeleteTarget)
-//{
-//  MessageBroker broker;
-//  MyPromiseSource  source(broker);
-//  MyPromiseTarget target(broker);
-
-//  // test a successful promise
-//  source.StartSomethingAsync()
-//      .PTHEN(&target, &MyPromiseTarget::IncrementCounter)
-//      .PELSE(&target, &MyPromiseTarget::DecrementCounter);
-
-//  testCounter = 0;
-//  source.CompleteSomethingAsyncWithSuccess();
-//  ASSERT_EQ(1, testCounter);
-
-//  // test a failing promise
-//  source.StartSomethingAsync()
-//      .PTHEN(&target, &MyPromiseTarget::IncrementCounter)
-//      .PELSE(&target, &MyPromiseTarget::DecrementCounter);
-
-//  testCounter = 0;
-//  source.CompleteSomethingAsyncWithFailure();
-//  ASSERT_EQ(-1, testCounter);
-//}