changeset 633:b0652595b62a

Merge
author Benjamin Golinvaux <bgo@osimis.io>
date Thu, 09 May 2019 14:38:27 +0200
parents 500c3f70b6c2 (current diff) ea8322566596 (diff)
children 6a144a45b2d8
files Samples/Sdl/CMakeLists.txt
diffstat 28 files changed, 1302 insertions(+), 122 deletions(-) [+]
line wrap: on
line diff
--- a/Framework/Layers/DicomSeriesVolumeSlicer.cpp	Thu May 09 10:41:31 2019 +0200
+++ b/Framework/Layers/DicomSeriesVolumeSlicer.cpp	Thu May 09 14:38:27 2019 +0200
@@ -36,17 +36,17 @@
   {
     if (message.GetOrigin().GetSliceCount() > 0)
     {
-      EmitMessage(IVolumeSlicer::GeometryReadyMessage(*this));
+      BroadcastMessage(IVolumeSlicer::GeometryReadyMessage(*this));
     }
     else
     {
-      EmitMessage(IVolumeSlicer::GeometryErrorMessage(*this));
+      BroadcastMessage(IVolumeSlicer::GeometryErrorMessage(*this));
     }
   }
 
   void DicomSeriesVolumeSlicer::OnSliceGeometryError(const OrthancSlicesLoader::SliceGeometryErrorMessage& message)
   {
-    EmitMessage(IVolumeSlicer::GeometryErrorMessage(*this));
+    BroadcastMessage(IVolumeSlicer::GeometryErrorMessage(*this));
   }
 
 
@@ -73,17 +73,17 @@
   void DicomSeriesVolumeSlicer::OnSliceImageReady(const OrthancSlicesLoader::SliceImageReadyMessage& message)
   {
     // first notify that the pixel data of the frame is ready (targeted to, i.e: an image cache)
-    EmitMessage(FrameReadyMessage(*this, message.GetImage(), 
+    BroadcastMessage(FrameReadyMessage(*this, message.GetImage(), 
                                   message.GetEffectiveQuality(), message.GetSlice()));
 
     // then notify that the layer is ready for rendering
     RendererFactory factory(message);
-    EmitMessage(IVolumeSlicer::LayerReadyMessage(*this, factory, message.GetSlice().GetGeometry()));
+    BroadcastMessage(IVolumeSlicer::LayerReadyMessage(*this, factory, message.GetSlice().GetGeometry()));
   }
 
   void DicomSeriesVolumeSlicer::OnSliceImageError(const OrthancSlicesLoader::SliceImageErrorMessage& message)
   {
-    EmitMessage(IVolumeSlicer::LayerErrorMessage(*this, message.GetSlice().GetGeometry()));
+    BroadcastMessage(IVolumeSlicer::LayerErrorMessage(*this, message.GetSlice().GetGeometry()));
   }
 
 
--- a/Framework/Layers/DicomStructureSetSlicer.cpp	Thu May 09 10:41:31 2019 +0200
+++ b/Framework/Layers/DicomStructureSetSlicer.cpp	Thu May 09 14:38:27 2019 +0200
@@ -164,7 +164,7 @@
     if (loader_.HasStructureSet())
     {
       RendererFactory factory(loader_.GetStructureSet(), viewportPlane);
-      EmitMessage(IVolumeSlicer::LayerReadyMessage(*this, factory, viewportPlane));
+      BroadcastMessage(IVolumeSlicer::LayerReadyMessage(*this, factory, viewportPlane));
     }
   }
 }
--- a/Framework/Layers/DicomStructureSetSlicer.h	Thu May 09 10:41:31 2019 +0200
+++ b/Framework/Layers/DicomStructureSetSlicer.h	Thu May 09 14:38:27 2019 +0200
@@ -38,7 +38,7 @@
 
     void OnStructureSetLoaded(const IVolumeLoader::ContentChangedMessage& message)
     {
-      EmitMessage(IVolumeSlicer::ContentChangedMessage(*this));
+      BroadcastMessage(IVolumeSlicer::ContentChangedMessage(*this));
     }
 
   public:
--- a/Framework/Messages/IMessage.h	Thu May 09 10:41:31 2019 +0200
+++ b/Framework/Messages/IMessage.h	Thu May 09 14:38:27 2019 +0200
@@ -31,10 +31,10 @@
   class IMessage : public boost::noncopyable
   {
   private:
-    int messageType_;
+    MessageType messageType_;
     
   protected:
-    IMessage(const int& messageType) :
+    IMessage(MessageType messageType) :
       messageType_(messageType)
     {
     }
@@ -44,7 +44,7 @@
     {
     }
 
-    virtual int GetType() const
+    virtual MessageType GetType() const
     {
       return messageType_;
     }
@@ -53,7 +53,7 @@
 
   // base class to derive from to implement your own messages
   // it handles the message type for you
-  template <int type>
+  template <MessageType type>
   class BaseMessage : public IMessage
   {
   public:
@@ -63,7 +63,7 @@
     };
 
     BaseMessage() :
-      IMessage(static_cast<int>(Type))
+      IMessage(static_cast<MessageType>(Type))
     {
     }
   };
@@ -72,7 +72,7 @@
   // simple message implementation when no payload is needed
   // sample usage:
   // typedef NoPayloadMessage<MessageType_VolumeSlicer_GeometryReady> GeometryReadyMessage;
-  template <int type>
+  template <MessageType type>
   class NoPayloadMessage : public BaseMessage<type>
   {
   public:
@@ -85,7 +85,7 @@
   // simple message implementation when no payload is needed but the origin is required
   // sample usage:
   // typedef OriginMessage<MessageType_SliceLoader_GeometryError, OrthancSlicesLoader> SliceGeometryErrorMessage;
-  template <int type, typename TOrigin>
+  template <MessageType type, typename TOrigin>
   class OriginMessage : public BaseMessage<type>
   {
   private:
--- a/Framework/Messages/IObservable.cpp	Thu May 09 10:41:31 2019 +0200
+++ b/Framework/Messages/IObservable.cpp	Thu May 09 14:38:27 2019 +0200
@@ -84,7 +84,8 @@
     }
   }
   
-  void IObservable::EmitMessage(const IMessage& message)
+  void IObservable::EmitMessageInternal(const IObserver* receiver,
+                                        const IMessage& message)
   {
     Callables::const_iterator found = callables_.find(message.GetType());
 
@@ -94,14 +95,33 @@
              it = found->second.begin(); it != found->second.end(); ++it)
       {
         assert(*it != NULL);
-        if (broker_.IsActive(*(*it)->GetObserver()))
+
+        const IObserver* observer = (*it)->GetObserver();
+        if (broker_.IsActive(*observer))
         {
-          (*it)->Apply(message);
+          if (receiver == NULL ||    // Are we broadcasting?
+              observer == receiver)  // Not broadcasting, but this is the receiver
+          {
+            (*it)->Apply(message);
+          }
         }
       }
     }
   }
 
+
+  void IObservable::BroadcastMessage(const IMessage& message)
+  {
+    EmitMessageInternal(NULL, message);
+  }
+
+  
+  void IObservable::EmitMessage(const IObserver& observer,
+                                const IMessage& message)
+  {
+    EmitMessageInternal(&observer, message);
+  }
+
   
   void IObservable::RegisterForwarder(IMessageForwarder* forwarder)
   {
--- a/Framework/Messages/IObservable.h	Thu May 09 10:41:31 2019 +0200
+++ b/Framework/Messages/IObservable.h	Thu May 09 14:38:27 2019 +0200
@@ -42,6 +42,9 @@
     Callables       callables_;
     Forwarders      forwarders_;
 
+    void EmitMessageInternal(const IObserver* receiver,
+                             const IMessage& message);
+
   public:
     IObservable(MessageBroker& broker) :
       broker_(broker)
@@ -60,7 +63,10 @@
 
     void Unregister(IObserver* observer);
 
-    void EmitMessage(const IMessage& message);
+    void BroadcastMessage(const IMessage& message);
+
+    void EmitMessage(const IObserver& observer,
+                     const IMessage& message);
 
     // Takes ownsership
     void RegisterForwarder(IMessageForwarder* forwarder);
--- a/Framework/Messages/MessageForwarder.cpp	Thu May 09 10:41:31 2019 +0200
+++ b/Framework/Messages/MessageForwarder.cpp	Thu May 09 14:38:27 2019 +0200
@@ -28,7 +28,7 @@
 
   void IMessageForwarder::ForwardMessageInternal(const IMessage& message)
   {
-    emitter_.EmitMessage(message);
+    emitter_.BroadcastMessage(message);
   }
 
   void IMessageForwarder::RegisterForwarderInEmitter()
--- a/Framework/Radiography/RadiographyAlphaLayer.cpp	Thu May 09 10:41:31 2019 +0200
+++ b/Framework/Radiography/RadiographyAlphaLayer.cpp	Thu May 09 14:38:27 2019 +0200
@@ -46,7 +46,7 @@
     SetSize(image->GetWidth(), image->GetHeight());
     alpha_ = raii;
 
-    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
+    BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
   void RadiographyAlphaLayer::Render(Orthanc::ImageAccessor& buffer,
--- a/Framework/Radiography/RadiographyDicomLayer.cpp	Thu May 09 10:41:31 2019 +0200
+++ b/Framework/Radiography/RadiographyDicomLayer.cpp	Thu May 09 14:38:27 2019 +0200
@@ -103,7 +103,7 @@
     source_ = raii;
     ApplyConverter();
 
-    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
+    BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
   void RadiographyDicomLayer::Render(Orthanc::ImageAccessor& buffer,
--- a/Framework/Radiography/RadiographyLayer.cpp	Thu May 09 10:41:31 2019 +0200
+++ b/Framework/Radiography/RadiographyLayer.cpp	Thu May 09 14:38:27 2019 +0200
@@ -139,7 +139,7 @@
   {
     prefferedPhotometricDisplayMode_ = prefferedPhotometricDisplayMode;
 
-    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
+    BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
   void RadiographyLayer::SetCrop(unsigned int x,
@@ -161,7 +161,7 @@
     geometry_.SetCrop(x, y, width, height);
     UpdateTransform();
 
-    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
+    BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
   void RadiographyLayer::SetGeometry(const Geometry& geometry)
@@ -173,7 +173,7 @@
       UpdateTransform();
     }
 
-    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
+    BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
 
@@ -201,7 +201,7 @@
     geometry_.SetAngle(angle);
     UpdateTransform();
 
-    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
+    BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
 
@@ -220,7 +220,7 @@
     height_ = height;
 
     UpdateTransform();
-    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
+    BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
 
@@ -298,7 +298,7 @@
   {
     geometry_.SetPan(x, y);
     UpdateTransform();
-    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
+    BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
 
@@ -307,7 +307,7 @@
   {
     geometry_.SetPixelSpacing(x, y);
     UpdateTransform();
-    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
+    BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
 
--- a/Framework/Radiography/RadiographyMaskLayer.cpp	Thu May 09 10:41:31 2019 +0200
+++ b/Framework/Radiography/RadiographyMaskLayer.cpp	Thu May 09 14:38:27 2019 +0200
@@ -63,7 +63,7 @@
       corners_.push_back(corner);
     invalidated_ = true;
 
-    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
+    BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
   void RadiographyMaskLayer::SetCorners(const std::vector<Orthanc::ImageProcessing::ImagePoint>& corners)
@@ -71,7 +71,7 @@
     corners_ = corners;
     invalidated_ = true;
 
-    EmitMessage(RadiographyLayer::LayerEditedMessage(*this));
+    BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this));
   }
 
   void RadiographyMaskLayer::Render(Orthanc::ImageAccessor& buffer,
--- a/Framework/Radiography/RadiographyScene.cpp	Thu May 09 10:41:31 2019 +0200
+++ b/Framework/Radiography/RadiographyScene.cpp	Thu May 09 14:38:27 2019 +0200
@@ -140,8 +140,8 @@
     raii->SetIndex(index);
     layers_[index] = raii.release();
 
-    EmitMessage(GeometryChangedMessage(*this, *layer));
-    EmitMessage(ContentChangedMessage(*this, *layer));
+    BroadcastMessage(GeometryChangedMessage(*this, *layer));
+    BroadcastMessage(ContentChangedMessage(*this, *layer));
     layer->RegisterObserverCallback(new Callable<RadiographyScene, RadiographyLayer::LayerEditedMessage>(*this, &RadiographyScene::OnLayerEdited));
 
     return *layer;
@@ -149,7 +149,7 @@
 
   void RadiographyScene::OnLayerEdited(const RadiographyLayer::LayerEditedMessage& message)
   {
-    EmitMessage(RadiographyScene::LayerEditedMessage(*this, message.GetOrigin()));
+    BroadcastMessage(RadiographyScene::LayerEditedMessage(*this, message.GetOrigin()));
   }
 
   RadiographyScene::RadiographyScene(MessageBroker& broker) :
@@ -266,7 +266,7 @@
     windowingCenter_ = center;
     windowingWidth_ = width;
 
-    EmitMessage(RadiographyScene::WindowingChangedMessage(*this));
+    BroadcastMessage(RadiographyScene::WindowingChangedMessage(*this));
   }
 
 
@@ -447,7 +447,7 @@
         windowingWidth_ = w;
       }
 
-      EmitMessage(GeometryChangedMessage(*this, *(layer->second)));
+      BroadcastMessage(GeometryChangedMessage(*this, *(layer->second)));
     }
   }
 
@@ -474,7 +474,7 @@
       reader->ReadFromMemory(content);
       dynamic_cast<RadiographyDicomLayer*>(layer->second)->SetSourceImage(reader.release());
 
-      EmitMessage(ContentChangedMessage(*this, *(layer->second)));
+      BroadcastMessage(ContentChangedMessage(*this, *(layer->second)));
     }
   }
 
--- a/Framework/Scene2D/FloatTextureSceneLayer.h	Thu May 09 10:41:31 2019 +0200
+++ b/Framework/Scene2D/FloatTextureSceneLayer.h	Thu May 09 14:38:27 2019 +0200
@@ -33,6 +33,7 @@
     float            customWidth_;
 
   public:
+    // The pixel format must be "Float32"
     FloatTextureSceneLayer(const Orthanc::ImageAccessor& texture);
 
     void SetWindowing(ImageWindowing windowing);
--- a/Framework/SmartLoader.cpp	Thu May 09 10:41:31 2019 +0200
+++ b/Framework/SmartLoader.cpp	Thu May 09 14:38:27 2019 +0200
@@ -95,7 +95,7 @@
         LOG(WARNING) << "ScheduleLayerCreation for CachedSlice (image is loaded): " << slice_->GetOrthancInstanceId();
 
         RendererFactory factory(*this);   
-        EmitMessage(IVolumeSlicer::LayerReadyMessage(*this, factory, slice_->GetGeometry()));
+        BroadcastMessage(IVolumeSlicer::LayerReadyMessage(*this, factory, slice_->GetGeometry()));
       }
       else
       {
@@ -174,7 +174,7 @@
 
     if (cachedSlice != NULL)
     {
-      EmitMessage(IVolumeSlicer::GeometryReadyMessage(*cachedSlice));
+      BroadcastMessage(IVolumeSlicer::GeometryReadyMessage(*cachedSlice));
     }
 
   }
@@ -242,7 +242,7 @@
     cachedSlices_[sliceKeyId] = boost::shared_ptr<CachedSlice>(cachedSlice);
 
     // re-emit original Layer message to observers
-    EmitMessage(message);
+    BroadcastMessage(message);
   }
 
 
@@ -264,7 +264,7 @@
     cachedSlices_[sliceKeyId] = cachedSlice;
 
     // re-emit original Layer message to observers
-    EmitMessage(message);
+    BroadcastMessage(message);
   }
 
 
@@ -286,6 +286,6 @@
     }
 
     // re-emit original Layer message to observers
-    EmitMessage(message);
+    BroadcastMessage(message);
   }
 }
--- a/Framework/StoneEnumerations.cpp	Thu May 09 10:41:31 2019 +0200
+++ b/Framework/StoneEnumerations.cpp	Thu May 09 14:38:27 2019 +0200
@@ -27,20 +27,18 @@
 
 namespace OrthancStone
 {  
-  bool StringToSopClassUid(SopClassUid& result,
-                           const std::string& source)
+  SopClassUid StringToSopClassUid(const std::string& source)
   {
     std::string s = Orthanc::Toolbox::StripSpaces(source);
 
     if (s == "1.2.840.10008.5.1.4.1.1.481.2")
     {
-      result = SopClassUid_RTDose;
-      return true;
+      return SopClassUid_RTDose;
     }
     else
     {
-      //LOG(INFO) << "Unknown SOP class UID: " << source;
-      return false;
+      //LOG(INFO) << "Other SOP class UID: " << source;
+      return SopClassUid_Other;
     }
   }  
 
--- a/Framework/StoneEnumerations.h	Thu May 09 10:41:31 2019 +0200
+++ b/Framework/StoneEnumerations.h	Thu May 09 14:38:27 2019 +0200
@@ -98,6 +98,7 @@
 
   enum SopClassUid
   {
+    SopClassUid_Other,
     SopClassUid_RTDose
   };
 
@@ -192,8 +193,7 @@
   };
 
   
-  bool StringToSopClassUid(SopClassUid& result,
-                           const std::string& source);
+  SopClassUid StringToSopClassUid(const std::string& source);
 
   void ComputeWindowing(float& targetCenter,
                         float& targetWidth,
--- a/Framework/Toolbox/OrthancSlicesLoader.cpp	Thu May 09 10:41:31 2019 +0200
+++ b/Framework/Toolbox/OrthancSlicesLoader.cpp	Thu May 09 14:38:27 2019 +0200
@@ -177,7 +177,7 @@
   {
     OrthancSlicesLoader::SliceImageReadyMessage msg
       (*this, operation.GetSliceIndex(), operation.GetSlice(), image, operation.GetQuality());
-    EmitMessage(msg);
+    BroadcastMessage(msg);
   }
   
   
@@ -185,7 +185,7 @@
   {
     OrthancSlicesLoader::SliceImageErrorMessage msg
       (*this, operation.GetSliceIndex(), operation.GetSlice(), operation.GetQuality());
-    EmitMessage(msg);
+    BroadcastMessage(msg);
   }
   
   
@@ -210,18 +210,18 @@
     if (ok)
     {
       LOG(INFO) << "Loaded a series with " << slices_.GetSliceCount() << " slice(s)";
-      EmitMessage(SliceGeometryReadyMessage(*this));
+      BroadcastMessage(SliceGeometryReadyMessage(*this));
     }
     else
     {
       LOG(ERROR) << "This series is empty";
-      EmitMessage(SliceGeometryErrorMessage(*this));
+      BroadcastMessage(SliceGeometryErrorMessage(*this));
     }
   }
   
   void OrthancSlicesLoader::OnGeometryError(const IWebService::HttpRequestErrorMessage& message)
   {
-    EmitMessage(SliceGeometryErrorMessage(*this));
+    BroadcastMessage(SliceGeometryErrorMessage(*this));
     state_ = State_Error;
   }
 
@@ -296,7 +296,7 @@
       else
       {
         LOG(WARNING) << "Skipping invalid multi-frame instance " << instanceId;
-        EmitMessage(SliceGeometryErrorMessage(*this));
+        BroadcastMessage(SliceGeometryErrorMessage(*this));
         return;
       }
     }
@@ -323,12 +323,12 @@
     {
       LOG(INFO) << "Loaded instance geometry " << instanceId;
       slices_.AddSlice(slice.release());
-      EmitMessage(SliceGeometryReadyMessage(*this));
+      BroadcastMessage(SliceGeometryReadyMessage(*this));
     }
     else
     {
       LOG(WARNING) << "Skipping invalid instance " << instanceId;
-      EmitMessage(SliceGeometryErrorMessage(*this));
+      BroadcastMessage(SliceGeometryErrorMessage(*this));
     }
   }
   
--- a/Framework/Toolbox/Slice.cpp	Thu May 09 10:41:31 2019 +0200
+++ b/Framework/Toolbox/Slice.cpp	Thu May 09 14:38:27 2019 +0200
@@ -194,20 +194,16 @@
       geometry_ = CoordinateSystem3D(position, orientation);
 
       bool ok = true;
-      SopClassUid tmp;
 
-      if (StringToSopClassUid(tmp, sopClassUid_))
+      switch (StringToSopClassUid(sopClassUid_))
       {
-        switch (tmp)
-        {
-          case SopClassUid_RTDose:
-            type_ = Type_OrthancRawFrame;
-            ok = ComputeRTDoseGeometry(dataset, frame);
-            break;
+        case SopClassUid_RTDose:
+          type_ = Type_OrthancRawFrame;
+          ok = ComputeRTDoseGeometry(dataset, frame);
+          break;
             
-          default:
-            break;
-        }
+        default:
+          break;
       }
 
       if (!ok)
--- a/Framework/Viewport/IViewport.h	Thu May 09 10:41:31 2019 +0200
+++ b/Framework/Viewport/IViewport.h	Thu May 09 14:38:27 2019 +0200
@@ -89,7 +89,7 @@
     // TODO Why should this be virtual?
     virtual void NotifyContentChanged()
     {
-      EmitMessage(ViewportChangedMessage(*this));
+      BroadcastMessage(ViewportChangedMessage(*this));
     }
   };
 }
--- a/Framework/Volumes/StructureSetLoader.cpp	Thu May 09 10:41:31 2019 +0200
+++ b/Framework/Volumes/StructureSetLoader.cpp	Thu May 09 14:38:27 2019 +0200
@@ -44,7 +44,7 @@
     MessagingToolbox::ConvertDataset(slice, dataset);
     structureSet_->AddReferencedSlice(slice);
 
-    EmitMessage(ContentChangedMessage(*this));
+    BroadcastMessage(ContentChangedMessage(*this));
   }
   
 
@@ -63,7 +63,7 @@
                             new Callable<StructureSetLoader, OrthancApiClient::JsonResponseReadyMessage>(*this, &StructureSetLoader::OnLookupCompleted));
     }
 
-    EmitMessage(GeometryReadyMessage(*this));
+    BroadcastMessage(GeometryReadyMessage(*this));
   }
 
   
--- a/Framework/Widgets/SliceViewerWidget.cpp	Thu May 09 10:41:31 2019 +0200
+++ b/Framework/Widgets/SliceViewerWidget.cpp	Thu May 09 14:38:27 2019 +0200
@@ -527,7 +527,7 @@
       InvalidateAllLayers();   // TODO Removing this line avoid loading twice the image in WASM
     }
 
-    EmitMessage(DisplayedSliceMessage(*this, displayedSlice));
+    BroadcastMessage(DisplayedSliceMessage(*this, displayedSlice));
   }
 
 
@@ -541,7 +541,7 @@
       changedLayers_[i] = true;
       //layers_[i]->ScheduleLayerCreation(plane_);
     }
-    EmitMessage(GeometryChangedMessage(*this));
+    BroadcastMessage(GeometryChangedMessage(*this));
   }
   
 
@@ -579,7 +579,7 @@
       InvalidateLayer(index);
     }
     
-    EmitMessage(SliceViewerWidget::ContentChangedMessage(*this));
+    BroadcastMessage(SliceViewerWidget::ContentChangedMessage(*this));
   }
   
 
@@ -594,7 +594,7 @@
       }
     }
     
-    EmitMessage(SliceViewerWidget::ContentChangedMessage(*this));
+    BroadcastMessage(SliceViewerWidget::ContentChangedMessage(*this));
   }
   
   
@@ -607,7 +607,7 @@
       UpdateLayer(index, message.CreateRenderer(), message.GetSlice());
     }
     
-    EmitMessage(SliceViewerWidget::ContentChangedMessage(*this));
+    BroadcastMessage(SliceViewerWidget::ContentChangedMessage(*this));
   }
 
 
@@ -621,7 +621,7 @@
       // TODO
       //UpdateLayer(index, new SliceOutlineRenderer(slice), slice);
 
-      EmitMessage(SliceViewerWidget::ContentChangedMessage(*this));
+      BroadcastMessage(SliceViewerWidget::ContentChangedMessage(*this));
     }
   }
 
--- a/Framework/dev.h	Thu May 09 10:41:31 2019 +0200
+++ b/Framework/dev.h	Thu May 09 14:38:27 2019 +0200
@@ -113,7 +113,7 @@
       if (loader_.GetSliceCount() == 0)
       {
         LOG(ERROR) << "Empty volume image";
-        EmitMessage(ISlicedVolume::GeometryErrorMessage(*this));
+        BroadcastMessage(ISlicedVolume::GeometryErrorMessage(*this));
         return;
       }
 
@@ -121,7 +121,7 @@
       {
         if (!IsCompatible(loader_.GetSlice(0), loader_.GetSlice(i)))
         {
-          EmitMessage(ISlicedVolume::GeometryErrorMessage(*this));
+          BroadcastMessage(ISlicedVolume::GeometryErrorMessage(*this));
           return;
         }
       }
@@ -145,7 +145,7 @@
                                    0.001 /* this is expressed in mm */))
         {
           LOG(ERROR) << "The distance between successive slices is not constant in a volume image";
-          EmitMessage(ISlicedVolume::GeometryErrorMessage(*this));
+          BroadcastMessage(ISlicedVolume::GeometryErrorMessage(*this));
           return;
         }
       }
@@ -172,7 +172,7 @@
 
       // TODO Check the DicomFrameConverter are constant
 
-      EmitMessage(ISlicedVolume::GeometryReadyMessage(*this));
+      BroadcastMessage(ISlicedVolume::GeometryReadyMessage(*this));
     }
 
 
@@ -181,7 +181,7 @@
       assert(&message.GetOrigin() == &loader_);
 
       LOG(ERROR) << "Unable to download a volume image";
-      EmitMessage(ISlicedVolume::GeometryErrorMessage(*this));
+      BroadcastMessage(ISlicedVolume::GeometryErrorMessage(*this));
     }
 
 
@@ -194,12 +194,12 @@
         Orthanc::ImageProcessing::Copy(writer.GetAccessor(), message.GetImage());
       }
 
-      EmitMessage(ISlicedVolume::SliceContentChangedMessage
+      BroadcastMessage(ISlicedVolume::SliceContentChangedMessage
                   (*this, message.GetSliceIndex(), message.GetSlice()));
 
       if (pendingSlices_ == 1)
       {
-        EmitMessage(ISlicedVolume::VolumeReadyMessage(*this));
+        BroadcastMessage(ISlicedVolume::VolumeReadyMessage(*this));
         pendingSlices_ = 0;
       }
       else if (pendingSlices_ > 1)
@@ -540,21 +540,21 @@
       coronalGeometry_.reset(new VolumeImageGeometry(volume_, VolumeProjection_Coronal));
       sagittalGeometry_.reset(new VolumeImageGeometry(volume_, VolumeProjection_Sagittal));
 
-      EmitMessage(IVolumeSlicer::GeometryReadyMessage(*this));
+      BroadcastMessage(IVolumeSlicer::GeometryReadyMessage(*this));
     }
 
     void OnGeometryError(const ISlicedVolume::GeometryErrorMessage& message)
     {
       assert(&message.GetOrigin() == &volume_);
 
-      EmitMessage(IVolumeSlicer::GeometryErrorMessage(*this));
+      BroadcastMessage(IVolumeSlicer::GeometryErrorMessage(*this));
     }
 
     void OnContentChanged(const ISlicedVolume::ContentChangedMessage& message)
     {
       assert(&message.GetOrigin() == &volume_);
 
-      EmitMessage(IVolumeSlicer::ContentChangedMessage(*this));
+      BroadcastMessage(IVolumeSlicer::ContentChangedMessage(*this));
     }
 
     void OnSliceContentChanged(const ISlicedVolume::SliceContentChangedMessage& message)
@@ -564,7 +564,7 @@
       //IVolumeSlicer::OnSliceContentChange(slice);
 
       // TODO Improve this?
-      EmitMessage(IVolumeSlicer::ContentChangedMessage(*this));
+      BroadcastMessage(IVolumeSlicer::ContentChangedMessage(*this));
     }
 
     const VolumeImageGeometry& GetProjectionGeometry(VolumeProjection projection)
@@ -697,14 +697,14 @@
 
           RendererFactory factory(*frame, *slice, isFullQuality);
 
-          EmitMessage(IVolumeSlicer::LayerReadyMessage(*this, factory, slice->GetGeometry()));
+          BroadcastMessage(IVolumeSlicer::LayerReadyMessage(*this, factory, slice->GetGeometry()));
           return;
         }
       }
 
       // Error
       CoordinateSystem3D slice;
-      EmitMessage(IVolumeSlicer::LayerErrorMessage(*this, slice));
+      BroadcastMessage(IVolumeSlicer::LayerErrorMessage(*this, slice));
     }
   };
 
@@ -906,7 +906,7 @@
       IVolumeSlicer(broker),
       otherPlane_(otherPlane)
     {
-      EmitMessage(IVolumeSlicer::GeometryReadyMessage(*this));
+      BroadcastMessage(IVolumeSlicer::GeometryReadyMessage(*this));
     }
 
     virtual bool GetExtent(std::vector<Vector>& points,
@@ -929,7 +929,7 @@
                                                viewportSlice.GetOrigin(), viewportSlice.GetNormal()))
       {
         // The two slice are parallel, don't try and display the intersection
-        EmitMessage(IVolumeSlicer::LayerErrorMessage(*this, reference.GetGeometry()));
+        BroadcastMessage(IVolumeSlicer::LayerErrorMessage(*this, reference.GetGeometry()));
       }
       else
       {
@@ -945,12 +945,12 @@
                                                  extent.GetX2(), extent.GetY2()))
         {
           RendererFactory factory(x1, y1, x2, y2, slice);
-          EmitMessage(IVolumeSlicer::LayerReadyMessage(*this, factory, reference.GetGeometry()));
+          BroadcastMessage(IVolumeSlicer::LayerReadyMessage(*this, factory, reference.GetGeometry()));
         }
         else
         {
           // Error: Parallel slices
-          EmitMessage(IVolumeSlicer::LayerErrorMessage(*this, reference.GetGeometry()));
+          BroadcastMessage(IVolumeSlicer::LayerErrorMessage(*this, reference.GetGeometry()));
         }
       }
     }
--- a/Samples/Sdl/CMakeLists.txt	Thu May 09 10:41:31 2019 +0200
+++ b/Samples/Sdl/CMakeLists.txt	Thu May 09 14:38:27 2019 +0200
@@ -83,3 +83,8 @@
 
 target_link_libraries(TrackerSample OrthancStone)
 
+add_executable(Loader
+  Loader.cpp
+  )
+
+target_link_libraries(Loader OrthancStone)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/Sdl/Loader.cpp	Thu May 09 14:38:27 2019 +0200
@@ -0,0 +1,1151 @@
+/**
+ * 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/>.
+ **/
+
+// From Stone
+#include "../../Framework/Messages/ICallable.h"
+#include "../../Framework/Messages/IMessage.h"
+#include "../../Framework/Messages/IObservable.h"
+#include "../../Framework/Messages/MessageBroker.h"
+#include "../../Framework/StoneInitialization.h"
+#include "../../Framework/Toolbox/GeometryToolbox.h"
+#include "../../Framework/Volumes/ImageBuffer3D.h"
+
+// From Orthanc framework
+#include <Core/DicomFormat/DicomArray.h>
+#include <Core/DicomFormat/DicomImageInformation.h>
+#include <Core/HttpClient.h>
+#include <Core/IDynamicObject.h>
+#include <Core/Images/Image.h>
+#include <Core/Images/ImageProcessing.h>
+#include <Core/Images/PngWriter.h>
+#include <Core/Logging.h>
+#include <Core/MultiThreading/SharedMessageQueue.h>
+#include <Core/OrthancException.h>
+#include <Core/Toolbox.h>
+
+#include <json/reader.h>
+#include <json/value.h>
+#include <json/writer.h>
+
+#include <list>
+#include <stdio.h>
+
+
+
+namespace Refactoring
+{
+  class IOracleCommand : public boost::noncopyable
+  {
+  public:
+    enum Type
+    {
+      Type_OrthancApi
+    };
+
+    virtual ~IOracleCommand()
+    {
+    }
+
+    virtual Type GetType() const = 0;
+  };
+
+
+  class IMessageEmitter : public boost::noncopyable
+  {
+  public:
+    virtual ~IMessageEmitter()
+    {
+    }
+
+    virtual void EmitMessage(const OrthancStone::IObserver& observer,
+                             const OrthancStone::IMessage& message) = 0;
+  };
+
+
+  class IOracle : public boost::noncopyable
+  {
+  public:
+    virtual ~IOracle()
+    {
+    }
+
+    virtual void Schedule(const OrthancStone::IObserver& receiver,
+                          IOracleCommand* command) = 0;  // Takes ownership
+  };
+
+
+
+
+  class OracleCommandWithPayload : public IOracleCommand
+  {
+  private:
+    std::auto_ptr<Orthanc::IDynamicObject>  payload_;
+
+  public:
+    void SetPayload(Orthanc::IDynamicObject* payload)
+    {
+      if (payload == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+      else
+      {
+        payload_.reset(payload);
+      }    
+    }
+
+    bool HasPayload() const
+    {
+      return (payload_.get() != NULL);
+    }
+
+    const Orthanc::IDynamicObject& GetPayload() const
+    {
+      if (HasPayload())
+      {
+        return *payload_;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+  };
+
+
+
+  typedef std::map<std::string, std::string>  HttpHeaders;
+
+  class OrthancApiOracleCommand : public OracleCommandWithPayload
+  {
+  public:
+    class SuccessMessage : public OrthancStone::OriginMessage<OrthancStone::MessageType_HttpRequestSuccess,   // TODO
+                                                              OrthancApiOracleCommand>
+    {
+    private:
+      HttpHeaders   headers_;
+      std::string   answer_;
+
+    public:
+      SuccessMessage(const OrthancApiOracleCommand& command,
+                     const HttpHeaders& answerHeaders,
+                     std::string& answer  /* will be swapped to avoid a memcpy() */) :
+        OriginMessage(command),
+        headers_(answerHeaders),
+        answer_(answer)
+      {
+      }
+
+      const std::string& GetAnswer() const
+      {
+        return answer_;
+      }
+
+      void ParseJsonBody(Json::Value& target) const
+      {
+        Json::Reader reader;
+        if (!reader.parse(answer_, target))
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+        }
+      }
+
+      const HttpHeaders&  GetAnswerHeaders() const
+      {
+        return headers_;
+      }
+    };
+
+
+    class FailureMessage : public OrthancStone::OriginMessage<OrthancStone::MessageType_HttpRequestError,   // TODO
+                                                              OrthancApiOracleCommand>
+    {
+    private:
+      Orthanc::HttpStatus  status_;
+
+    public:
+      FailureMessage(const OrthancApiOracleCommand& command,
+                     Orthanc::HttpStatus status) :
+        OriginMessage(command),
+        status_(status)
+      {
+      }
+
+      Orthanc::HttpStatus GetHttpStatus() const
+      {
+        return status_;
+      }
+    };
+
+
+  private:
+    Orthanc::HttpMethod  method_;
+    std::string          uri_;
+    std::string          body_;
+    HttpHeaders          headers_;
+    unsigned int         timeout_;
+
+    std::auto_ptr< OrthancStone::MessageHandler<SuccessMessage> >  successCallback_;
+    std::auto_ptr< OrthancStone::MessageHandler<FailureMessage> >  failureCallback_;
+
+  public:
+    OrthancApiOracleCommand() :
+      method_(Orthanc::HttpMethod_Get),
+      uri_("/"),
+      timeout_(10)
+    {
+    }
+
+    virtual Type GetType() const
+    {
+      return Type_OrthancApi;
+    }
+
+    void SetMethod(Orthanc::HttpMethod method)
+    {
+      method_ = method;
+    }
+
+    void SetUri(const std::string& uri)
+    {
+      uri_ = uri;
+    }
+
+    void SetBody(const std::string& body)
+    {
+      body_ = body;
+    }
+
+    void SetBody(const Json::Value& json)
+    {
+      Json::FastWriter writer;
+      body_ = writer.write(json);
+    }
+
+    void SetHttpHeader(const std::string& key,
+                       const std::string& value)
+    {
+      headers_[key] = value;
+    }
+
+    Orthanc::HttpMethod GetMethod() const
+    {
+      return method_;
+    }
+
+    const std::string& GetUri() const
+    {
+      return uri_;
+    }
+
+    const std::string& GetBody() const
+    {
+      if (method_ == Orthanc::HttpMethod_Post ||
+          method_ == Orthanc::HttpMethod_Put)
+      {
+        return body_;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    const HttpHeaders& GetHttpHeaders() const
+    {
+      return headers_;
+    }
+
+    void SetTimeout(unsigned int seconds)
+    {
+      timeout_ = seconds;
+    }
+
+    unsigned int GetTimeout() const
+    {
+      return timeout_;
+    }
+  };
+
+
+
+  class NativeOracle : public IOracle
+  {
+  private:
+    class Item : public Orthanc::IDynamicObject
+    {
+    private:
+      const OrthancStone::IObserver&  receiver_;
+      std::auto_ptr<IOracleCommand>   command_;
+
+    public:
+      Item(const OrthancStone::IObserver& receiver,
+           IOracleCommand* command) :
+        receiver_(receiver),
+        command_(command)
+      {
+        if (command == NULL)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+        }
+      }
+
+      const OrthancStone::IObserver& GetReceiver() const
+      {
+        return receiver_;
+      }
+
+      const IOracleCommand& GetCommand() const
+      {
+        assert(command_.get() != NULL);
+        return *command_;
+      }
+    };
+
+
+    enum State
+    {
+      State_Setup,
+      State_Running,
+      State_Stopped
+    };
+
+
+    IMessageEmitter&               emitter_;
+    Orthanc::WebServiceParameters  orthanc_;
+    Orthanc::SharedMessageQueue    queue_;
+    State                          state_;
+    boost::mutex                   mutex_;
+    std::vector<boost::thread*>    workers_;
+
+
+    void Execute(const OrthancStone::IObserver& receiver,
+                 const OrthancApiOracleCommand& command)
+    {
+      Orthanc::HttpClient  client(orthanc_, command.GetUri());
+      client.SetMethod(command.GetMethod());
+      client.SetTimeout(command.GetTimeout());
+
+      if (command.GetMethod() == Orthanc::HttpMethod_Post ||
+          command.GetMethod() == Orthanc::HttpMethod_Put)
+      {
+        client.SetBody(command.GetBody());
+      }
+      
+      {
+        const HttpHeaders& headers = command.GetHttpHeaders();
+        for (HttpHeaders::const_iterator it = headers.begin(); it != headers.end(); it++ )
+        {
+          client.AddHeader(it->first, it->second);
+        }
+      }
+
+      std::string answer;
+      HttpHeaders answerHeaders;
+
+      bool success;
+      try
+      {
+        success = client.Apply(answer, answerHeaders);
+      }
+      catch (Orthanc::OrthancException& e)
+      {
+        success = false;
+      }
+
+      if (success)
+      {
+        OrthancApiOracleCommand::SuccessMessage message(command, answerHeaders, answer);
+        emitter_.EmitMessage(receiver, message);
+      }
+      else
+      {
+        OrthancApiOracleCommand::FailureMessage message(command, client.GetLastStatus());
+        emitter_.EmitMessage(receiver, message);
+      }
+    }
+
+
+
+    void Step()
+    {
+      std::auto_ptr<Orthanc::IDynamicObject>  object(queue_.Dequeue(100));
+
+      if (object.get() != NULL)
+      {
+        const Item& item = dynamic_cast<Item&>(*object);
+
+        try
+        {
+          switch (item.GetCommand().GetType())
+          {
+            case IOracleCommand::Type_OrthancApi:
+              Execute(item.GetReceiver(), 
+                      dynamic_cast<const OrthancApiOracleCommand&>(item.GetCommand()));
+              break;
+
+            default:
+              throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+          }
+        }
+        catch (Orthanc::OrthancException& e)
+        {
+          LOG(ERROR) << "Exception within the oracle: " << e.What();
+        }
+        catch (...)
+        {
+          LOG(ERROR) << "Native exception within the oracle";
+        }
+      }
+    }
+
+
+    static void Worker(NativeOracle* that)
+    {
+      assert(that != NULL);
+      
+      for (;;)
+      {
+        {
+          boost::mutex::scoped_lock lock(that->mutex_);
+          if (that->state_ != State_Running)
+          {
+            return;
+          }
+        }
+
+        that->Step();
+      }
+    }
+
+
+    void StopInternal()
+    {
+      {
+        boost::mutex::scoped_lock lock(mutex_);
+
+        if (state_ == State_Setup ||
+            state_ == State_Stopped)
+        {
+          return;
+        }
+        else
+        {
+          state_ = State_Stopped;
+        }
+      }
+
+      for (size_t i = 0; i < workers_.size(); i++)
+      {
+        if (workers_[i] != NULL)
+        {
+          if (workers_[i]->joinable())
+          {
+            workers_[i]->join();
+          }
+
+          delete workers_[i];
+        }
+      } 
+    }
+
+
+  public:
+    NativeOracle(IMessageEmitter& emitter) :
+    emitter_(emitter),
+      state_(State_Setup),
+      workers_(4)
+    {
+    }
+
+    virtual ~NativeOracle()
+    {
+      StopInternal();
+    }
+
+    void SetOrthancParameters(const Orthanc::WebServiceParameters& orthanc)
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+
+      if (state_ != State_Setup)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        orthanc_ = orthanc;
+      }
+    }
+
+    void SetWorkersCount(unsigned int count)
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+
+      if (count <= 0)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+      else if (state_ != State_Setup)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        workers_.resize(count);
+      }
+    }
+
+    void Start()
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+
+      if (state_ != State_Setup)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        state_ = State_Running;
+
+        for (unsigned int i = 0; i < workers_.size(); i++)
+        {
+          workers_[i] = new boost::thread(Worker, this);
+        }
+      }      
+    }
+
+    void Stop()
+    {
+      StopInternal();
+    }
+
+    virtual void Schedule(const OrthancStone::IObserver& receiver,
+                          IOracleCommand* command)
+    {
+      queue_.Enqueue(new Item(receiver, command));
+    }
+  };
+
+
+
+  class NativeApplicationContext : public IMessageEmitter
+  {
+  private:
+    boost::shared_mutex            mutex_;
+    OrthancStone::MessageBroker    broker_;
+    OrthancStone::IObservable      oracleObservable_;
+
+  public:
+    NativeApplicationContext() :
+      oracleObservable_(broker_)
+    {
+    }
+
+
+    virtual void EmitMessage(const OrthancStone::IObserver& observer,
+                             const OrthancStone::IMessage& message)
+    {
+      boost::unique_lock<boost::shared_mutex>  lock(mutex_);
+      oracleObservable_.EmitMessage(observer, message);
+    }
+
+
+    class ReaderLock : public boost::noncopyable
+    {
+    private:
+      NativeApplicationContext&                that_;
+      boost::shared_lock<boost::shared_mutex>  lock_;
+
+    public:
+      ReaderLock(NativeApplicationContext& that) : 
+      that_(that),
+      lock_(that.mutex_)
+      {
+      }
+    };
+
+
+    class WriterLock : public boost::noncopyable
+    {
+    private:
+      NativeApplicationContext&                that_;
+      boost::unique_lock<boost::shared_mutex>  lock_;
+
+    public:
+      WriterLock(NativeApplicationContext& that) : 
+      that_(that),
+      lock_(that.mutex_)
+      {
+      }
+
+      OrthancStone::MessageBroker& GetBroker() 
+      {
+        return that_.broker_;
+      }
+
+      OrthancStone::IObservable& GetOracleObservable()
+      {
+        return that_.oracleObservable_;
+      }
+    };
+  };
+
+
+
+  class DicomInstanceParameters : public boost::noncopyable
+  {
+  private:
+    Orthanc::DicomImageInformation    imageInformation_;
+    OrthancStone::SopClassUid         sopClassUid_;
+    double                            thickness_;
+    double                            pixelSpacingX_;
+    double                            pixelSpacingY_;
+    OrthancStone::CoordinateSystem3D  geometry_;
+    OrthancStone::Vector              frameOffsets_;
+    bool                              isColor_;
+    bool                              hasRescale_;
+    double                            rescaleOffset_;
+    double                            rescaleSlope_;
+    bool                              hasDefaultWindowing_;
+    float                             defaultWindowingCenter_;
+    float                             defaultWindowingWidth_;
+    Orthanc::PixelFormat              expectedPixelFormat_;
+
+    void ComputeDoseOffsets(const Orthanc::DicomMap& dicom)
+    {
+      // http://dicom.nema.org/medical/Dicom/2016a/output/chtml/part03/sect_C.8.8.3.2.html
+
+      {
+        std::string increment;
+
+        if (dicom.CopyToString(increment, Orthanc::DICOM_TAG_FRAME_INCREMENT_POINTER, false))
+        {
+          Orthanc::Toolbox::ToUpperCase(increment);
+          if (increment != "3004,000C")  // This is the "Grid Frame Offset Vector" tag
+          {
+            LOG(ERROR) << "RT-DOSE: Bad value for the \"FrameIncrementPointer\" tag";
+            return;
+          }
+        }
+      }
+
+      if (!OrthancStone::LinearAlgebra::ParseVector(frameOffsets_, dicom, Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR) ||
+          frameOffsets_.size() < imageInformation_.GetNumberOfFrames())
+      {
+        LOG(ERROR) << "RT-DOSE: No information about the 3D location of some slice(s)";
+        frameOffsets_.clear();
+      }
+      else
+      {
+        if (frameOffsets_.size() >= 2)
+        {
+          thickness_ = frameOffsets_[1] - frameOffsets_[0];
+
+          if (thickness_ < 0)
+          {
+            thickness_ = -thickness_;
+          }
+        }
+      }
+    }
+
+  public:
+    DicomInstanceParameters(const Orthanc::DicomMap& dicom) :
+      imageInformation_(dicom)
+    {
+      if (imageInformation_.GetNumberOfFrames() <= 0)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+            
+      std::string s;
+      if (!dicom.CopyToString(s, Orthanc::DICOM_TAG_SOP_CLASS_UID, false))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        sopClassUid_ = OrthancStone::StringToSopClassUid(s);
+      }
+
+      if (!dicom.ParseDouble(thickness_, Orthanc::DICOM_TAG_SLICE_THICKNESS))
+      {
+        thickness_ = 100.0 * std::numeric_limits<double>::epsilon();
+      }
+
+      OrthancStone::GeometryToolbox::GetPixelSpacing(pixelSpacingX_, pixelSpacingY_, dicom);
+
+      std::string position, orientation;
+      if (dicom.CopyToString(position, Orthanc::DICOM_TAG_IMAGE_POSITION_PATIENT, false) &&
+          dicom.CopyToString(orientation, Orthanc::DICOM_TAG_IMAGE_ORIENTATION_PATIENT, false))
+      {
+        geometry_ = OrthancStone::CoordinateSystem3D(position, orientation);
+      }
+
+      if (sopClassUid_ == OrthancStone::SopClassUid_RTDose)
+      {
+        ComputeDoseOffsets(dicom);
+      }
+
+      isColor_ = (imageInformation_.GetPhotometricInterpretation() != Orthanc::PhotometricInterpretation_Monochrome1 &&
+                  imageInformation_.GetPhotometricInterpretation() != Orthanc::PhotometricInterpretation_Monochrome2);
+
+      double doseGridScaling;
+
+      if (dicom.ParseDouble(rescaleOffset_, Orthanc::DICOM_TAG_RESCALE_INTERCEPT) &&
+          dicom.ParseDouble(rescaleSlope_, Orthanc::DICOM_TAG_RESCALE_SLOPE))
+      {
+        hasRescale_ = true;
+      }
+      else if (dicom.ParseDouble(doseGridScaling, Orthanc::DICOM_TAG_DOSE_GRID_SCALING))
+      {
+        hasRescale_ = true;
+        rescaleOffset_ = 0;
+        rescaleSlope_ = doseGridScaling;
+      }
+      else
+      {
+        hasRescale_ = false;
+      }
+
+      OrthancStone::Vector c, w;
+      if (OrthancStone::LinearAlgebra::ParseVector(c, dicom, Orthanc::DICOM_TAG_WINDOW_CENTER) &&
+          OrthancStone::LinearAlgebra::ParseVector(w, dicom, Orthanc::DICOM_TAG_WINDOW_WIDTH) &&
+          c.size() > 0 && 
+          w.size() > 0)
+      {
+        hasDefaultWindowing_ = true;
+        defaultWindowingCenter_ = static_cast<float>(c[0]);
+        defaultWindowingWidth_ = static_cast<float>(w[0]);
+      }
+      else
+      {
+        hasDefaultWindowing_ = false;
+      }
+
+      if (sopClassUid_ == OrthancStone::SopClassUid_RTDose)
+      {
+        switch (imageInformation_.GetBitsStored())
+        {
+          case 16:
+            expectedPixelFormat_ = Orthanc::PixelFormat_Grayscale16;
+            break;
+
+          case 32:
+            expectedPixelFormat_ = Orthanc::PixelFormat_Grayscale32;
+            break;
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+        } 
+      }
+      else if (isColor_)
+      {
+        expectedPixelFormat_ = Orthanc::PixelFormat_RGB24;
+      }
+      else if (imageInformation_.IsSigned())
+      {
+        expectedPixelFormat_ = Orthanc::PixelFormat_SignedGrayscale16;
+      }
+      else
+      {
+        expectedPixelFormat_ = Orthanc::PixelFormat_Grayscale16;
+      }
+    }
+
+    const Orthanc::DicomImageInformation& GetImageInformation() const
+    {
+      return imageInformation_;
+    }
+
+    OrthancStone::SopClassUid GetSopClassUid() const
+    {
+      return sopClassUid_;
+    }
+
+    double GetThickness() const
+    {
+      return thickness_;
+    }
+
+    double GetPixelSpacingX() const
+    {
+      return pixelSpacingX_;
+    }
+
+    double GetPixelSpacingY() const
+    {
+      return pixelSpacingY_;
+    }
+
+    const OrthancStone::CoordinateSystem3D&  GetGeometry() const
+    {
+      return geometry_;
+    }
+
+    OrthancStone::CoordinateSystem3D  GetFrameGeometry(unsigned int frame) const
+    {
+      if (frame >= imageInformation_.GetNumberOfFrames())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+
+      if (sopClassUid_ == OrthancStone::SopClassUid_RTDose)
+      {
+        if (frame >= frameOffsets_.size())
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+
+        return OrthancStone::CoordinateSystem3D(
+          geometry_.GetOrigin() + frameOffsets_[frame] * geometry_.GetNormal(),
+          geometry_.GetAxisX(),
+          geometry_.GetAxisY());
+      }
+    }
+
+    bool FrameContainsPlane(unsigned int frame,
+                            const OrthancStone::CoordinateSystem3D& plane) const
+    {
+      if (frame >= imageInformation_.GetNumberOfFrames())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+
+      OrthancStone::CoordinateSystem3D tmp = geometry_;
+
+      if (frame != 0)
+      {
+        tmp = GetFrameGeometry(frame);
+      }
+
+      bool opposite;   // Ignored
+      return (OrthancStone::GeometryToolbox::IsParallelOrOpposite(
+                opposite, tmp.GetNormal(), plane.GetNormal()) &&
+              OrthancStone::LinearAlgebra::IsNear(
+                tmp.ProjectAlongNormal(tmp.GetOrigin()),
+                tmp.ProjectAlongNormal(plane.GetOrigin()),
+                thickness_ / 2.0));
+    }
+
+    bool IsColor() const
+    {
+      return isColor_;
+    }
+
+    bool HasRescale() const
+    {
+      return hasRescale_;
+    }
+
+    double GetRescaleOffset() const
+    {
+      if (hasRescale_)
+      {
+        return rescaleOffset_;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    double GetRescaleSlope() const
+    {
+      if (hasRescale_)
+      {
+        return rescaleSlope_;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    bool HasDefaultWindowing() const
+    {
+      return hasDefaultWindowing_;
+    }
+
+    float GetDefaultWindowingCenter() const
+    {
+      if (hasDefaultWindowing_)
+      {
+        return defaultWindowingCenter_;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    float GetDefaultWindowingWidth() const
+    {
+      if (hasDefaultWindowing_)
+      {
+        return defaultWindowingWidth_;
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    Orthanc::PixelFormat GetExpectedPixelFormat() const
+    {
+      return expectedPixelFormat_;
+    }
+  };
+
+
+  class AxialVolumeOrthancLoader : public OrthancStone::IObserver
+  {
+  private:
+    class MessageHandler : public Orthanc::IDynamicObject
+    {
+    public:
+      virtual void Handle(const OrthancApiOracleCommand::SuccessMessage& message) const = 0;
+    };
+
+    void Handle(const OrthancApiOracleCommand::SuccessMessage& message)
+    {
+      dynamic_cast<const MessageHandler&>(message.GetOrigin().GetPayload()).Handle(message);
+    }
+
+
+    class LoadSeriesGeometryHandler : public MessageHandler
+    {
+    private:
+      AxialVolumeOrthancLoader&  that_;
+
+    public:
+      LoadSeriesGeometryHandler(AxialVolumeOrthancLoader& that) :
+      that_(that)
+      {
+      }
+
+      virtual void Handle(const OrthancApiOracleCommand::SuccessMessage& message) const
+      {
+        Json::Value value;
+        message.ParseJsonBody(value);
+
+        if (value.type() != Json::objectValue)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+        }
+
+        Json::Value::Members instances = value.getMemberNames();
+
+        for (size_t i = 0; i < instances.size(); i++)
+        {
+          Orthanc::DicomMap dicom;
+          dicom.FromDicomAsJson(value[instances[i]]);
+
+          DicomInstanceParameters instance(dicom);
+        }
+      }
+    };
+
+
+    class LoadInstanceGeometryHandler : public MessageHandler
+    {
+    private:
+      AxialVolumeOrthancLoader&  that_;
+
+    public:
+      LoadInstanceGeometryHandler(AxialVolumeOrthancLoader& that) :
+      that_(that)
+      {
+      }
+
+      virtual void Handle(const OrthancApiOracleCommand::SuccessMessage& message) const
+      {
+        Json::Value value;
+        message.ParseJsonBody(value);
+
+        if (value.type() != Json::objectValue)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+        }
+
+        Orthanc::DicomMap dicom;
+        dicom.FromDicomAsJson(value);
+
+        DicomInstanceParameters instance(dicom);
+      }
+    };
+
+
+    bool                                        active_;
+    std::auto_ptr<OrthancStone::ImageBuffer3D>  image_;
+
+
+  public:
+    AxialVolumeOrthancLoader(OrthancStone::IObservable& oracle) :
+      IObserver(oracle.GetBroker()),
+      active_(false)
+    {
+      oracle.RegisterObserverCallback(
+        new OrthancStone::Callable<AxialVolumeOrthancLoader, OrthancApiOracleCommand::SuccessMessage>
+        (*this, &AxialVolumeOrthancLoader::Handle));
+    }
+
+    void LoadSeries(IOracle& oracle,
+                    const std::string& seriesId)
+    {
+      if (active_)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+
+      active_ = true;
+
+      std::auto_ptr<Refactoring::OrthancApiOracleCommand> command(new Refactoring::OrthancApiOracleCommand);
+      command->SetUri("/series/" + seriesId + "/instances-tags");
+      command->SetPayload(new LoadSeriesGeometryHandler(*this));
+
+      oracle.Schedule(*this, command.release());
+    }
+
+    void LoadInstance(IOracle& oracle,
+                      const std::string& instanceId)
+    {
+      if (active_)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+
+      active_ = true;
+
+      // Tag "3004-000c" is "Grid Frame Offset Vector", which is
+      // mandatory to read RT DOSE, but is too long to be returned by default
+
+      // TODO => Should be part of a second call if needed
+
+      std::auto_ptr<Refactoring::OrthancApiOracleCommand> command(new Refactoring::OrthancApiOracleCommand);
+      command->SetUri("/instances/" + instanceId + "/tags?ignore-length=3004-000c");
+      command->SetPayload(new LoadInstanceGeometryHandler(*this));
+
+      oracle.Schedule(*this, command.release());
+    }
+  };
+
+}
+
+
+
+class Toto : public OrthancStone::IObserver
+{
+private:
+  void Handle(const Refactoring::OrthancApiOracleCommand::SuccessMessage& message)
+  {
+    Json::Value v;
+    message.ParseJsonBody(v);
+
+    printf("ICI [%s]\n", v.toStyledString().c_str());
+  }
+
+  void Handle(const Refactoring::OrthancApiOracleCommand::FailureMessage& message)
+  {
+    printf("ERROR %d\n", message.GetHttpStatus());
+  }
+
+public:
+  Toto(OrthancStone::IObservable& oracle) :
+    IObserver(oracle.GetBroker())
+  {
+    oracle.RegisterObserverCallback
+      (new OrthancStone::Callable
+       <Toto, Refactoring::OrthancApiOracleCommand::SuccessMessage>(*this, &Toto::Handle));
+  }
+};
+
+
+void Run(Refactoring::NativeApplicationContext& context)
+{
+  std::auto_ptr<Toto> toto;
+  std::auto_ptr<Refactoring::AxialVolumeOrthancLoader> loader1, loader2;
+
+  {
+    Refactoring::NativeApplicationContext::WriterLock lock(context);
+    toto.reset(new Toto(lock.GetOracleObservable()));
+    loader1.reset(new Refactoring::AxialVolumeOrthancLoader(lock.GetOracleObservable()));
+    loader2.reset(new Refactoring::AxialVolumeOrthancLoader(lock.GetOracleObservable()));
+  }
+
+  Refactoring::NativeOracle oracle(context);
+
+  {
+    Orthanc::WebServiceParameters p;
+    //p.SetUrl("http://localhost:8043/");
+    p.SetCredentials("orthanc", "orthanc");
+    oracle.SetOrthancParameters(p);
+  }
+
+  oracle.Start();
+
+  {
+    Json::Value v = Json::objectValue;
+    v["Level"] = "Series";
+    v["Query"] = Json::objectValue;
+
+    std::auto_ptr<Refactoring::OrthancApiOracleCommand>  command(new Refactoring::OrthancApiOracleCommand);
+    command->SetMethod(Orthanc::HttpMethod_Post);
+    command->SetUri("/tools/find");
+    command->SetBody(v);
+
+    oracle.Schedule(*toto, command.release());
+  }
+  
+  // 2017-11-17-Anonymized
+  loader1->LoadSeries(oracle, "cb3ea4d1-d08f3856-ad7b6314-74d88d77-60b05618");  // CT
+  loader2->LoadInstance(oracle, "41029085-71718346-811efac4-420e2c15-d39f99b6");  // RT-DOSE
+
+  boost::this_thread::sleep(boost::posix_time::seconds(1));
+
+  oracle.Stop();
+}
+
+
+
+/**
+ * IMPORTANT: The full arguments to "main()" are needed for SDL on
+ * Windows. Otherwise, one gets the linking error "undefined reference
+ * to `SDL_main'". https://wiki.libsdl.org/FAQWindows
+ **/
+int main(int argc, char* argv[])
+{
+  OrthancStone::StoneInitialize();
+  Orthanc::Logging::EnableInfoLevel(true);
+
+  try
+  {
+    Refactoring::NativeApplicationContext context;
+    Run(context);
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    LOG(ERROR) << "EXCEPTION: " << e.What();
+  }
+
+  OrthancStone::StoneFinalize();
+
+  return 0;
+}
--- a/Samples/WebAssembly/BasicScene.cpp	Thu May 09 10:41:31 2019 +0200
+++ b/Samples/WebAssembly/BasicScene.cpp	Thu May 09 14:38:27 2019 +0200
@@ -343,10 +343,16 @@
 
 void OrthancStone::WebAssemblyViewport::SetupEvents(const std::string& canvas)
 {
-  //emscripten_set_click_callback(canvas.c_str(), this, false, OnMouseEvent);
-  emscripten_set_mousedown_callback(canvas.c_str(), this, false, OnMouseEvent);
-  emscripten_set_mousemove_callback(canvas.c_str(), this, false, OnMouseEvent);
-  emscripten_set_mouseup_callback(canvas.c_str(), this, false, OnMouseEvent);
+  if (0)
+  {
+    emscripten_set_click_callback(canvas.c_str(), this, false, OnMouseEvent);
+  }
+  else
+  {
+    emscripten_set_mousedown_callback(canvas.c_str(), this, false, OnMouseEvent);
+    emscripten_set_mousemove_callback(canvas.c_str(), this, false, OnMouseEvent);
+    emscripten_set_mouseup_callback(canvas.c_str(), this, false, OnMouseEvent);
+  }
 }
 
 
--- a/Samples/WebAssembly/CMakeLists.txt	Thu May 09 10:41:31 2019 +0200
+++ b/Samples/WebAssembly/CMakeLists.txt	Thu May 09 14:38:27 2019 +0200
@@ -1,10 +1,3 @@
-
-# source ~/Downloads/emsdk/emsdk_env.sh
-# cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_BUILD_TYPE=Release -DALLOW_DOWNLOADS=ON .. -DCMAKE_INSTALL_PREFIX=/tmp/stone
-# ninja install
-# sudo docker run -p 4242:4242 -p 8042:8042 --rm -v /tmp/stone:/root/stone:ro jodogne/orthanc-plugins:1.5.6 /root/stone/Configuration.json --verbose
-
-
 cmake_minimum_required(VERSION 2.8.3)
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Samples/WebAssembly/NOTES.txt	Thu May 09 14:38:27 2019 +0200
@@ -0,0 +1,4 @@
+$ source ~/Downloads/emsdk/emsdk_env.sh
+$ cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_BUILD_TYPE=Release -DALLOW_DOWNLOADS=ON .. -DCMAKE_INSTALL_PREFIX=/tmp/stone
+$ ninja install
+$ sudo docker run -p 4242:4242 -p 8042:8042 --rm -v /tmp/stone:/root/stone:ro jodogne/orthanc-plugins:1.5.6 /root/stone/Configuration.json --verbose
--- a/UnitTestsSources/TestMessageBroker.cpp	Thu May 09 10:41:31 2019 +0200
+++ b/UnitTestsSources/TestMessageBroker.cpp	Thu May 09 14:38:27 2019 +0200
@@ -46,7 +46,7 @@
   class MyObservable : public IObservable
   {
   public:
-    struct MyCustomMessage: public BaseMessage<CustomMessageType_Completed>
+    struct MyCustomMessage: public BaseMessage<(MessageType) CustomMessageType_Completed>
     {
       int payload_;
 
@@ -160,18 +160,18 @@
   observable.RegisterObserverCallback(new Callable<MyObserver, MyObservable::MyCustomMessage>(observer, &MyObserver::HandleCompletedMessage));
 
   testCounter = 0;
-  observable.EmitMessage(MyObservable::MyCustomMessage(12));
+  observable.BroadcastMessage(MyObservable::MyCustomMessage(12));
   ASSERT_EQ(12, testCounter);
 
   // the connection is permanent; if we emit the same message again, the observer will be notified again
   testCounter = 0;
-  observable.EmitMessage(MyObservable::MyCustomMessage(20));
+  observable.BroadcastMessage(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));
+  observable.BroadcastMessage(MyObservable::MyCustomMessage(20));
   ASSERT_EQ(0, testCounter);
 }
 
@@ -186,12 +186,12 @@
   intermediate.RegisterObserverCallback(new Callable<MyObserver, MyObservable::MyCustomMessage>(observer, &MyObserver::HandleCompletedMessage));
 
   testCounter = 0;
-  observable.EmitMessage(MyObservable::MyCustomMessage(12));
+  observable.BroadcastMessage(MyObservable::MyCustomMessage(12));
   ASSERT_EQ(12, testCounter);
 
   // the connection is permanent; if we emit the same message again, the observer will be notified again
   testCounter = 0;
-  observable.EmitMessage(MyObservable::MyCustomMessage(20));
+  observable.BroadcastMessage(MyObservable::MyCustomMessage(20));
   ASSERT_EQ(20, testCounter);
 }
 
@@ -205,7 +205,7 @@
   observable.RegisterObserverCallback(new Callable<MyObserver, MyObservable::MyCustomMessage>(*observer, &MyObserver::HandleCompletedMessage));
 
   testCounter = 0;
-  observable.EmitMessage(MyObservable::MyCustomMessage(12));
+  observable.BroadcastMessage(MyObservable::MyCustomMessage(12));
   ASSERT_EQ(12, testCounter);
 
   // delete the observer and check that the callback is not called anymore
@@ -213,7 +213,7 @@
 
   // the connection is permanent; if we emit the same message again, the observer will be notified again
   testCounter = 0;
-  observable.EmitMessage(MyObservable::MyCustomMessage(20));
+  observable.BroadcastMessage(MyObservable::MyCustomMessage(20));
   ASSERT_EQ(0, testCounter);
 }
 
@@ -228,12 +228,12 @@
   intermediate->RegisterObserverCallback(new Callable<MyObserver, MyObservable::MyCustomMessage>(observer, &MyObserver::HandleCompletedMessage));
 
   testCounter = 0;
-  observable.EmitMessage(MyObservable::MyCustomMessage(12));
+  observable.BroadcastMessage(MyObservable::MyCustomMessage(12));
   ASSERT_EQ(12, testCounter);
 
   delete intermediate;
 
-  observable.EmitMessage(MyObservable::MyCustomMessage(20));
+  observable.BroadcastMessage(MyObservable::MyCustomMessage(20));
   ASSERT_EQ(12, testCounter);
 }
 
@@ -248,12 +248,12 @@
   intermediate.RegisterObserverCallback(new Callable<MyObserver, MyObservable::MyCustomMessage>(observer, &MyObserver::HandleCompletedMessage));
 
   testCounter = 0;
-  observable.EmitMessage(MyObservable::MyCustomMessage(12));
+  observable.BroadcastMessage(MyObservable::MyCustomMessage(12));
   ASSERT_EQ(12, testCounter);
 
   // the connection is permanent; if we emit the same message again, the observer will be notified again
   testCounter = 0;
-  observable.EmitMessage(MyObservable::MyCustomMessage(20));
+  observable.BroadcastMessage(MyObservable::MyCustomMessage(20));
   ASSERT_EQ(20, testCounter);
 }
 
@@ -324,7 +324,7 @@
   observable.RegisterObserverCallback(new LambdaCallable<MyObservable::MyCustomMessage>(*observer, [&](const MyObservable::MyCustomMessage& message) {testCounter += 2 * message.payload_;}));
 
   testCounter = 0;
-  observable.EmitMessage(MyObservable::MyCustomMessage(12));
+  observable.BroadcastMessage(MyObservable::MyCustomMessage(12));
   ASSERT_EQ(24, testCounter);
 
   // delete the observer and check that the callback is not called anymore
@@ -332,7 +332,7 @@
 
   // the connection is permanent; if we emit the same message again, the observer will be notified again
   testCounter = 0;
-  observable.EmitMessage(MyObservable::MyCustomMessage(20));
+  observable.BroadcastMessage(MyObservable::MyCustomMessage(20));
   ASSERT_EQ(0, testCounter);
 }
 
@@ -362,7 +362,7 @@
   MyObserverWithLambda*   observer = new MyObserverWithLambda(broker, 3, observable);
 
   testCounter = 0;
-  observable.EmitMessage(MyObservable::MyCustomMessage(12));
+  observable.BroadcastMessage(MyObservable::MyCustomMessage(12));
   ASSERT_EQ(36, testCounter);
 
   // delete the observer and check that the callback is not called anymore
@@ -370,7 +370,7 @@
 
   // the connection is permanent; if we emit the same message again, the observer will be notified again
   testCounter = 0;
-  observable.EmitMessage(MyObservable::MyCustomMessage(20));
+  observable.BroadcastMessage(MyObservable::MyCustomMessage(20));
   ASSERT_EQ(0, testCounter);
 }